aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.scss79
-rw-r--r--src/components/conversation/conversation.js3
-rw-r--r--src/components/conversation/conversation.vue1
-rw-r--r--src/components/delete_button/delete_button.js21
-rw-r--r--src/components/delete_button/delete_button.vue21
-rw-r--r--src/components/extra_buttons/extra_buttons.js64
-rw-r--r--src/components/extra_buttons/extra_buttons.vue47
-rw-r--r--src/components/login_form/login_form.vue29
-rw-r--r--src/components/moderation_tools/moderation_tools.vue8
-rw-r--r--src/components/post_status_form/post_status_form.vue9
-rw-r--r--src/components/scope_selector/scope_selector.vue18
-rw-r--r--src/components/settings/settings.vue68
-rw-r--r--src/components/status/status.js17
-rw-r--r--src/components/status/status.vue49
-rw-r--r--src/components/user_card/user_card.vue41
-rw-r--r--src/components/user_profile/user_profile.js6
-rw-r--r--src/components/user_profile/user_profile.vue32
-rw-r--r--src/components/user_settings/user_settings.vue4
-rwxr-xr-xsrc/i18n/compare (renamed from src/i18n/compare.js)2
-rw-r--r--src/i18n/en.json5
-rw-r--r--src/modules/statuses.js16
-rw-r--r--src/modules/users.js17
-rw-r--r--src/services/api/api.service.js35
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js6
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js6
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