aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.js8
-rw-r--r--src/App.scss18
-rw-r--r--src/App.vue3
-rw-r--r--src/boot/after_store.js4
-rw-r--r--src/components/conversation/conversation.js19
-rw-r--r--src/components/follow_list/follow_list.js3
-rw-r--r--src/components/follow_list/follow_list.vue2
-rw-r--r--src/components/settings/settings.js3
-rw-r--r--src/components/settings/settings.vue2
-rw-r--r--src/components/status/status.js25
-rw-r--r--src/components/status/status.vue6
-rw-r--r--src/components/user_card/user_card.js29
-rw-r--r--src/components/user_card/user_card.vue117
-rw-r--r--src/components/user_card_content/user_card_content.js67
-rw-r--r--src/components/user_card_content/user_card_content.vue2
-rw-r--r--src/components/user_profile/user_profile.js2
-rw-r--r--src/components/user_settings/user_settings.vue6
-rw-r--r--src/i18n/en.json2
-rw-r--r--src/i18n/es.json1
-rw-r--r--src/i18n/ja.json28
-rw-r--r--src/lib/persisted_state.js2
-rw-r--r--src/modules/instance.js6
-rw-r--r--src/modules/statuses.js2
-rw-r--r--src/services/api/api.service.js2
-rw-r--r--src/services/follow_manipulate/follow_manipulate.js74
-rw-r--r--src/services/matcher/matcher.service.js23
-rw-r--r--src/services/mention_matcher/mention_matcher.js9
-rw-r--r--src/services/style_setter/style_setter.js2
28 files changed, 321 insertions, 146 deletions
diff --git a/src/App.js b/src/App.js
index 200254f3..214e0f48 100644
--- a/src/App.js
+++ b/src/App.js
@@ -66,12 +66,16 @@ export default {
})
},
logo () { return this.$store.state.instance.logo },
- style () {
+ bgStyle () {
return {
- '--body-background-image': `url(${this.background})`,
'background-image': `url(${this.background})`
}
},
+ bgAppStyle () {
+ return {
+ '--body-background-image': `url(${this.background})`
+ }
+ },
sitename () { return this.$store.state.instance.name },
chat () { return this.$store.state.chat.channel.state === 'joined' },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
diff --git a/src/App.scss b/src/App.scss
index 1eaed6ea..e7784329 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1,15 +1,21 @@
@import './_variables.scss';
#app {
- background-size: cover;
- background-attachment: fixed;
- background-repeat: no-repeat;
- background-position: 0 50px;
min-height: 100vh;
max-width: 100%;
overflow: hidden;
}
+.app-bg-wrapper {
+ position: fixed;
+ z-index: -1;
+ height: 100%;
+ width: 100%;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: 0 50%;
+}
+
i {
user-select: none;
}
@@ -733,3 +739,7 @@ nav {
width: 100%;
}
}
+
+.btn.btn-default {
+ min-height: 28px;
+}
diff --git a/src/App.vue b/src/App.vue
index 7541928f..acbbeb75 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,5 +1,6 @@
<template>
- <div id="app" v-bind:style="style">
+ <div id="app" v-bind:style="bgAppStyle">
+ <div class="app-bg-wrapper" v-bind:style="bgStyle"></div>
<nav class='nav-bar container' @click="scrollToTop()" id="nav">
<div class='logo' :style='logoBgStyle'>
<div class='mask' :style='logoMaskStyle'></div>
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index fa44dace..53ecc083 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -55,7 +55,6 @@ const afterStoreSetup = ({ store, i18n }) => {
}
copyInstanceOption('nsfwCensorImage')
- copyInstanceOption('theme')
copyInstanceOption('background')
copyInstanceOption('hidePostStats')
copyInstanceOption('hideUserStats')
@@ -96,6 +95,9 @@ const afterStoreSetup = ({ store, i18n }) => {
store.dispatch('initializeSocket')
}
+ return store.dispatch('setTheme', config['theme'])
+ })
+ .then(() => {
const router = new VueRouter({
mode: 'history',
routes: routes(store),
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index c18781de..48b8aaaa 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -9,9 +9,9 @@ const sortById = (a, b) => {
if (isSeqA && isSeqB) {
return seqA < seqB ? -1 : 1
} else if (isSeqA && !isSeqB) {
- return 1
- } else if (!isSeqA && isSeqB) {
return -1
+ } else if (!isSeqA && isSeqB) {
+ return 1
} else {
return a.id < b.id ? -1 : 1
}
@@ -36,6 +36,13 @@ const conversation = {
status () {
return this.statusoid
},
+ statusId () {
+ if (this.statusoid.retweeted_status) {
+ return this.statusoid.retweeted_status.id
+ } else {
+ return this.statusoid.id
+ }
+ },
conversation () {
if (!this.status) {
return []
@@ -79,7 +86,7 @@ const conversation = {
const conversationId = this.status.statusnet_conversation_id
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
- .then(() => this.setHighlight(this.statusoid.id))
+ .then(() => this.setHighlight(this.statusId))
} else {
const id = this.$route.params.id
this.$store.state.api.backendInteractor.fetchStatus({id})
@@ -91,11 +98,7 @@ const conversation = {
return this.replies[id] || []
},
focused (id) {
- if (this.statusoid.retweeted_status) {
- return (id === this.statusoid.retweeted_status.id)
- } else {
- return (id === this.statusoid.id)
- }
+ return id === this.statusId
},
setHighlight (id) {
this.highlight = id
diff --git a/src/components/follow_list/follow_list.js b/src/components/follow_list/follow_list.js
index 6d00eb94..51327e2f 100644
--- a/src/components/follow_list/follow_list.js
+++ b/src/components/follow_list/follow_list.js
@@ -25,6 +25,9 @@ const FollowList = {
},
entries () {
return this.showFollowers ? this.user.followers : this.user.friends
+ },
+ showFollowsYou () {
+ return !this.showFollowers || (this.showFollowers && this.userId !== this.$store.state.users.currentUser.id)
}
},
methods: {
diff --git a/src/components/follow_list/follow_list.vue b/src/components/follow_list/follow_list.vue
index 24ab97d8..27102edf 100644
--- a/src/components/follow_list/follow_list.vue
+++ b/src/components/follow_list/follow_list.vue
@@ -3,7 +3,7 @@
<user-card
v-for="entry in entries"
:key="entry.id" :user="entry"
- :showFollows="true"
+ :noFollowsYou="!showFollowsYou"
/>
<div class="text-center panel-footer">
<a v-if="error" @click="fetchEntries" class="alert error">
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index 534a9839..23c1acdb 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -91,7 +91,8 @@ const settings = {
},
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
- }
+ },
+ instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }
},
watch: {
hideAttachmentsLocal (value) {
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index dfb2e49d..e5f8fefb 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -27,7 +27,7 @@
<li>
<interface-language-switcher />
</li>
- <li>
+ <li v-if="instanceSpecificPanelPresent">
<input type="checkbox" id="hideISP" v-model="hideISPLocal">
<label for="hideISP">{{$t('settings.hide_isp')}}</label>
</li>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 8181b0db..0273a5be 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -10,8 +10,8 @@ import LinkPreview from '../link-preview/link-preview.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
-import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js'
-import { filter, find } from 'lodash'
+import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
+import { filter, find, unescape } from 'lodash'
const Status = {
name: 'Status',
@@ -209,14 +209,15 @@ const Status = {
},
replySubject () {
if (!this.status.summary) return ''
+ const decodedSummary = unescape(this.status.summary)
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
? this.$store.state.instance.subjectLineBehavior
: this.$store.state.config.subjectLineBehavior
- const startsWithRe = this.status.summary.match(/^re[: ]/i)
+ const startsWithRe = decodedSummary.match(/^re[: ]/i)
if (behavior !== 'noop' && startsWithRe || behavior === 'masto') {
- return this.status.summary
+ return decodedSummary
} else if (behavior === 'email') {
- return 're: '.concat(this.status.summary)
+ return 're: '.concat(decodedSummary)
} else if (behavior === 'noop') {
return ''
}
@@ -281,7 +282,7 @@ const Status = {
}
if (target.tagName === 'A') {
if (target.className.match(/mention/)) {
- const href = target.getAttribute('href')
+ const href = target.href
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
if (attn) {
event.stopPropagation()
@@ -291,6 +292,15 @@ const Status = {
return
}
}
+ if (target.className.match(/hashtag/)) {
+ // Extract tag name from link url
+ const tag = extractTagFromUrl(target.href)
+ if (tag) {
+ const link = this.generateTagLink(tag)
+ this.$router.push(link)
+ return
+ }
+ }
window.open(target.href, '_blank')
}
},
@@ -347,6 +357,9 @@ const Status = {
generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},
+ generateTagLink (tag) {
+ return `/tag/${tag}`
+ },
setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => this.$store.dispatch('setMedia', attachments)
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 5bc7c664..3fc5b486 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -56,7 +56,7 @@
</div>
<h4 class="replies" v-if="inConversation && !noReplyLinks">
<small v-if="replies.length">Replies:</small>
- <small class="reply-link" v-for="reply in replies">
+ <small class="reply-link" v-bind:key="reply.id" v-for="reply in replies">
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}&nbsp;</a>
</small>
</h4>
@@ -438,6 +438,8 @@
.user-name {
font-weight: bold;
+ overflow: hidden;
+ text-overflow: ellipsis;
img {
width: 14px;
@@ -552,7 +554,7 @@ a.unmute {
.timeline > {
.status-el:last-child {
- border-bottom-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;;
+ border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
border-bottom: none;
}
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index ecc36a4d..28e22f09 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -1,16 +1,20 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
const UserCard = {
props: [
'user',
- 'showFollows',
+ 'noFollowsYou',
'showApproval'
],
data () {
return {
- userExpanded: false
+ userExpanded: false,
+ followRequestInProgress: false,
+ followRequestSent: false,
+ updated: false
}
},
components: {
@@ -18,7 +22,11 @@ const UserCard = {
UserAvatar
},
computed: {
- currentUser () { return this.$store.state.users.currentUser }
+ currentUser () { return this.$store.state.users.currentUser },
+ following () { return this.updated ? this.updated.following : this.user.following },
+ showFollow () {
+ return !this.showApproval && (!this.following || this.updated && !this.updated.following)
+ }
},
methods: {
toggleUserExpanded () {
@@ -34,6 +42,21 @@ const UserCard = {
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
+ },
+ followUser () {
+ this.followRequestInProgress = true
+ requestFollow(this.user, this.$store).then(({ sent, updated }) => {
+ this.followRequestInProgress = false
+ this.followRequestSent = sent
+ this.updated = updated
+ })
+ },
+ unfollowUser () {
+ this.followRequestInProgress = true
+ requestUnfollow(this.user, this.$store).then(({ updated }) => {
+ this.followRequestInProgress = false
+ this.updated = updated
+ })
}
}
}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 7129430b..ce4edb3c 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -1,32 +1,57 @@
<template>
<div class="card">
<router-link :to="userProfileLink(user)">
- <UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
+ <UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
</router-link>
- <div class="usercard" v-if="userExpanded">
- <user-card-content :user="user" :switcher="false"></user-card-content>
- </div>
- <div class="name-and-screen-name" v-else>
- <div :title="user.name" v-if="user.name_html" class="user-name">
- <span v-html="user.name_html"></span>
- <span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
- {{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
- </span>
+ <div class="user-card-main-content">
+ <div class="usercard" v-if="userExpanded">
+ <user-card-content :user="user" :switcher="false"></user-card-content>
</div>
- <div :title="user.name" v-else class="user-name">
- {{ user.name }}
- <span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
+ <div class="name-and-screen-name" v-if="!userExpanded">
+ <div :title="user.name" class="user-name">
+ <span v-if="user.name_html" v-html="user.name_html"></span>
+ <span v-else>{{ user.name }}</span>
+ </div>
+ <div class="user-link-action">
+ <router-link class='user-screen-name' :to="userProfileLink(user)">
+ @{{user.screen_name}}
+ </router-link>
+ </div>
+ </div>
+ <div class="follow-box" v-if="!userExpanded">
+ <span class="faint" v-if="!noFollowsYou && user.follows_you">
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
+ <button
+ v-if="showFollow"
+ class="btn btn-default"
+ @click="followUser"
+ :disabled="followRequestInProgress"
+ :title="followRequestSent ? $t('user_card.follow_again') : ''"
+ >
+ <template v-if="followRequestInProgress">
+ {{ $t('user_card.follow_progress') }}
+ </template>
+ <template v-else-if="followRequestSent">
+ {{ $t('user_card.follow_sent') }}
+ </template>
+ <template v-else>
+ {{ $t('user_card.follow') }}
+ </template>
+ </button>
+ <button v-if="following" class="btn btn-default pressed" @click="unfollowUser" :disabled="followRequestInProgress">
+ <template v-if="followRequestInProgress">
+ {{ $t('user_card.follow_progress') }}
+ </template>
+ <template v-else>
+ {{ $t('user_card.follow_unfollow') }}
+ </template>
+ </button>
+ </div>
+ <div class="approval" v-if="showApproval">
+ <button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
+ <button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
</div>
-
- <router-link class='user-screen-name' :to="userProfileLink(user)">
- @{{user.screen_name}}
- </router-link>
- </div>
- <div class="approval" v-if="showApproval">
- <button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
- <button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
</div>
</div>
</template>
@@ -36,11 +61,18 @@
<style lang="scss">
@import '../../_variables.scss';
-.name-and-screen-name {
+.user-card-main-content {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 100%;
margin-left: 0.7em;
- margin-top:0.0em;
+ min-width: 0;
+}
+
+.name-and-screen-name {
text-align: left;
width: 100%;
+
.user-name {
img {
object-fit: contain;
@@ -49,13 +81,15 @@
vertical-align: middle;
}
}
-}
-.follows-you {
- margin-left: 2em;
- float: right;
+ .user-link-action {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ }
}
+
.card {
display: flex;
flex: 1 0;
@@ -66,16 +100,31 @@
border-bottom: 1px solid;
margin: 0;
border-bottom-color: $fallback--border;
- border-bottom-color: var(--border, $fallback--border);
+ border-bottom-color: var(--border, $fallback--border);
.avatar {
padding: 0;
}
+
+ .follow-box {
+ text-align: center;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ line-height: 1.5em;
+
+ .btn {
+ margin-top: 0.5em;
+ margin-left: auto;
+ width: 10em;
+ }
+ }
}
.usercard {
width: fill-available;
- margin: 0.2em 0 0 0.7em;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
@@ -96,9 +145,15 @@
}
.approval {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
button {
- width: 100%;
- margin-bottom: 0.5em;
+ margin-top: 0.5em;
+ margin-right: 0.5em;
+ flex: 1 1;
+ max-width: 12em;
+ min-width: 8em;
}
}
</style>
diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js
index 1888f8c6..7a7b89d4 100644
--- a/src/components/user_card_content/user_card_content.js
+++ b/src/components/user_card_content/user_card_content.js
@@ -1,5 +1,6 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js'
+import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
export default {
@@ -92,69 +93,17 @@ export default {
},
methods: {
followUser () {
- const store = this.$store
this.followRequestInProgress = true
- store.state.api.backendInteractor.followUser(this.user.id)
- .then((followedUser) => store.commit('addNewUsers', [followedUser]))
- .then(() => {
- // For locked users we just mark it that we sent the follow request
- if (this.user.locked) {
- this.followRequestInProgress = false
- this.followRequestSent = true
- return
- }
-
- if (this.user.following) {
- // If we get result immediately, just stop.
- this.followRequestInProgress = false
- return
- }
-
- // But usually we don't get result immediately, so we ask server
- // for updated user profile to confirm if we are following them
- // Sometimes it takes several tries. Sometimes we end up not following
- // user anyway, probably because they locked themselves and we
- // don't know that yet.
- // Recursive Promise, it will call itself up to 3 times.
- const fetchUser = (attempt) => new Promise((resolve, reject) => {
- setTimeout(() => {
- store.state.api.backendInteractor.fetchUser({ id: this.user.id })
- .then((user) => store.commit('addNewUsers', [user]))
- .then(() => resolve([this.user.following, attempt]))
- .catch((e) => reject(e))
- }, 500)
- }).then(([following, attempt]) => {
- if (!following && attempt <= 3) {
- // If we BE reports that we still not following that user - retry,
- // increment attempts by one
- return fetchUser(++attempt)
- } else {
- // If we run out of attempts, just return whatever status is.
- return following
- }
- })
-
- return fetchUser(1)
- .then((following) => {
- if (following) {
- // We confirmed and everything its good.
- this.followRequestInProgress = false
- } else {
- // If after all the tries, just treat it as if user is locked
- this.followRequestInProgress = false
- this.followRequestSent = true
- }
- })
- })
+ requestFollow(this.user, this.$store).then(({sent}) => {
+ this.followRequestInProgress = false
+ this.followRequestSent = sent
+ })
},
unfollowUser () {
- const store = this.$store
this.followRequestInProgress = true
- store.state.api.backendInteractor.unfollowUser(this.user.id)
- .then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
- .then(() => {
- this.followRequestInProgress = false
- })
+ requestUnfollow(this.user, this.$store).then(() => {
+ this.followRequestInProgress = false
+ })
},
blockUser () {
const store = this.$store
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index 7f9909c4..4d1950c5 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -386,6 +386,4 @@
}
}
-.floater {
-}
</style>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index a22b8722..37179ce1 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -16,7 +16,7 @@ const UserProfile = {
}
},
destroyed () {
- this.cleanUp(this.userId)
+ this.cleanUp()
},
computed: {
timeline () {
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index ea5b3de5..d2381da2 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -178,7 +178,7 @@
}
.banner {
- max-width: 400px;
+ max-width: 100%;
}
.uploading {
@@ -189,5 +189,9 @@
.name-changer {
width: 100%;
}
+
+ .bg {
+ max-width: 100%;
+ }
}
</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index c664fbfa..eba90b50 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -344,7 +344,7 @@
"follow_sent": "Request sent!",
"follow_progress": "Requesting…",
"follow_again": "Send request again?",
- "follow_unfollow": "Stop following",
+ "follow_unfollow": "Unfollow",
"followees": "Following",
"followers": "Followers",
"following": "Following!",
diff --git a/src/i18n/es.json b/src/i18n/es.json
index 29c8aec4..d14e7a31 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -140,6 +140,7 @@
"use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click.",
"hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)",
"hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
+ "hide_filtered_statuses": "Ocultar estados filtrados",
"import_followers_from_a_csv_file": "Importar personas que tú sigues a partir de un archivo csv",
"import_theme": "Importar tema",
"inputRadius": "Campos de entrada",
diff --git a/src/i18n/ja.json b/src/i18n/ja.json
index afce03a4..7849aa20 100644
--- a/src/i18n/ja.json
+++ b/src/i18n/ja.json
@@ -17,7 +17,9 @@
},
"general": {
"apply": "てきよう",
- "submit": "そうしん"
+ "submit": "そうしん",
+ "more": "つづき",
+ "generic_error": "エラーになりました"
},
"login": {
"login": "ログイン",
@@ -26,7 +28,8 @@
"password": "パスワード",
"placeholder": "れい: lain",
"register": "はじめる",
- "username": "ユーザーめい"
+ "username": "ユーザーめい",
+ "hint": "はなしあいにくわわるには、ログインしてください"
},
"nav": {
"about": "これはなに?",
@@ -49,7 +52,8 @@
"load_older": "ふるいつうちをみる",
"notifications": "つうち",
"read": "よんだ!",
- "repeated_you": "あなたのステータスがリピートされました"
+ "repeated_you": "あなたのステータスがリピートされました",
+ "no_more_notifications": "つうちはありません"
},
"post_status": {
"new_status": "とうこうする",
@@ -117,6 +121,7 @@
"delete_account_description": "あなたのアカウントとメッセージが、きえます。",
"delete_account_error": "アカウントをけすことが、できなかったかもしれません。インスタンスのかんりしゃに、れんらくしてください。",
"delete_account_instructions": "ほんとうにアカウントをけしてもいいなら、パスワードをかいてください。",
+ "avatar_size_instruction": "アバターのおおきさは、150×150ピクセルか、それよりもおおきくするといいです。",
"export_theme": "セーブ",
"filtering": "フィルタリング",
"filtering_explanation": "これらのことばをふくむすべてのものがミュートされます。1ぎょうに1つのことばをかいてください。",
@@ -132,8 +137,10 @@
"hide_attachments_in_tl": "タイムラインのファイルをかくす",
"hide_isp": "インスタンススペシフィックパネルをかくす",
"preload_images": "がぞうをさきよみする",
+ "use_one_click_nsfw": "NSFWなファイルを1クリックでひらく",
"hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)",
"hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)",
+ "hide_filtered_statuses": "フィルターされたとうこうをかくす",
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
"import_theme": "ロード",
"inputRadius": "インプットフィールド",
@@ -148,6 +155,8 @@
"lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできる",
"loop_video": "ビデオをくりかえす",
"loop_video_silent_only": "おとのないビデオだけくりかえす",
+ "play_videos_in_modal": "ビデオをメディアビューアーでみる",
+ "use_contain_fit": "がぞうのサムネイルを、きりぬかない",
"name": "なまえ",
"name_bio": "なまえとプロフィール",
"new_password": "あたらしいパスワード",
@@ -157,8 +166,10 @@
"notification_visibility_mentions": "メンション",
"notification_visibility_repeats": "リピート",
"no_rich_text_description": "リッチテキストをつかわない",
- "hide_follows_description": "フォローしている人を表示しない",
- "hide_followers_description": "フォローしている人を表示しない",
+ "hide_follows_description": "フォローしているひとをみせない",
+ "hide_followers_description": "フォロワーをみせない",
+ "show_admin_badge": "アドミンのしるしをみる",
+ "show_moderator_badge": "モデレーターのしるしをみる",
"nsfw_clickthrough": "NSFWなファイルをかくす",
"panelRadius": "パネル",
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
@@ -185,6 +196,8 @@
"subject_line_email": "メールふう: \"re: サブジェクト\"",
"subject_line_mastodon": "マストドンふう: そのままコピー",
"subject_line_noop": "コピーしない",
+ "post_status_content_type": "とうこうのコンテントタイプ",
+ "status_content_type_plain": "プレーンテキスト",
"stop_gifs": "カーソルをかさねたとき、GIFをうごかす",
"streaming": "うえまでスクロールしたとき、じどうてきにストリーミングする",
"text": "もじ",
@@ -318,13 +331,15 @@
"no_retweet_hint": "とうこうを「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります",
"repeated": "リピート",
"show_new": "よみこみ",
- "up_to_date": "さいしん"
+ "up_to_date": "さいしん",
+ "no_more_statuses": "これでおわりです"
},
"user_card": {
"approve": "うけいれ",
"block": "ブロック",
"blocked": "ブロックしています!",
"deny": "おことわり",
+ "favorites": "おきにいり",
"follow": "フォロー",
"follow_sent": "リクエストを、おくりました!",
"follow_progress": "リクエストしています…",
@@ -335,6 +350,7 @@
"following": "フォローしています!",
"follows_you": "フォローされました!",
"its_you": "これはあなたです!",
+ "media": "メディア",
"mute": "ミュート",
"muted": "ミュートしています!",
"per_day": "/日",
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index ccd92633..6f7202ce 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -48,7 +48,7 @@ export default function createPersistedState ({
return getState(key, storage).then((savedState) => {
return store => {
try {
- if (typeof savedState === 'object') {
+ if (savedState !== null && typeof savedState === 'object') {
// build user cache
const usersState = savedState.users || {}
usersState.usersObject = {}
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 9bef5235..59c6b91c 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -66,9 +66,11 @@ const instance = {
case 'name':
dispatch('setPageTitle')
break
- case 'theme':
- setPreset(value, commit)
}
+ },
+ setTheme ({ commit }, themeName) {
+ commit('setInstanceOption', { name: 'theme', value: themeName })
+ return setPreset(themeName, commit)
}
}
}
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 56619455..46117fd7 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -296,7 +296,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
notifObj.image = action.attachments[0].url
}
- if (notification.fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) {
+ if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
let notification = new window.Notification(title, notifObj)
// Chrome is known for not closing notifications automatically
// according to MDN, anyway.
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 92daa04e..13d31d91 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -258,7 +258,7 @@ const fetchFriends = ({id, page, credentials}) => {
}
const exportFriends = ({id, credentials}) => {
- let url = `${FRIENDS_URL}?user_id=${id}&export=true`
+ let url = `${FRIENDS_URL}?user_id=${id}&all=true`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((data) => data.map(parseUser))
diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js
new file mode 100644
index 00000000..1e9bd679
--- /dev/null
+++ b/src/services/follow_manipulate/follow_manipulate.js
@@ -0,0 +1,74 @@
+const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
+ setTimeout(() => {
+ store.state.api.backendInteractor.fetchUser({ id: user.id })
+ .then((user) => store.commit('addNewUsers', [user]))
+ .then(() => resolve([user.following, attempt]))
+ .catch((e) => reject(e))
+ }, 500)
+}).then(([following, attempt]) => {
+ if (!following && attempt <= 3) {
+ // If we BE reports that we still not following that user - retry,
+ // increment attempts by one
+ return fetchUser(++attempt, user, store)
+ } else {
+ // If we run out of attempts, just return whatever status is.
+ return following
+ }
+})
+
+export const requestFollow = (user, store) => new Promise((resolve, reject) => {
+ store.state.api.backendInteractor.followUser(user.id)
+ .then((updated) => {
+ store.commit('addNewUsers', [updated])
+
+ // For locked users we just mark it that we sent the follow request
+ if (updated.locked) {
+ resolve({
+ sent: true,
+ updated
+ })
+ }
+
+ if (updated.following) {
+ // If we get result immediately, just stop.
+ resolve({
+ sent: false,
+ updated
+ })
+ }
+
+ // But usually we don't get result immediately, so we ask server
+ // for updated user profile to confirm if we are following them
+ // Sometimes it takes several tries. Sometimes we end up not following
+ // user anyway, probably because they locked themselves and we
+ // don't know that yet.
+ // Recursive Promise, it will call itself up to 3 times.
+
+ return fetchUser(1, user, store)
+ .then((following) => {
+ if (following) {
+ // We confirmed and everything's good.
+ resolve({
+ sent: false,
+ updated
+ })
+ } else {
+ // If after all the tries, just treat it as if user is locked
+ resolve({
+ sent: false,
+ updated
+ })
+ }
+ })
+ })
+})
+
+export const requestUnfollow = (user, store) => new Promise((resolve, reject) => {
+ store.state.api.backendInteractor.unfollowUser(user.id)
+ .then((updated) => {
+ store.commit('addNewUsers', [updated])
+ resolve({
+ updated
+ })
+ })
+})
diff --git a/src/services/matcher/matcher.service.js b/src/services/matcher/matcher.service.js
new file mode 100644
index 00000000..b6c4e909
--- /dev/null
+++ b/src/services/matcher/matcher.service.js
@@ -0,0 +1,23 @@
+export const mentionMatchesUrl = (attention, url) => {
+ if (url === attention.statusnet_profile_url) {
+ return true
+ }
+ const [namepart, instancepart] = attention.screen_name.split('@')
+ const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
+
+ return !!url.match(matchstring)
+}
+
+/**
+ * Extract tag name from pleroma or mastodon url.
+ * i.e https://bikeshed.party/tag/photo or https://quey.org/tags/sky
+ * @param {string} url
+ */
+export const extractTagFromUrl = (url) => {
+ const regex = /tag[s]*\/(\w+)$/g
+ const result = regex.exec(url)
+ if (!result) {
+ return false
+ }
+ return result[1]
+}
diff --git a/src/services/mention_matcher/mention_matcher.js b/src/services/mention_matcher/mention_matcher.js
deleted file mode 100644
index 2c1ed970..00000000
--- a/src/services/mention_matcher/mention_matcher.js
+++ /dev/null
@@ -1,9 +0,0 @@
-
-export const mentionMatchesUrl = (attention, url) => {
- if (url === attention.statusnet_profile_url) {
- return true
- }
- const [namepart, instancepart] = attention.screen_name.split('@')
- const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
- return !!url.match(matchstring)
-}
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 10e7ed9b..d0b6ccbf 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -480,7 +480,7 @@ const getThemes = () => {
}
const setPreset = (val, commit) => {
- getThemes().then((themes) => {
+ return getThemes().then((themes) => {
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
const isV1 = Array.isArray(theme)
const data = isV1 ? {} : theme.theme