diff options
Diffstat (limited to 'src')
38 files changed, 370 insertions, 139 deletions
@@ -82,7 +82,7 @@ export default { unseenNotificationsCount () { return this.unseenNotifications.length }, - showFeaturesPanel () { return this.$store.state.config.showFeaturesPanel } + showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel } }, methods: { scrollToTop () { diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 60cee1bf..53ecc083 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -58,6 +58,7 @@ const afterStoreSetup = ({ store, i18n }) => { copyInstanceOption('background') copyInstanceOption('hidePostStats') copyInstanceOption('hideUserStats') + copyInstanceOption('hideFilteredStatuses') copyInstanceOption('logo') store.dispatch('setInstanceOption', { @@ -83,8 +84,10 @@ const afterStoreSetup = ({ store, i18n }) => { copyInstanceOption('loginMethod') copyInstanceOption('scopeCopy') copyInstanceOption('subjectLineBehavior') + copyInstanceOption('postContentType') copyInstanceOption('alwaysShowSubjectInput') copyInstanceOption('noAttachmentLinks') + copyInstanceOption('showFeaturesPanel') if ((config.chatDisabled)) { store.dispatch('disableChat') diff --git a/src/components/about/about.js b/src/components/about/about.js index b1ce3c7d..ae1cb182 100644 --- a/src/components/about/about.js +++ b/src/components/about/about.js @@ -9,7 +9,7 @@ const About = { TermsOfServicePanel }, computed: { - showFeaturesPanel () { return this.$store.state.config.showFeaturesPanel } + showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel } } } diff --git a/src/components/follow_list/follow_list.js b/src/components/follow_list/follow_list.js index 6d00eb94..acdb216d 100644 --- a/src/components/follow_list/follow_list.js +++ b/src/components/follow_list/follow_list.js @@ -25,7 +25,8 @@ const FollowList = { }, entries () { return this.showFollowers ? this.user.followers : this.user.friends - } + }, + showActions () { return this.$store.state.users.currentUser.id === this.userId } }, methods: { fetchEntries () { diff --git a/src/components/follow_list/follow_list.vue b/src/components/follow_list/follow_list.vue index 24ab97d8..7be2e7b7 100644 --- a/src/components/follow_list/follow_list.vue +++ b/src/components/follow_list/follow_list.vue @@ -3,7 +3,8 @@ <user-card v-for="entry in entries" :key="entry.id" :user="entry" - :showFollows="true" + :showFollows="!showFollowers" + :showActions="showActions" /> <div class="text-center panel-footer"> <a v-if="error" @click="fetchEntries" class="alert error"> diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 5e8c2252..ab379c23 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -65,7 +65,6 @@ const PostStatusForm = { newStatus: { spoilerText: this.subject || '', status: statusText, - contentType: 'text/plain', nsfw: false, files: [], visibility: scope @@ -167,6 +166,11 @@ const PostStatusForm = { }, formattingOptionsEnabled () { return this.$store.state.instance.formattingOptionsEnabled + }, + defaultPostContentType () { + return typeof this.$store.state.config.postContentType === 'undefined' + ? this.$store.state.instance.postContentType + : this.$store.state.config.postContentType } }, methods: { diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index e09ad37f..6ed5d92e 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -35,7 +35,7 @@ <div class="visibility-tray"> <span class="text-format" v-if="formattingOptionsEnabled"> <label for="post-content-type" class="select"> - <select id="post-content-type" v-model="newStatus.contentType" class="form-control"> + <select id="post-content-type" v-model="defaultPostContentType" class="form-control"> <option value="text/plain">{{$t('post_status.content_type.plain_text')}}</option> <option value="text/html">HTML</option> <option value="text/markdown">Markdown</option> diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.js b/src/components/public_and_external_timeline/public_and_external_timeline.js index 0db6efae..d45677e0 100644 --- a/src/components/public_and_external_timeline/public_and_external_timeline.js +++ b/src/components/public_and_external_timeline/public_and_external_timeline.js @@ -7,7 +7,7 @@ const PublicAndExternalTimeline = { timeline () { return this.$store.state.statuses.timelines.publicAndExternal } }, created () { - this.$store.dispatch('startFetching', 'publicAndExternal') + this.$store.dispatch('startFetching', { timeline: 'publicAndExternal' }) }, destroyed () { this.$store.dispatch('stopFetching', 'publicAndExternal') diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.vue b/src/components/public_and_external_timeline/public_and_external_timeline.vue index aded2ead..6be9f955 100644 --- a/src/components/public_and_external_timeline/public_and_external_timeline.vue +++ b/src/components/public_and_external_timeline/public_and_external_timeline.vue @@ -1,5 +1,5 @@ <template> - <Timeline :title="$t('nav.twkn')"v-bind:timeline="timeline" v-bind:timeline-name="'publicAndExternal'"/> + <Timeline :title="$t('nav.twkn')" v-bind:timeline="timeline" v-bind:timeline-name="'publicAndExternal'"/> </template> <script src="./public_and_external_timeline.js"></script> diff --git a/src/components/public_timeline/public_timeline.js b/src/components/public_timeline/public_timeline.js index 9b866be8..64c951ac 100644 --- a/src/components/public_timeline/public_timeline.js +++ b/src/components/public_timeline/public_timeline.js @@ -7,7 +7,7 @@ const PublicTimeline = { timeline () { return this.$store.state.statuses.timelines.public } }, created () { - this.$store.dispatch('startFetching', 'public') + this.$store.dispatch('startFetching', { timeline: 'public' }) }, destroyed () { this.$store.dispatch('stopFetching', 'public') diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index 8d138485..534a9839 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -27,6 +27,11 @@ const settings = { : user.hideUserStats, hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats), + hideFilteredStatusesLocal: typeof user.hideFilteredStatuses === 'undefined' + ? instance.hideFilteredStatuses + : user.hideFilteredStatuses, + hideFilteredStatusesDefault: this.$t('settings.values.' + instance.hideFilteredStatuses), + notificationVisibilityLocal: user.notificationVisibility, replyVisibilityLocal: user.replyVisibility, loopVideoLocal: user.loopVideo, @@ -46,6 +51,11 @@ const settings = { : user.subjectLineBehavior, subjectLineBehaviorDefault: instance.subjectLineBehavior, + postContentTypeLocal: typeof user.postContentType === 'undefined' + ? instance.postContentType + : user.postContentType, + postContentTypeDefault: instance.postContentType, + alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined' ? instance.alwaysShowSubjectInput : user.alwaysShowSubjectInput, @@ -96,6 +106,9 @@ const settings = { hideUserStatsLocal (value) { this.$store.dispatch('setOption', { name: 'hideUserStats', value }) }, + hideFilteredStatusesLocal (value) { + this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value }) + }, hideNsfwLocal (value) { this.$store.dispatch('setOption', { name: 'hideNsfw', value }) }, @@ -157,6 +170,9 @@ const settings = { subjectLineBehaviorLocal (value) { this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value }) }, + postContentTypeLocal (value) { + this.$store.dispatch('setOption', { name: 'postContentType', value }) + }, stopGifs (value) { this.$store.dispatch('setOption', { name: 'stopGifs', value }) }, diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 9953780f..dfb2e49d 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -100,6 +100,28 @@ </label> </div> </li> + <li> + <div> + {{$t('settings.post_status_content_type')}} + <label for="postContentType" class="select"> + <select id="postContentType" v-model="postContentTypeLocal"> + <option value="text/plain"> + {{$t('settings.status_content_type_plain')}} + {{postContentTypeDefault == 'text/plain' ? $t('settings.instance_default_simple') : ''}} + </option> + <option value="text/html"> + HTML + {{postContentTypeDefault == 'text/html' ? $t('settings.instance_default_simple') : ''}} + </option> + <option value="text/markdown"> + Markdown + {{postContentTypeDefault == 'text/markdown' ? $t('settings.instance_default_simple') : ''}} + </option> + </select> + <i class="icon-down-open"/> + </label> + </div> + </li> </ul> </div> @@ -205,7 +227,6 @@ </label> </li> </ul> - </label> </div> <div> {{$t('settings.replies_in_timeline')}} @@ -232,11 +253,18 @@ </div> </div> <div class="setting-item"> - <p>{{$t('settings.filtering_explanation')}}</p> - <textarea id="muteWords" v-model="muteWordsString"></textarea> + <div> + <p>{{$t('settings.filtering_explanation')}}</p> + <textarea id="muteWords" v-model="muteWordsString"></textarea> + </div> + <div> + <input type="checkbox" id="hideFilteredStatuses" v-model="hideFilteredStatusesLocal"> + <label for="hideFilteredStatuses"> + {{$t('settings.hide_filtered_statuses')}} {{$t('settings.instance_default', { value: hideFilteredStatusesDefault })}} + </label> + </div> </div> </div> - </tab-switcher> </keep-alive> </div> diff --git a/src/components/status/status.js b/src/components/status/status.js index 65ddcb9f..06e4fe93 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', @@ -110,6 +110,14 @@ const Status = { return hits }, muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) }, + hideFilteredStatuses () { + return typeof this.$store.state.config.hideFilteredStatuses === 'undefined' + ? this.$store.state.instance.hideFilteredStatuses + : this.$store.state.config.hideFilteredStatuses + }, + hideStatus () { + return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses) + }, isFocused () { // retweet or root of an expanded conversation if (this.focused) { @@ -201,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 '' } @@ -273,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() @@ -283,7 +292,14 @@ const Status = { return } } - window.open(target.href, '_blank') + 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) + } + } } }, toggleReplying () { @@ -339,6 +355,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 21eb4d56..76daf73a 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,5 +1,5 @@ <template> - <div class="status-el" v-if="!hideReply && !deleted" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]"> + <div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]"> <template v-if="muted && !noReplyLinks"> <div class="media status container muted"> <small> @@ -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}} </a> </small> </h4> diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js index f9c3f927..423df258 100644 --- a/src/components/tab_switcher/tab_switcher.js +++ b/src/components/tab_switcher/tab_switcher.js @@ -37,7 +37,7 @@ export default Vue.component('tab-switcher', { return ( <div class={ classesWrapper.join(' ')}> - <button onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button> + <button disabled={slot.data.attrs.disabled} onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button> </div> ) }) diff --git a/src/components/tag_timeline/tag_timeline.js b/src/components/tag_timeline/tag_timeline.js index 43de4f49..41b09706 100644 --- a/src/components/tag_timeline/tag_timeline.js +++ b/src/components/tag_timeline/tag_timeline.js @@ -3,7 +3,7 @@ import Timeline from '../timeline/timeline.vue' const TagTimeline = { created () { this.$store.commit('clearTimeline', { timeline: 'tag' }) - this.$store.dispatch('startFetching', { 'tag': this.tag }) + this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag }) }, components: { Timeline @@ -15,7 +15,7 @@ const TagTimeline = { watch: { tag () { this.$store.commit('clearTimeline', { timeline: 'tag' }) - this.$store.dispatch('startFetching', { 'tag': this.tag }) + this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag }) } }, destroyed () { diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index ecc36a4d..a4c84716 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -1,16 +1,21 @@ 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', - 'showApproval' + 'showApproval', + 'showActions' ], data () { return { - userExpanded: false + userExpanded: false, + followRequestInProgress: false, + followRequestSent: false, + updated: false } }, components: { @@ -18,7 +23,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.showActions && (!this.showFollows && !this.following || this.updated && !this.updated.following) + } }, methods: { toggleUserExpanded () { @@ -34,6 +43,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..12960c02 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -7,22 +7,43 @@ <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> + <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> <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> - <div :title="user.name" v-else class="user-name"> - {{ user.name }} - <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-link-action"> + <router-link class='user-screen-name' :to="userProfileLink(user)"> + @{{user.screen_name}} + </router-link> + <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="showActions && showFollows && following" class="btn btn-default" @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> - - <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> @@ -42,6 +63,9 @@ text-align: left; width: 100%; .user-name { + display: flex; + justify-content: space-between; + img { object-fit: contain; height: 16px; @@ -49,11 +73,20 @@ vertical-align: middle; } } + + .user-link-action { + display: flex; + align-items: flex-start; + justify-content: space-between; + + button { + margin-top: 3px; + } + } } .follows-you { margin-left: 2em; - float: right; } .card { diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js index 6f6d04a7..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 { @@ -79,6 +80,12 @@ export default { set (color) { this.$store.dispatch('setHighlight', { user: this.user.screen_name, color }) } + }, + visibleRole () { + const validRole = (this.user.role === 'admin' || this.user.role === 'moderator') + const showRole = this.isOtherUser || this.user.show_role + + return validRole && showRole && this.user.role } }, components: { @@ -86,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 ce65ec2f..7f9909c4 100644 --- a/src/components/user_card_content/user_card_content.vue +++ b/src/components/user_card_content/user_card_content.vue @@ -19,7 +19,9 @@ </div> <router-link class='user-screen-name' :to="userProfileLink(user)"> - <span class="handle">@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span> + <span class="handle">@{{user.screen_name}} + <span class="alert staff" v-if="!hideBio && !!visibleRole">{{visibleRole}}</span> + </span><span v-if="user.locked"><i class="icon icon-lock"></i></span> <span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span> </router-link> </div> @@ -247,6 +249,15 @@ text-overflow: ellipsis; overflow: hidden; } + + // TODO use proper colors + .staff { + text-transform: capitalize; + color: $fallback--text; + color: var(--btnText, $fallback--text); + background-color: $fallback--fg; + background-color: var(--btn, $fallback--fg); + } } .user-meta { diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 7b0ab705..a22b8722 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -8,8 +8,8 @@ const UserProfile = { this.$store.commit('clearTimeline', { timeline: 'user' }) this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.commit('clearTimeline', { timeline: 'media' }) - this.$store.dispatch('startFetching', ['user', this.fetchBy]) - this.$store.dispatch('startFetching', ['media', this.fetchBy]) + this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy }) + this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy }) this.startFetchFavorites() if (!this.user.id) { this.$store.dispatch('fetchUser', this.fetchBy) @@ -58,17 +58,23 @@ const UserProfile = { }, isExternal () { return this.$route.name === 'external-user-profile' + }, + followsTabVisible () { + return this.isUs || !this.user.hide_follows + }, + followersTabVisible () { + return this.isUs || !this.user.hide_followers } }, methods: { startFetchFavorites () { if (this.isUs) { - this.$store.dispatch('startFetching', ['favorites', this.fetchBy]) + this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.fetchBy }) } }, startUp () { - this.$store.dispatch('startFetching', ['user', this.fetchBy]) - this.$store.dispatch('startFetching', ['media', this.fetchBy]) + this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy }) + this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy }) this.startFetchFavorites() }, diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 6d5b00d1..79461291 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -9,19 +9,20 @@ <tab-switcher :renderOnlyFocused="true"> <Timeline :label="$t('user_card.statuses')" + :disabled="!user.statuses_count" :embedded="true" :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="fetchBy" /> - <div :label="$t('user_card.followees')"> + <div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count"> <FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" /> <div class="userlist-placeholder" v-else> <i class="icon-spin3 animate-spin"></i> </div> </div> - <div :label="$t('user_card.followers')"> + <div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count"> <FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" /> <div class="userlist-placeholder" v-else> <i class="icon-spin3 animate-spin"></i> @@ -29,6 +30,7 @@ </div> <Timeline :label="$t('user_card.media')" + :disabled="!media.visibleStatuses.length" :embedded="true" :title="$t('user_card.media')" timeline-name="media" :timeline="media" @@ -37,6 +39,7 @@ <Timeline v-if="isUs" :label="$t('user_card.favorites')" + :disabled="!favorites.visibleStatuses.length" :embedded="true" :title="$t('user_card.favorites')" timeline-name="favorites" diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index ef9398f6..d20bf308 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -14,6 +14,8 @@ const UserSettings = { newDefaultScope: this.$store.state.users.currentUser.default_scope, hideFollows: this.$store.state.users.currentUser.hide_follows, hideFollowers: this.$store.state.users.currentUser.hide_followers, + showRole: this.$store.state.users.currentUser.show_role, + role: this.$store.state.users.currentUser.role, followList: null, followImportError: false, followsImported: false, @@ -71,6 +73,8 @@ const UserSettings = { const no_rich_text = this.newNoRichText const hide_follows = this.hideFollows const hide_followers = this.hideFollowers + const show_role = this.showRole + /* eslint-enable camelcase */ this.$store.state.api.backendInteractor .updateProfile({ @@ -83,7 +87,8 @@ const UserSettings = { default_scope, no_rich_text, hide_follows, - hide_followers + hide_followers, + show_role /* eslint-enable camelcase */ }}).then((user) => { if (!user.error) { @@ -238,7 +243,9 @@ const UserSettings = { exportFollows () { this.enableFollowsExport = false this.$store.state.api.backendInteractor - .fetchFriends({id: this.$store.state.users.currentUser.id}) + .exportFriends({ + id: this.$store.state.users.currentUser.id + }) .then((friendList) => { this.exportPeople(friendList, 'friends.csv') setTimeout(() => { this.enableFollowsExport = true }, 2000) diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 19b7bdbd..134f70ef 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -37,6 +37,11 @@ <input type="checkbox" v-model="hideFollowers" id="account-hide-followers"> <label for="account-hide-followers">{{$t('settings.hide_followers_description')}}</label> </p> + <p> + <input type="checkbox" v-model="showRole" id="account-show-role"> + <label for="account-show-role" v-if="role === 'admin'">{{$t('settings.show_admin_badge')}}</label> + <label for="account-show-role" v-if="role === 'moderator'">{{$t('settings.show_moderator_badge')}}</label> + </p> <button :disabled='newName && newName.length === 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button> </div> <div class="setting-item"> @@ -184,5 +189,9 @@ .name-changer { width: 100%; } + + .bg { + max-width: 100%; + } } </style> diff --git a/src/i18n/de.json b/src/i18n/de.json index 59449e33..d0bfba38 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -132,6 +132,7 @@ "preload_images": "Bilder vorausladen", "hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)", "hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)", + "hide_filtered_statuses": "Gefilterte Beiträge verbergen", "import_followers_from_a_csv_file": "Importiere Follower, denen du folgen möchtest, aus einer CSV-Datei", "import_theme": "Farbschema laden", "inputRadius": "Eingabefelder", diff --git a/src/i18n/en.json b/src/i18n/en.json index cde0caad..c664fbfa 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -140,6 +140,7 @@ "use_one_click_nsfw": "Open NSFW attachments with just one click", "hide_post_stats": "Hide post statistics (e.g. the number of favorites)", "hide_user_stats": "Hide user statistics (e.g. the number of followers)", + "hide_filtered_statuses": "Hide filtered statuses", "import_followers_from_a_csv_file": "Import follows from a csv file", "import_theme": "Load preset", "inputRadius": "Input fields", @@ -167,6 +168,8 @@ "no_rich_text_description": "Strip rich text formatting from all posts", "hide_follows_description": "Don't show who I'm following", "hide_followers_description": "Don't show who's following me", + "show_admin_badge": "Show Admin badge in my profile", + "show_moderator_badge": "Show Moderator badge in my profile", "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", "panelRadius": "Panels", "pause_on_unfocused": "Pause streaming when tab is not focused", @@ -193,6 +196,8 @@ "subject_line_email": "Like email: \"re: subject\"", "subject_line_mastodon": "Like mastodon: copy as is", "subject_line_noop": "Do not copy", + "post_status_content_type": "Post status content type", + "status_content_type_plain": "Plain text", "stop_gifs": "Play-on-hover GIFs", "streaming": "Enable automatic streaming of new posts when scrolled to the top", "text": "Text", diff --git a/src/i18n/es.json b/src/i18n/es.json index 7eaef1e7..29c8aec4 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -28,7 +28,8 @@ "password": "Contraseña", "placeholder": "p.ej. lain", "register": "Registrar", - "username": "Usuario" + "username": "Usuario", + "hint": "Inicia sesión para unirte a la discusión" }, "nav": { "about": "Sobre", @@ -55,7 +56,7 @@ "no_more_notifications": "No hay más notificaciones" }, "post_status": { - "new_status": "Post new status", + "new_status": "Publicar un nuevo estado", "account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las entradas para Solo-Seguidores.", "account_not_locked_warning_link": "bloqueada", "attachments_sensitive": "Contenido sensible", @@ -139,7 +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)", - "import_followers_from_a_csv_file": "Importar personas que tú sigues apartir de un archivo csv", + "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", "checkboxRadius": "Casillas de verificación", @@ -164,7 +165,10 @@ "notification_visibility_mentions": "Menciones", "notification_visibility_repeats": "Repeticiones (Repeats)", "no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas", - "hide_network_description": "No mostrar a quién sigo, ni quién me sigue", + "hide_follows_description": "No mostrar a quién sigo", + "hide_followers_description": "No mostrar quién me sigue", + "show_admin_badge": "Mostrar la placa de administrador en mi perfil", + "show_moderator_badge": "Mostrar la placa de moderador en mi perfil", "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW", "panelRadius": "Paneles", "pause_on_unfocused": "Parar la transmisión cuando no estés en foco.", @@ -191,6 +195,8 @@ "subject_line_email": "Tipo email: \"re: tema\"", "subject_line_mastodon": "Tipo mastodon: copiar como es", "subject_line_noop": "No copiar", + "post_status_content_type": "Formato de publicación", + "status_content_type_plain": "Texto plano", "stop_gifs": "Iniciar GIFs al pasar el ratón", "streaming": "Habilite la transmisión automática de nuevas publicaciones cuando se desplaza hacia la parte superior", "text": "Texto", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index e86eaff9..4b0bd4b4 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -129,6 +129,8 @@ "no_rich_text_description": "Убрать форматирование из всех постов", "hide_follows_description": "Не показывать кого я читаю", "hide_followers_description": "Не показывать кто читает меня", + "show_admin_badge": "Показывать значок администратора в моем профиле", + "show_moderator_badge": "Показывать значок модератора в моем профиле", "nsfw_clickthrough": "Включить скрытие NSFW вложений", "panelRadius": "Панели", "pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе", diff --git a/src/modules/api.js b/src/modules/api.js index 7bda13e7..31cb55c6 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -1,5 +1,4 @@ import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' -import {isArray} from 'lodash' import { Socket } from 'phoenix' const api = { @@ -34,20 +33,12 @@ const api = { } }, actions: { - startFetching (store, timeline) { - let userId = false - - // This is for user timelines - if (isArray(timeline)) { - userId = timeline[1] - timeline = timeline[0] - } - + startFetching (store, {timeline = 'friends', tag = false, userId = false}) { // Don't start fetching if we already are. - if (!store.state.fetchers[timeline]) { - const fetcher = store.state.backendInteractor.startFetching({timeline, store, userId}) - store.commit('addFetcher', {timeline, fetcher}) - } + if (store.state.fetchers[timeline]) return + + const fetcher = store.state.backendInteractor.startFetching({ timeline, store, userId, tag }) + store.commit('addFetcher', { timeline, fetcher }) }, stopFetching (store, timeline) { const fetcher = store.state.fetchers[timeline] diff --git a/src/modules/config.js b/src/modules/config.js index 526a7021..71f71376 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -31,7 +31,7 @@ const defaultState = { scopeCopy: undefined, // instance default subjectLineBehavior: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default - showFeaturesPanel: true + postContentType: undefined // instance default } const config = { diff --git a/src/modules/instance.js b/src/modules/instance.js index 2755e418..59c6b91c 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -21,13 +21,16 @@ const defaultState = { collapseMessageWithSubject: false, hidePostStats: false, hideUserStats: false, + hideFilteredStatuses: true, disableChat: false, scopeCopy: true, subjectLineBehavior: 'email', + postContentType: 'text/plain', loginMethod: 'password', nsfwCensorImage: undefined, vapidPublicKey: undefined, noAttachmentLinks: false, + showFeaturesPanel: true, // Nasty stuff pleromaBackend: true, diff --git a/src/modules/users.js b/src/modules/users.js index ca2e0f31..4d56ec6f 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -271,7 +271,7 @@ const users = { } // Start getting fresh posts. - store.dispatch('startFetching', 'friends') + store.dispatch('startFetching', { timeline: 'friends' }) // Get user mutes and follower info store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => { diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index d4d52ab1..92daa04e 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -130,7 +130,7 @@ const updateBanner = ({credentials, params}) => { // description const updateProfile = ({credentials, params}) => { // Always include these fields, because they might be empty or false - const fields = ['description', 'locked', 'no_rich_text', 'hide_follows', 'hide_followers'] + const fields = ['description', 'locked', 'no_rich_text', 'hide_follows', 'hide_followers', 'show_role'] let url = PROFILE_UPDATE_URL const form = new FormData() @@ -257,6 +257,13 @@ const fetchFriends = ({id, page, credentials}) => { .then((data) => data.map(parseUser)) } +const exportFriends = ({id, credentials}) => { + let url = `${FRIENDS_URL}?user_id=${id}&export=true` + return fetch(url, { headers: authHeaders(credentials) }) + .then((data) => data.json()) + .then((data) => data.map(parseUser)) +} + const fetchFollowers = ({id, page, credentials}) => { let url = `${FOLLOWERS_URL}?user_id=${id}` if (page) { @@ -536,6 +543,7 @@ const apiService = { fetchConversation, fetchStatus, fetchFriends, + exportFriends, fetchFollowers, followUser, unfollowUser, diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index ed7d4b49..80c5cc5e 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -14,6 +14,10 @@ const backendInteractorService = (credentials) => { return apiService.fetchFriends({id, page, credentials}) } + const exportFriends = ({id}) => { + return apiService.exportFriends({id, credentials}) + } + const fetchFollowers = ({id, page}) => { return apiService.fetchFollowers({id, page, credentials}) } @@ -78,6 +82,7 @@ const backendInteractorService = (credentials) => { fetchStatus, fetchConversation, fetchFriends, + exportFriends, fetchFollowers, followUser, unfollowUser, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index bba6b363..828c48f9 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -90,6 +90,8 @@ export const parseUser = (data) => { output.statusnet_blocking = data.statusnet_blocking output.is_local = data.is_local + output.role = data.role + output.show_role = data.show_role output.follows_you = data.follows_you 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) -} |
