diff options
Diffstat (limited to 'src')
25 files changed, 404 insertions, 200 deletions
diff --git a/src/App.scss b/src/App.scss index 2729e0b0..52a786ad 100644 --- a/src/App.scss +++ b/src/App.scss @@ -625,21 +625,6 @@ nav { text-align: right; } -.visibility-tray { - font-size: 1.2em; - padding: 3px; - cursor: pointer; - - .selected { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } - - div { - padding-top: 5px; - } -} - .visibility-notice { padding: .5em; border: 1px solid $fallback--faint; @@ -740,6 +725,70 @@ nav { } } +.setting-item { + border-bottom: 2px solid var(--fg, $fallback--fg); + margin: 1em 1em 1.4em; + padding-bottom: 1.4em; + + > div { + margin-bottom: .5em; + &:last-child { + margin-bottom: 0; + } + } + + &:last-child { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 1em; + } + + select { + min-width: 10em; + } + + + textarea { + width: 100%; + max-width: 100%; + height: 100px; + } + + .unavailable, + .unavailable i { + color: var(--cRed, $fallback--cRed); + color: $fallback--cRed; + } + + .btn { + min-height: 28px; + min-width: 10em; + padding: 0 2em; + } + + .number-input { + max-width: 6em; + } +} +.select-multiple { + display: flex; + .option-list { + margin: 0; + padding-left: .5em; + } +} +.setting-list, +.option-list{ + list-style-type: none; + padding-left: 2em; + li { + margin-bottom: 0.5em; + } + .suboptions { + margin-top: 0.3em + } +} + .login-hint { text-align: center; diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index ffeb7244..b3074590 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -41,7 +41,8 @@ const conversation = { props: [ 'statusoid', 'collapsable', - 'isPage' + 'isPage', + 'showPinned' ], created () { if (this.isPage) { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index d04ff722..0b4998c3 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -14,6 +14,7 @@ :inlineExpanded="collapsable && isExpanded" :statusoid="status" :expandable='!isExpanded' + :showPinned="showPinned" :focused="focused(status.id)" :inConversation="isExpanded" :highlight="getHighlight()" diff --git a/src/components/delete_button/delete_button.js b/src/components/delete_button/delete_button.js deleted file mode 100644 index 22f24625..00000000 --- a/src/components/delete_button/delete_button.js +++ /dev/null @@ -1,21 +0,0 @@ -const DeleteButton = { - props: [ 'status' ], - methods: { - deleteStatus () { - const confirmed = window.confirm('Do you really want to delete this status?') - if (confirmed) { - this.$store.dispatch('deleteStatus', { id: this.status.id }) - } - } - }, - computed: { - currentUser () { return this.$store.state.users.currentUser }, - canDelete () { - if (!this.currentUser) { return } - const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin - return superuser || this.status.user.id === this.currentUser.id - } - } -} - -export default DeleteButton diff --git a/src/components/delete_button/delete_button.vue b/src/components/delete_button/delete_button.vue deleted file mode 100644 index f4c91cfd..00000000 --- a/src/components/delete_button/delete_button.vue +++ /dev/null @@ -1,21 +0,0 @@ -<template> - <div v-if="canDelete"> - <a href="#" v-on:click.prevent="deleteStatus()"> - <i class='button-icon icon-cancel delete-status'></i> - </a> - </div> -</template> - -<script src="./delete_button.js" ></script> - -<style lang="scss"> -@import '../../_variables.scss'; - -.icon-cancel,.delete-status { - cursor: pointer; - &:hover { - color: $fallback--cRed; - color: var(--cRed, $fallback--cRed); - } -} -</style> diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js new file mode 100644 index 00000000..528da301 --- /dev/null +++ b/src/components/extra_buttons/extra_buttons.js @@ -0,0 +1,64 @@ +import Popper from 'vue-popperjs/src/component/popper.js.vue' + +const ExtraButtons = { + props: [ 'status' ], + components: { + Popper + }, + data () { + return { + showDropDown: false, + showPopper: true + } + }, + methods: { + deleteStatus () { + this.refreshPopper() + const confirmed = window.confirm(this.$t('status.delete_confirm')) + if (confirmed) { + this.$store.dispatch('deleteStatus', { id: this.status.id }) + } + }, + toggleMenu () { + this.showDropDown = !this.showDropDown + }, + pinStatus () { + this.refreshPopper() + this.$store.dispatch('pinStatus', this.status.id) + .then(() => this.$emit('onSuccess')) + .catch(err => this.$emit('onError', err.error.error)) + }, + unpinStatus () { + this.refreshPopper() + this.$store.dispatch('unpinStatus', this.status.id) + .then(() => this.$emit('onSuccess')) + .catch(err => this.$emit('onError', err.error.error)) + }, + refreshPopper () { + this.showPopper = false + this.showDropDown = false + setTimeout(() => { + this.showPopper = true + }) + } + }, + computed: { + currentUser () { return this.$store.state.users.currentUser }, + canDelete () { + if (!this.currentUser) { return } + const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin + return superuser || this.status.user.id === this.currentUser.id + }, + ownStatus () { + return this.status.user.id === this.currentUser.id + }, + canPin () { + return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted') + }, + enabled () { + return this.canPin || this.canDelete + } + } +} + +export default ExtraButtons diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue new file mode 100644 index 00000000..ef11138d --- /dev/null +++ b/src/components/extra_buttons/extra_buttons.vue @@ -0,0 +1,47 @@ +<template> + <Popper + trigger="click" + @hide='showDropDown = false' + append-to-body + v-if="enabled && showPopper" + :options="{ + placement: 'top', + modifiers: { + arrow: { enabled: true }, + offset: { offset: '0, 5px' }, + } + }" + > + <div class="popper-wrapper"> + <div class="dropdown-menu"> + <button class="dropdown-item dropdown-item-icon" @click.prevent="pinStatus" v-if="!status.pinned && canPin"> + <i class="icon-pin"></i><span>{{$t("status.pin")}}</span> + </button> + <button class="dropdown-item dropdown-item-icon" @click.prevent="unpinStatus" v-if="status.pinned && canPin"> + <i class="icon-pin"></i><span>{{$t("status.unpin")}}</span> + </button> + <button class="dropdown-item dropdown-item-icon" @click.prevent="deleteStatus" v-if="canDelete"> + <i class="icon-cancel"></i><span>{{$t("status.delete")}}</span> + </button> + </div> + </div> + <div class="button-icon" slot="reference" @click="toggleMenu"> + <i class='icon-ellipsis' :class="{'icon-clicked': showDropDown}"></i> + </div> + </Popper> +</template> + +<script src="./extra_buttons.js" ></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.icon-ellipsis { + cursor: pointer; + + &:hover, &.icon-clicked { + color: $fallback--text; + color: var(--text, $fallback--text); + } +} +</style> diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue index 27a8e48a..c6be2e00 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -50,6 +50,10 @@ @import '../../_variables.scss'; .login-form { + display: flex; + flex-direction: column; + padding: 0.6em; + .btn { min-height: 28px; width: 10em; @@ -66,9 +70,30 @@ align-items: center; justify-content: space-between; } -} -.login { + .form-group { + display: flex; + flex-direction: column; + padding: 0.3em 0.5em 0.6em; + line-height:24px; + } + + .form-bottom { + display: flex; + padding: 0.5em; + height: 32px; + + button { + width: 10em; + } + + p { + margin: 0.35em; + padding: 0.35em; + display: flex; + } + } + .error { text-align: center; diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index c24a2280..c9e3fc78 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -127,6 +127,14 @@ width: 100%; height: 100%; + &-icon { + padding-left: 0.5rem; + + i { + margin-right: 0.25rem; + } + } + &:hover { // TODO: improve the look on breeze themes background-color: $fallback--fg; diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index b8b93936..25c5284f 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -58,7 +58,7 @@ > </textarea> <div class="visibility-tray"> - <span class="text-format" v-if="formattingOptionsEnabled"> + <div 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"> <option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat"> @@ -67,7 +67,7 @@ </select> <i class="icon-down-open"></i> </label> - </span> + </div> <scope-selector :showAll="showAllScopes" @@ -152,10 +152,11 @@ display: flex; justify-content: space-between; flex-direction: row-reverse; + padding-top: 5px; } } -.post-status-form, .login { +.post-status-form { .form-bottom { display: flex; padding: 0.5em; @@ -250,7 +251,7 @@ .form-group { display: flex; flex-direction: column; - padding: 0.3em 0.5em 0.6em; + padding: 0.25em 0.5em 0.5em; line-height:24px; } diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue index 33ea488f..5ebb5d56 100644 --- a/src/components/scope_selector/scope_selector.vue +++ b/src/components/scope_selector/scope_selector.vue @@ -1,5 +1,5 @@ <template> -<div v-if="!showNothing"> +<div v-if="!showNothing" class="scope-selector"> <i class="icon-mail-alt" :class="css.direct" :title="$t('post_status.scope.direct')" @@ -28,3 +28,19 @@ </template> <script src="./scope_selector.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.scope-selector { + i { + font-size: 1.2em; + cursor: pointer; + + &.selected { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } + } +} +</style> diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 920e6e12..4cf6fae2 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -303,71 +303,3 @@ <script src="./settings.js"> </script> - -<style lang="scss"> -@import '../../_variables.scss'; - -.setting-item { - border-bottom: 2px solid var(--fg, $fallback--fg); - margin: 1em 1em 1.4em; - padding-bottom: 1.4em; - - > div { - margin-bottom: .5em; - &:last-child { - margin-bottom: 0; - } - } - - &:last-child { - border-bottom: none; - padding-bottom: 0; - margin-bottom: 1em; - } - - select { - min-width: 10em; - } - - - textarea { - width: 100%; - max-width: 100%; - height: 100px; - } - - .unavailable, - .unavailable i { - color: var(--cRed, $fallback--cRed); - color: $fallback--cRed; - } - - .btn { - min-height: 28px; - min-width: 10em; - padding: 0 2em; - } - - .number-input { - max-width: 6em; - } -} -.select-multiple { - display: flex; - .option-list { - margin: 0; - padding-left: .5em; - } -} -.setting-list, -.option-list{ - list-style-type: none; - padding-left: 2em; - li { - margin-bottom: 0.5em; - } - .suboptions { - margin-top: 0.3em - } -} -</style> diff --git a/src/components/status/status.js b/src/components/status/status.js index c01cfe79..5b3d98c3 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -1,7 +1,7 @@ import Attachment from '../attachment/attachment.vue' import FavoriteButton from '../favorite_button/favorite_button.vue' import RetweetButton from '../retweet_button/retweet_button.vue' -import DeleteButton from '../delete_button/delete_button.vue' +import ExtraButtons from '../extra_buttons/extra_buttons.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' import UserCard from '../user_card/user_card.vue' import UserAvatar from '../user_avatar/user_avatar.vue' @@ -26,7 +26,8 @@ const Status = { 'replies', 'isPreview', 'noHeading', - 'inlineExpanded' + 'inlineExpanded', + 'showPinned' ], data () { return { @@ -37,6 +38,7 @@ const Status = { showPreview: false, showingTall: this.inConversation && this.focused, showingLongSubject: false, + error: null, expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' ? !this.$store.state.instance.collapseMessageWithSubject : !this.$store.state.config.collapseMessageWithSubject, @@ -269,13 +271,16 @@ const Status = { this.statusFromGlobalRepository.rebloggedBy ) return uniqBy(combinedUsers, 'id') + }, + ownStatus () { + return this.status.user.id === this.$store.state.users.currentUser.id } }, components: { Attachment, FavoriteButton, RetweetButton, - DeleteButton, + ExtraButtons, PostStatusForm, UserCard, UserAvatar, @@ -296,6 +301,12 @@ const Status = { return 'icon-globe' } }, + showError (error) { + this.error = error + }, + clearError () { + this.error = undefined + }, linkClicked (event) { let { target } = event if (target.tagName === 'SPAN') { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 02715253..aea5b78b 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,5 +1,9 @@ <template> <div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]"> + <div v-if="error" class="alert error"> + {{error}} + <i class="button-icon icon-cancel" @click="clearError"></i> + </div> <template v-if="muted && !isPreview"> <div class="media status container muted"> <small> @@ -12,6 +16,10 @@ </div> </template> <template v-else> + <div v-if="showPinned && statusoid.pinned" class="status-pin"> + <i class="fa icon-pin faint"></i> + <span class="faint">{{$t('status.pinned')}}</span> + </div> <div v-if="retweet && !noHeading && !inConversation" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info"> <UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :user="statusoid.user"/> <div class="media-body faint"> @@ -95,7 +103,7 @@ v-if="preview" :isPreview="true" :statusoid="preview" - :compact=true + :compact="true" /> <div v-else class="status-preview status-preview-loading"> <i class="icon-spin4 animate-spin"></i> @@ -157,18 +165,18 @@ </transition> <div v-if="!noHeading && !isPreview" class='status-actions media-body'> - <div v-if="loggedIn"> - <i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'icon-reply-active': replying}"></i> + <div> + <i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'button-icon-active': replying}" v-if="loggedIn"/> + <i class="button-icon button-icon-disabled icon-reply" :title="$t('tool_tip.reply')" v-else /> <span v-if="status.replies_count > 0">{{status.replies_count}}</span> </div> <retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-button> <favorite-button :loggedIn='loggedIn' :status='status'></favorite-button> - <delete-button :status='status'></delete-button> + <extra-buttons :status="status" @onError="showError" @onSuccess="clearError"></extra-buttons> </div> </div> </div> <div class="container" v-if="replying"> - <div class="reply-left"/> <post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" :copy-message-scope="status.visibility" :subject="replySubject" v-on:posted="toggleReplying"/> </div> </template> @@ -198,6 +206,13 @@ $status-margin: 0.75em; max-width: 100%; } +.status-pin { + padding: $status-margin $status-margin 0; + display: flex; + align-items: center; + justify-content: flex-end; +} + .status-preview { position: absolute; max-width: 95%; @@ -241,7 +256,6 @@ $status-margin: 0.75em; } .status-el { - hyphens: auto; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; @@ -570,15 +584,13 @@ $status-margin: 0.75em; } } -.icon-reply:hover { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - cursor: pointer; -} - -.icon-reply.icon-reply-active { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); +.button-icon.icon-reply { + &:not(.button-icon-disabled):hover, + &.button-icon-active { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); + cursor: pointer; + } } .status:hover .animated.avatar { @@ -618,16 +630,11 @@ a.unmute { margin-left: auto; } -.reply-left { - flex: 0; - min-width: 48px; -} - .reply-body { flex: 1; } -.timeline > { +.timeline :not(.panel-disabled) > { .status-el:last-child { border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 2d02ca03..b4495673 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -6,7 +6,7 @@ <router-link :to="userProfileLink(user)"> <UserAvatar :betterShadow="betterShadow" :user="user"/> </router-link> - <div class="name-and-screen-name"> + <div class="user-summary"> <div class="top-line"> <div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div> <div :title="user.name" class='user-name' v-else>{{user.name}}</div> @@ -18,12 +18,12 @@ </a> </div> - <router-link class='user-screen-name' :to="userProfileLink(user)"> - <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> + <div class="bottom-line"> + <router-link class="user-screen-name" :to="userProfileLink(user)">@{{user.screen_name}}</router-link> + <span class="alert staff" v-if="!hideBio && !!visibleRole">{{visibleRole}}</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> </div> </div> <div class="user-meta"> @@ -232,7 +232,7 @@ opacity: .8; } - .name-and-screen-name { + .user-summary { display: block; margin-left: 0.6em; text-align: left; @@ -249,6 +249,7 @@ vertical-align: middle; object-fit: contain } + .top-line { display: flex; } @@ -269,15 +270,19 @@ } } - .user-screen-name { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - display: inline-block; + .bottom-line { + display: flex; font-weight: light; font-size: 15px; - padding-right: 0.1em; - width: 100%; - display: flex; + + .user-screen-name { + min-width: 1px; + flex: 0 1 auto; + text-overflow: ellipsis; + overflow: hidden; + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } .dailyAvg { min-width: 1px; @@ -288,15 +293,9 @@ color: var(--text, $fallback--text); } - .handle { - min-width: 1px; - flex: 0 1 auto; - text-overflow: ellipsis; - overflow: hidden; - } - // TODO use proper colors .staff { + flex: none; text-transform: capitalize; color: $fallback--text; color: var(--btnText, $fallback--text); diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 4eddb8b1..eab330e7 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -2,6 +2,7 @@ import get from 'lodash/get' import UserCard from '../user_card/user_card.vue' import FollowCard from '../follow_card/follow_card.vue' import Timeline from '../timeline/timeline.vue' +import Conversation from '../conversation/conversation.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue' import List from '../list/list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' @@ -95,6 +96,8 @@ const UserProfile = { if (this.isUs) { this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId }) } + // Fetch all pinned statuses immediately + this.$store.dispatch('fetchPinnedStatuses', userId) }, cleanUp () { this.$store.dispatch('stopFetching', 'user') @@ -128,7 +131,8 @@ const UserProfile = { FollowerList, FriendList, ModerationTools, - FollowCard + FollowCard, + Conversation } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 71c625b7..48b774ea 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -3,16 +3,28 @@ <div v-if="user" class="user-profile panel panel-default"> <UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/> <tab-switcher :renderOnlyFocused="true" ref="tabSwitcher"> - <Timeline - :label="$t('user_card.statuses')" - :disabled="!user.statuses_count" - :count="user.statuses_count" - :embedded="true" - :title="$t('user_profile.timeline_title')" - :timeline="timeline" - :timeline-name="'user'" - :user-id="userId" - /> + <div :label="$t('user_card.statuses')" :disabled="!user.statuses_count"> + <div class="timeline"> + <template v-for="statusId in user.pinnedStatuseIds"> + <Conversation + v-if="timeline.statusesObject[statusId]" + class="status-fadein" + :key="statusId" + :statusoid="timeline.statusesObject[statusId]" + :collapsable="true" + :showPinned="true" + /> + </template> + </div> + <Timeline + :count="user.statuses_count" + :embedded="true" + :title="$t('user_profile.timeline_title')" + :timeline="timeline" + :timeline-name="'user'" + :user-id="userId" + /> + </div> <div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count"> <FriendList :userId="userId"> <template slot="item" slot-scope="{item}"> diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 8a94f0b8..2cb8b37a 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -251,6 +251,10 @@ margin: 0; } + .visibility-tray { + padding-top: 5px; + } + input[type=file] { padding: 5px; height: auto; diff --git a/src/i18n/compare.js b/src/i18n/compare index e9314376..4dc1e47d 100755 --- a/src/i18n/compare.js +++ b/src/i18n/compare @@ -19,7 +19,7 @@ if (typeof arg === 'undefined') { console.log('') console.log('There are no other arguments or options. Make an issue if you encounter a bug or want') console.log('some feature to be implemented. Merge requests are welcome as well.') - return + process.exit() } const english = require('./en.json') diff --git a/src/i18n/en.json b/src/i18n/en.json index 92d63be8..031c93de 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -408,6 +408,11 @@ "status": { "favorites": "Favorites", "repeats": "Repeats", + "delete": "Delete status", + "pin": "Pin on profile", + "unpin": "Unpin from profile", + "pinned": "Pinned", + "delete_confirm": "Do you really want to delete this status?", "reply_to": "Reply to", "replies_list": "Replies:" }, diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 4c92d4e1..e6ee5447 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -424,6 +424,10 @@ export const mutations = { newStatus.favoritedBy.push(user) } }, + setPinned (state, status) { + const newStatus = state.allStatusesObject[status.id] + newStatus.pinned = status.pinned + }, setRetweeted (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] @@ -533,6 +537,18 @@ const statuses = { rootState.api.backendInteractor.unfavorite(status.id) .then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser })) }, + fetchPinnedStatuses ({ rootState, dispatch }, userId) { + rootState.api.backendInteractor.fetchPinnedStatuses(userId) + .then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true })) + }, + pinStatus ({ rootState, commit }, statusId) { + return rootState.api.backendInteractor.pinOwnStatus(statusId) + .then((status) => commit('setPinned', status)) + }, + unpinStatus ({ rootState, commit }, statusId) { + rootState.api.backendInteractor.unpinOwnStatus(statusId) + .then((status) => commit('setPinned', status)) + }, retweet ({ rootState, commit }, status) { // Optimistic retweeting... commit('setRetweeted', { status, value: true }) diff --git a/src/modules/users.js b/src/modules/users.js index adcab233..e72a657c 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -165,6 +165,15 @@ export const mutations = { state.currentUser.muteIds.push(muteId) } }, + setPinned (state, status) { + const user = state.usersObject[status.user.id] + const index = user.pinnedStatuseIds.indexOf(status.id) + if (status.pinned && index === -1) { + user.pinnedStatuseIds.push(status.id) + } else if (!status.pinned && index !== -1) { + user.pinnedStatuseIds.splice(index, 1) + } + }, setUserForStatus (state, status) { status.user = state.usersObject[status.user.id] }, @@ -318,13 +327,17 @@ const users = { store.commit('addNewUsers', users) store.commit('addNewUsers', retweetedUsers) - // Reconnect users to statuses each(statuses, (status) => { + // Reconnect users to statuses store.commit('setUserForStatus', status) + // Set pinned statuses to user + store.commit('setPinned', status) }) - // Reconnect users to retweets each(compact(map(statuses, 'retweeted_status')), (status) => { + // Reconnect users to retweets store.commit('setUserForStatus', status) + // Set pinned retweets to user + store.commit('setPinned', status) }) }, addNewNotifications (store, { notifications }) { diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 75a001a4..c67eccf1 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -12,9 +12,9 @@ const FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests' const APPROVE_USER_URL = '/api/pleroma/friendships/approve' const DENY_USER_URL = '/api/pleroma/friendships/deny' const TAG_USER_URL = '/api/pleroma/admin/users/tag' -const PERMISSION_GROUP_URL = '/api/pleroma/admin/permission_group' -const ACTIVATION_STATUS_URL = '/api/pleroma/admin/activation_status' -const ADMIN_USER_URL = '/api/pleroma/admin/user' +const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}` +const ACTIVATION_STATUS_URL = screenName => `/api/pleroma/admin/users/${screenName}/activation_status` +const ADMIN_USERS_URL = '/api/pleroma/admin/users' const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' @@ -49,6 +49,8 @@ const MASTODON_STATUS_FAVORITEDBY_URL = id => `/api/v1/statuses/${id}/favourited const MASTODON_STATUS_REBLOGGEDBY_URL = id => `/api/v1/statuses/${id}/reblogged_by` const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials' const MASTODON_REPORT_USER_URL = '/api/v1/reports' +const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin` +const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin` import { each, map, concat, last } from 'lodash' import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js' @@ -208,6 +210,16 @@ const unfollowUser = ({id, credentials}) => { }).then((data) => data.json()) } +const pinOwnStatus = ({ id, credentials }) => { + return promisedRequest({ url: MASTODON_PIN_OWN_STATUS(id), credentials, method: 'POST' }) + .then((data) => parseStatus(data)) +} + +const unpinOwnStatus = ({ id, credentials }) => { + return promisedRequest({ url: MASTODON_UNPIN_OWN_STATUS(id), credentials, method: 'POST' }) + .then((data) => parseStatus(data)) +} + const blockUser = ({id, credentials}) => { return fetch(MASTODON_BLOCK_USER_URL(id), { headers: authHeaders(credentials), @@ -378,7 +390,7 @@ const untagUser = ({tag, credentials, ...options}) => { const addRight = ({right, credentials, ...user}) => { const screenName = user.screen_name - return fetch(`${PERMISSION_GROUP_URL}/${screenName}/${right}`, { + return fetch(PERMISSION_GROUP_URL(screenName, right), { method: 'POST', headers: authHeaders(credentials), body: {} @@ -388,7 +400,7 @@ const addRight = ({right, credentials, ...user}) => { const deleteRight = ({right, credentials, ...user}) => { const screenName = user.screen_name - return fetch(`${PERMISSION_GROUP_URL}/${screenName}/${right}`, { + return fetch(PERMISSION_GROUP_URL(screenName, right), { method: 'DELETE', headers: authHeaders(credentials), body: {} @@ -404,7 +416,7 @@ const setActivationStatus = ({status, credentials, ...user}) => { const headers = authHeaders(credentials) headers['Content-Type'] = 'application/json' - return fetch(`${ACTIVATION_STATUS_URL}/${screenName}.json`, { + return fetch(ACTIVATION_STATUS_URL(screenName), { method: 'PUT', headers: headers, body: JSON.stringify(body) @@ -415,7 +427,7 @@ const deleteUser = ({credentials, ...user}) => { const screenName = user.screen_name const headers = authHeaders(credentials) - return fetch(`${ADMIN_USER_URL}.json?nickname=${screenName}`, { + return fetch(`${ADMIN_USERS_URL}?nickname=${screenName}`, { method: 'DELETE', headers: headers }) @@ -478,6 +490,12 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use .then((data) => data.map(isNotifications ? parseNotification : parseStatus)) } +const fetchPinnedStatuses = ({ id, credentials }) => { + const url = MASTODON_USER_TIMELINE_URL(id) + '?pinned=true' + return promisedRequest({ url, credentials }) + .then((data) => data.map(parseStatus)) +} + const verifyCredentials = (user) => { return fetch(LOGIN_URL, { method: 'POST', @@ -698,6 +716,7 @@ const reportUser = ({credentials, userId, statusIds, comment, forward}) => { const apiService = { verifyCredentials, fetchTimeline, + fetchPinnedStatuses, fetchConversation, fetchStatus, fetchFriends, @@ -705,6 +724,8 @@ const apiService = { fetchFollowers, followUser, unfollowUser, + pinOwnStatus, + unpinOwnStatus, blockUser, unblockUser, fetchUser, diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 5d7ae62e..639bcabc 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -94,6 +94,9 @@ const backendInteractorService = (credentials) => { const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials}) const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials}) const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials}) + const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({credentials, id}) + const pinOwnStatus = (id) => apiService.pinOwnStatus({credentials, id}) + const unpinOwnStatus = (id) => apiService.unpinOwnStatus({credentials, id}) const getCaptcha = () => apiService.getCaptcha() const register = (params) => apiService.register(params) @@ -139,6 +142,9 @@ const backendInteractorService = (credentials) => { fetchBlocks, fetchOAuthTokens, revokeOAuthToken, + fetchPinnedStatuses, + pinOwnStatus, + unpinOwnStatus, tagUser, untagUser, addRight, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 7a8708d5..8e413584 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -131,6 +131,8 @@ export const parseUser = (data) => { output.statuses_count = data.statuses_count output.friendIds = [] output.followerIds = [] + output.pinnedStatuseIds = [] + if (data.pleroma) { output.follow_request_count = data.pleroma.follow_request_count } @@ -141,6 +143,7 @@ export const parseUser = (data) => { } output.tags = output.tags || [] + output.rights = output.rights || {} return output } @@ -168,7 +171,7 @@ export const addEmojis = (string, emojis) => { return emojis.reduce((acc, emoji) => { return acc.replace( new RegExp(`:${emoji.shortcode}:`, 'g'), - `<img src='${emoji.url}' alt='${emoji.shortcode}' class='emoji' />` + `<img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' />` ) }, string) } @@ -211,6 +214,7 @@ export const parseStatus = (data) => { output.summary_html = addEmojis(data.spoiler_text, data.emojis) output.external_url = data.url + output.pinned = data.pinned } else { output.favorited = data.favorited output.fave_num = data.fave_num |
