aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHenry Jameson <me@hjkos.com>2020-01-28 20:44:13 +0200
committerHenry Jameson <me@hjkos.com>2020-01-28 20:44:13 +0200
commitf0c4bb1193f71cb93546be7e8f41236c4c192f85 (patch)
tree26c35a98a8aa80279a1d8d938e41629a774fda37
parentb63e679a31a573c30868477f17322d6ed040af58 (diff)
parentc54111797ae1058e59931b2d1f12e6ab6a6f96a9 (diff)
Merge remote-tracking branch 'upstream/develop' into themes-accent
* upstream/develop: (33 commits) add emoji reactions to changelog fix emoji reaction classes broken in develop review changes Fix password and email update remove unnecessary anonymous function Apply suggestion to src/services/api/api.service.js remove logs/commented code remove favs count from react button remove mock data change emoji reactions to use new format Added polyfills for EventTarget (needed for Safari) and CustomEvent (needed for IE) Fix missing TWKN when logged in, but server is set to private mode. Fix follower request fetching Add domain mutes to changelog Implement domain mutes v2 change changelog to reflect actual release history of frontend Fix #750 , fix error messages and captcha resetting Optimize Notifications Rendering update CHANGELOG Use last seen notif instead of first unseen notif for sinceId ...
-rw-r--r--CHANGELOG.md18
-rw-r--r--package.json2
-rw-r--r--src/boot/after_store.js11
-rw-r--r--src/components/conversation/conversation.js1
-rw-r--r--src/components/domain_mute_card/domain_mute_card.js15
-rw-r--r--src/components/domain_mute_card/domain_mute_card.vue38
-rw-r--r--src/components/emoji_reactions/emoji_reactions.js32
-rw-r--r--src/components/emoji_reactions/emoji_reactions.vue49
-rw-r--r--src/components/interactions/interactions.js3
-rw-r--r--src/components/interactions/interactions.vue4
-rw-r--r--src/components/nav_panel/nav_panel.js2
-rw-r--r--src/components/nav_panel/nav_panel.vue2
-rw-r--r--src/components/notification/notification.js14
-rw-r--r--src/components/notification/notification.vue14
-rw-r--r--src/components/notifications/notifications.js27
-rw-r--r--src/components/notifications/notifications.scss7
-rw-r--r--src/components/notifications/notifications.vue2
-rw-r--r--src/components/react_button/react_button.js43
-rw-r--r--src/components/react_button/react_button.vue109
-rw-r--r--src/components/registration/registration.js3
-rw-r--r--src/components/registration/registration.vue2
-rw-r--r--src/components/settings/settings.vue5
-rw-r--r--src/components/side_drawer/side_drawer.js2
-rw-r--r--src/components/side_drawer/side_drawer.vue2
-rw-r--r--src/components/staff_panel/staff_panel.js3
-rw-r--r--src/components/status/status.js6
-rw-r--r--src/components/status/status.vue8
-rw-r--r--src/components/user_settings/mfa.js2
-rw-r--r--src/components/user_settings/user_settings.js23
-rw-r--r--src/components/user_settings/user_settings.vue157
-rw-r--r--src/i18n/en.json15
-rw-r--r--src/lib/event_target_polyfill.js9
-rw-r--r--src/main.js3
-rw-r--r--src/modules/api.js1
-rw-r--r--src/modules/config.js3
-rw-r--r--src/modules/statuses.js92
-rw-r--r--src/modules/users.js50
-rw-r--r--src/services/api/api.service.js56
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js2
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js6
-rw-r--r--src/services/errors/errors.js14
-rw-r--r--src/services/notification_utils/notification_utils.js7
-rw-r--r--src/services/notifications_fetcher/notifications_fetcher.service.js7
-rw-r--r--src/services/push/push.js3
-rwxr-xr-xstatic/fontello.json6
-rw-r--r--static/styles.json1
-rw-r--r--test/unit/specs/modules/statuses.spec.js45
-rw-r--r--test/unit/specs/services/notification_utils/notification_utils.spec.js4
-rw-r--r--yarn.lock10
49 files changed, 838 insertions, 102 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 554870ca..9fae1acb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,12 +5,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
+- Icons in nav panel
+- Private mode support
+- Support for 'Move' type notifications
+- Pleroma AMOLED dark theme
+- User level domain mutes, under User Settings -> Mutes
+- Emoji reactions for statuses
+### Changed
+- Captcha now resets on failed registrations
+- Notifications column now cleans itself up to optimize performance when tab is left open for a long time
+- 403 messaging
+### Fixed
+- Single notifications left unread when hitting read on another device/tab
+- Registration fixed
+- Deactivation of remote accounts from frontend
+
+## [1.1.7 and earlier] - 2019-12-14
+### Added
- Ability to hide/show repeats from user
- User profile button clutter organized into a menu
- Emoji picker
- Started changelog anew
- Ability to change user's email
- About page
+- Added remote user redirect
### Changed
- theme engine update to 3
- theme doesn't get saved to local storage when opening FE anonymously
diff --git a/package.json b/package.json
index 38936b23..9ec8c1eb 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.6",
"@babel/register": "^7.7.4",
+ "@ungap/event-target": "^0.1.0",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
"@vue/test-utils": "^1.0.0-beta.26",
@@ -56,6 +57,7 @@
"connect-history-api-fallback": "^1.1.0",
"cross-spawn": "^4.0.2",
"css-loader": "^0.28.0",
+ "custom-event-polyfill": "^1.0.7",
"eslint": "^5.16.0",
"eslint-config-standard": "^12.0.0",
"eslint-friendly-formatter": "^2.0.5",
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index f61e9252..86874e47 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -187,12 +187,9 @@ const getAppSecret = async ({ store }) => {
})
}
-const resolveStaffAccounts = async ({ store, accounts }) => {
- const backendInteractor = store.state.api.backendInteractor
- let nicknames = accounts.map(uri => uri.split('/').pop())
- .map(id => backendInteractor.fetchUser({ id }))
- nicknames = await Promise.all(nicknames)
-
+const resolveStaffAccounts = ({ store, accounts }) => {
+ const nicknames = accounts.map(uri => uri.split('/').pop())
+ nicknames.map(nickname => store.dispatch('fetchUser', nickname))
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
}
@@ -238,7 +235,7 @@ const getNodeInfo = async ({ store }) => {
})
const accounts = metadata.staffAccounts
- await resolveStaffAccounts({ store, accounts })
+ resolveStaffAccounts({ store, accounts })
} else {
throw (res)
}
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 08283fff..45fb2bf6 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -150,6 +150,7 @@ const conversation = {
if (!id) return
this.highlight = id
this.$store.dispatch('fetchFavsAndRepeats', id)
+ this.$store.dispatch('fetchEmojiReactionsBy', id)
},
getHighlight () {
return this.isExpanded ? this.highlight : null
diff --git a/src/components/domain_mute_card/domain_mute_card.js b/src/components/domain_mute_card/domain_mute_card.js
new file mode 100644
index 00000000..c8e838ba
--- /dev/null
+++ b/src/components/domain_mute_card/domain_mute_card.js
@@ -0,0 +1,15 @@
+import ProgressButton from '../progress_button/progress_button.vue'
+
+const DomainMuteCard = {
+ props: ['domain'],
+ components: {
+ ProgressButton
+ },
+ methods: {
+ unmuteDomain () {
+ return this.$store.dispatch('unmuteDomain', this.domain)
+ }
+ }
+}
+
+export default DomainMuteCard
diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue
new file mode 100644
index 00000000..567d81c5
--- /dev/null
+++ b/src/components/domain_mute_card/domain_mute_card.vue
@@ -0,0 +1,38 @@
+<template>
+ <div class="domain-mute-card">
+ <div class="domain-mute-card-domain">
+ {{ domain }}
+ </div>
+ <ProgressButton
+ :click="unmuteDomain"
+ class="btn btn-default"
+ >
+ {{ $t('domain_mute_card.unmute') }}
+ <template slot="progress">
+ {{ $t('domain_mute_card.unmute_progress') }}
+ </template>
+ </ProgressButton>
+ </div>
+</template>
+
+<script src="./domain_mute_card.js"></script>
+
+<style lang="scss">
+.domain-mute-card {
+ flex: 1 0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.6em 1em 0.6em 0;
+
+ &-domain {
+ margin-right: 1em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ button {
+ width: 10em;
+ }
+}
+</style>
diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js
new file mode 100644
index 00000000..95d52cb6
--- /dev/null
+++ b/src/components/emoji_reactions/emoji_reactions.js
@@ -0,0 +1,32 @@
+
+const EmojiReactions = {
+ name: 'EmojiReactions',
+ props: ['status'],
+ computed: {
+ emojiReactions () {
+ return this.status.emoji_reactions
+ }
+ },
+ methods: {
+ reactedWith (emoji) {
+ const user = this.$store.state.users.currentUser
+ const reaction = this.status.emoji_reactions.find(r => r.emoji === emoji)
+ return reaction.accounts && reaction.accounts.find(u => u.id === user.id)
+ },
+ reactWith (emoji) {
+ this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+ },
+ unreact (emoji) {
+ this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
+ },
+ emojiOnClick (emoji, event) {
+ if (this.reactedWith(emoji)) {
+ this.unreact(emoji)
+ } else {
+ this.reactWith(emoji)
+ }
+ }
+ }
+}
+
+export default EmojiReactions
diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue
new file mode 100644
index 00000000..00d6d2b7
--- /dev/null
+++ b/src/components/emoji_reactions/emoji_reactions.vue
@@ -0,0 +1,49 @@
+<template>
+ <div class="emoji-reactions">
+ <button
+ v-for="(reaction) in emojiReactions"
+ :key="reaction.emoji"
+ class="emoji-reaction btn btn-default"
+ :class="{ 'picked-reaction': reactedWith(reaction.emoji) }"
+ @click="emojiOnClick(reaction.emoji, $event)"
+ >
+ <span class="reaction-emoji">{{ reaction.emoji }}</span>
+ <span>{{ reaction.count }}</span>
+ </button>
+ </div>
+</template>
+
+<script src="./emoji_reactions.js" ></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+.emoji-reactions {
+ display: flex;
+ margin-top: 0.25em;
+ flex-wrap: wrap;
+}
+
+.emoji-reaction {
+ padding: 0 0.5em;
+ margin-right: 0.5em;
+ margin-top: 0.5em;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ .reaction-emoji {
+ width: 1.25em;
+ margin-right: 0.25em;
+ }
+ &:focus {
+ outline: none;
+ }
+}
+
+.picked-reaction {
+ border: 1px solid var(--link, $fallback--link);
+ margin-left: -1px; // offset the border, can't use inset shadows either
+ margin-right: calc(0.5em - 1px);
+}
+
+</style>
diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js
index 1f8a9de9..cc31ff20 100644
--- a/src/components/interactions/interactions.js
+++ b/src/components/interactions/interactions.js
@@ -3,7 +3,8 @@ import Notifications from '../notifications/notifications.vue'
const tabModeDict = {
mentions: ['mention'],
'likes+repeats': ['repeat', 'like'],
- follows: ['follow']
+ follows: ['follow'],
+ moves: ['move']
}
const Interactions = {
diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue
index 08cee343..a2e252ab 100644
--- a/src/components/interactions/interactions.vue
+++ b/src/components/interactions/interactions.vue
@@ -21,6 +21,10 @@
key="follows"
:label="$t('interactions.follows')"
/>
+ <span
+ key="moves"
+ :label="$t('interactions.moves')"
+ />
</tab-switcher>
<Notifications
ref="notifications"
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
index d9268585..8f7edb7f 100644
--- a/src/components/nav_panel/nav_panel.js
+++ b/src/components/nav_panel/nav_panel.js
@@ -3,7 +3,7 @@ import { mapState } from 'vuex'
const NavPanel = {
created () {
if (this.currentUser && this.currentUser.locked) {
- this.$store.dispatch('startFetchingFollowRequest')
+ this.$store.dispatch('startFetchingFollowRequests')
}
},
computed: mapState({
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index a934a411..8cd04dc7 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -33,7 +33,7 @@
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
</router-link>
</li>
- <li v-if="federating && !privateMode">
+ <li v-if="federating && (currentUser || !privateMode)">
<router-link :to="{ name: 'public-external-timeline' }">
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
</router-link>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 7d46eb5a..e7bd769e 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -43,18 +43,18 @@ const Notification = {
const user = this.notification.from_profile
return highlightStyle(highlight[user.screen_name])
},
- userInStore () {
- return this.$store.getters.findUser(this.notification.from_profile.id)
- },
user () {
- if (this.userInStore) {
- return this.userInStore
- }
- return this.notification.from_profile
+ return this.$store.getters.findUser(this.notification.from_profile.id)
},
userProfileLink () {
return this.generateUserProfileLink(this.user)
},
+ targetUser () {
+ return this.$store.getters.findUser(this.notification.target.id)
+ },
+ targetUserProfileLink () {
+ return this.generateUserProfileLink(this.targetUser)
+ },
needMute () {
return this.user.muted
}
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 1f192c77..16124e50 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -74,9 +74,13 @@
<i class="fa icon-user-plus lit" />
<small>{{ $t('notifications.followed_you') }}</small>
</span>
+ <span v-if="notification.type === 'move'">
+ <i class="fa icon-arrow-curved lit" />
+ <small>{{ $t('notifications.migrated_to') }}</small>
+ </span>
</div>
<div
- v-if="notification.type === 'follow'"
+ v-if="notification.type === 'follow' || notification.type === 'move'"
class="timeago"
>
<span class="faint">
@@ -115,6 +119,14 @@
@{{ notification.from_profile.screen_name }}
</router-link>
</div>
+ <div
+ v-else-if="notification.type === 'move'"
+ class="move-text"
+ >
+ <router-link :to="targetUserProfileLink">
+ @{{ notification.target.screen_name }}
+ </router-link>
+ </div>
<template v-else>
<status
class="faint"
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index a89c0cdc..26ffbab6 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -2,10 +2,12 @@ import Notification from '../notification/notification.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import {
notificationsFromStore,
- visibleNotificationsFromStore,
+ filteredNotificationsFromStore,
unseenNotificationsFromStore
} from '../../services/notification_utils/notification_utils.js'
+const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
+
const Notifications = {
props: {
// Disables display of panel header
@@ -18,7 +20,11 @@ const Notifications = {
},
data () {
return {
- bottomedOut: false
+ bottomedOut: false,
+ // How many seen notifications to display in the list. The more there are,
+ // the heavier the page becomes. This count is increased when loading
+ // older notifications, and cut back to default whenever hitting "Read!".
+ seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
}
},
computed: {
@@ -34,14 +40,17 @@ const Notifications = {
unseenNotifications () {
return unseenNotificationsFromStore(this.$store)
},
- visibleNotifications () {
- return visibleNotificationsFromStore(this.$store, this.filterMode)
+ filteredNotifications () {
+ return filteredNotificationsFromStore(this.$store, this.filterMode)
},
unseenCount () {
return this.unseenNotifications.length
},
loading () {
return this.$store.state.statuses.notifications.loading
+ },
+ notificationsToDisplay () {
+ return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
}
},
components: {
@@ -64,12 +73,21 @@ const Notifications = {
methods: {
markAsSeen () {
this.$store.dispatch('markNotificationsAsSeen')
+ this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT
},
fetchOlderNotifications () {
if (this.loading) {
return
}
+ const seenCount = this.filteredNotifications.length - this.unseenCount
+ if (this.seenToDisplayCount < seenCount) {
+ this.seenToDisplayCount = Math.min(this.seenToDisplayCount + 20, seenCount)
+ return
+ } else if (this.seenToDisplayCount > seenCount) {
+ this.seenToDisplayCount = seenCount
+ }
+
const store = this.$store
const credentials = store.state.users.currentUser.credentials
store.commit('setNotificationsLoading', { value: true })
@@ -82,6 +100,7 @@ const Notifications = {
if (notifs.length === 0) {
this.bottomedOut = true
}
+ this.seenToDisplayCount += notifs.length
})
}
}
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index ca762432..64ded936 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -79,7 +79,7 @@
}
}
- .follow-text {
+ .follow-text, .move-text {
padding: 0.5em 0;
}
@@ -154,6 +154,11 @@
color: var(--cOrange, $fallback--cOrange);
}
+ .icon-arrow-curved.lit {
+ color: $fallback--cBlue;
+ color: var(--cBlue, $fallback--cBlue);
+ }
+
.status-content {
margin: 0;
max-height: 300px;
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index c42c35e6..d477a41b 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -32,7 +32,7 @@
</div>
<div class="panel-body">
<div
- v-for="notification in visibleNotifications"
+ v-for="notification in notificationsToDisplay"
:key="notification.id"
class="notification"
:class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
new file mode 100644
index 00000000..6fb2a780
--- /dev/null
+++ b/src/components/react_button/react_button.js
@@ -0,0 +1,43 @@
+import { mapGetters } from 'vuex'
+
+const ReactButton = {
+ props: ['status', 'loggedIn'],
+ data () {
+ return {
+ showTooltip: false,
+ filterWord: '',
+ popperOptions: {
+ modifiers: {
+ preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
+ }
+ }
+ }
+ },
+ methods: {
+ openReactionSelect () {
+ this.showTooltip = true
+ this.filterWord = ''
+ },
+ closeReactionSelect () {
+ this.showTooltip = false
+ },
+ addReaction (event, emoji) {
+ this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+ this.closeReactionSelect()
+ }
+ },
+ computed: {
+ commonEmojis () {
+ return ['❤️', '😠', '👀', '😂', '🔥']
+ },
+ emojis () {
+ if (this.filterWord !== '') {
+ return this.$store.state.instance.emoji.filter(emoji => emoji.displayText.includes(this.filterWord))
+ }
+ return this.$store.state.instance.emoji || []
+ },
+ ...mapGetters(['mergedConfig'])
+ }
+}
+
+export default ReactButton
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
new file mode 100644
index 00000000..c925dd71
--- /dev/null
+++ b/src/components/react_button/react_button.vue
@@ -0,0 +1,109 @@
+<template>
+ <v-popover
+ :popper-options="popperOptions"
+ :open="showTooltip"
+ trigger="manual"
+ placement="top"
+ class="react-button-popover"
+ @hide="closeReactionSelect"
+ >
+ <div slot="popover">
+ <div class="reaction-picker-filter">
+ <input
+ v-model="filterWord"
+ :placeholder="$t('emoji.search_emoji')"
+ >
+ </div>
+ <div class="reaction-picker">
+ <span
+ v-for="emoji in commonEmojis"
+ :key="emoji"
+ class="emoji-button"
+ @click="addReaction($event, emoji)"
+ >
+ {{ emoji }}
+ </span>
+ <div class="reaction-picker-divider" />
+ <span
+ v-for="(emoji, key) in emojis"
+ :key="key"
+ class="emoji-button"
+ @click="addReaction($event, emoji.replacement)"
+ >
+ {{ emoji.replacement }}
+ </span>
+ <div class="reaction-bottom-fader" />
+ </div>
+ </div>
+ <div
+ v-if="loggedIn"
+ @click.prevent="openReactionSelect"
+ >
+ <i
+ class="icon-smile button-icon add-reaction-button"
+ :title="$t('tool_tip.add_reaction')"
+ />
+ </div>
+ </v-popover>
+</template>
+
+<script src="./react_button.js" ></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.reaction-picker-filter {
+ padding: 0.5em;
+}
+
+.reaction-picker-divider {
+ height: 1px;
+ width: 100%;
+ margin: 0.5em;
+ background-color: var(--border, $fallback--border);
+}
+
+.reaction-picker {
+ width: 10em;
+ height: 9em;
+ font-size: 1.5em;
+ overflow-y: scroll;
+ display: flex;
+ flex-wrap: wrap;
+ padding: 0.5em;
+ text-align: center;
+ align-content: flex-start;
+ user-select: none;
+
+ mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
+ linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
+ linear-gradient(to top, white, white);
+ transition: mask-size 150ms;
+ mask-size: 100% 20px, 100% 20px, auto;
+ // Autoprefixed seem to ignore this one, and also syntax is different
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+
+ .emoji-button {
+ cursor: pointer;
+
+ flex-basis: 20%;
+ line-height: 1.5em;
+ align-content: center;
+
+ &:hover {
+ transform: scale(1.25);
+ }
+ }
+}
+
+.add-reaction-button {
+ cursor: pointer;
+
+ &:hover {
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+}
+
+</style>
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index 57f3caf0..ace8cc7c 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -63,7 +63,8 @@ const registration = {
await this.signUp(this.user)
this.$router.push({ name: 'friends' })
} catch (error) {
- console.warn('Registration failed: ' + error)
+ console.warn('Registration failed: ', error)
+ this.setCaptcha()
}
}
},
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 222b67a8..fdbda007 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -170,7 +170,7 @@
<label
class="form--label"
for="captcha-label"
- >{{ $t('captcha') }}</label>
+ >{{ $t('registration.captcha') }}</label>
<template v-if="['kocaptcha', 'native'].includes(captcha.type)">
<img
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index e118dbcc..22e14a89 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -323,6 +323,11 @@
{{ $t('settings.notification_visibility_mentions') }}
</Checkbox>
</li>
+ <li>
+ <Checkbox v-model="notificationVisibility.moves">
+ {{ $t('settings.notification_visibility_moves') }}
+ </Checkbox>
+ </li>
</ul>
</div>
<div>
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index 2534eb8f..2181ecc7 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -12,7 +12,7 @@ const SideDrawer = {
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
if (this.currentUser && this.currentUser.locked) {
- this.$store.dispatch('startFetchingFollowRequest')
+ this.$store.dispatch('startFetchingFollowRequests')
}
},
components: { UserCard },
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 25e4de01..df1d22a4 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -88,7 +88,7 @@
</router-link>
</li>
<li
- v-if="federating && !privateMode"
+ v-if="federating && (currentUser || !privateMode)"
@click="toggleDrawer"
>
<router-link to="/main/all">
diff --git a/src/components/staff_panel/staff_panel.js b/src/components/staff_panel/staff_panel.js
index 93e950ad..4f98fff6 100644
--- a/src/components/staff_panel/staff_panel.js
+++ b/src/components/staff_panel/staff_panel.js
@@ -1,3 +1,4 @@
+import map from 'lodash/map'
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const StaffPanel = {
@@ -6,7 +7,7 @@ const StaffPanel = {
},
computed: {
staffAccounts () {
- return this.$store.state.instance.staffAccounts
+ return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _)
}
}
}
diff --git a/src/components/status/status.js b/src/components/status/status.js
index c49e729c..81b57667 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,5 +1,6 @@
import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
+import ReactButton from '../react_button/react_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
import Poll from '../poll/poll.vue'
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
@@ -11,6 +12,7 @@ import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue'
import StatusPopover from '../status_popover/status_popover.vue'
+import EmojiReactions from '../emoji_reactions/emoji_reactions.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 { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
@@ -319,6 +321,7 @@ const Status = {
components: {
Attachment,
FavoriteButton,
+ ReactButton,
RetweetButton,
ExtraButtons,
PostStatusForm,
@@ -329,7 +332,8 @@ const Status = {
LinkPreview,
AvatarList,
Timeago,
- StatusPopover
+ StatusPopover,
+ EmojiReactions
},
methods: {
visibilityIcon (visibility) {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index b1048832..a326874e 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -354,6 +354,10 @@
</div>
</transition>
+ <EmojiReactions
+ :status="status"
+ />
+
<div
v-if="!noHeading && !isPreview"
class="status-actions media-body"
@@ -382,6 +386,10 @@
:logged-in="loggedIn"
:status="status"
/>
+ <ReactButton
+ :logged-in="loggedIn"
+ :status="status"
+ />
<extra-buttons
:status="status"
@onError="showError"
diff --git a/src/components/user_settings/mfa.js b/src/components/user_settings/mfa.js
index 3090138a..abf37062 100644
--- a/src/components/user_settings/mfa.js
+++ b/src/components/user_settings/mfa.js
@@ -139,7 +139,7 @@ const Mfa = {
// fetch settings from server
async fetchSettings () {
- let result = await this.backendInteractor.fetchSettingsMFA()
+ let result = await this.backendInteractor.settingsMFA()
if (result.error) return
this.settings = result.settings
this.settings.available = true
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index d5d671e4..38373056 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -9,6 +9,7 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue'
+import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
import SelectableList from '../selectable_list/selectable_list.vue'
import ProgressButton from '../progress_button/progress_button.vue'
import EmojiInput from '../emoji_input/emoji_input.vue'
@@ -32,6 +33,12 @@ const MuteList = withSubscription({
childPropName: 'items'
})(SelectableList)
+const DomainMuteList = withSubscription({
+ fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
+ select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
+ childPropName: 'items'
+})(SelectableList)
+
const UserSettings = {
data () {
return {
@@ -67,7 +74,8 @@ const UserSettings = {
changedPassword: false,
changePasswordError: false,
activeTab: 'profile',
- notificationSettings: this.$store.state.users.currentUser.notification_settings
+ notificationSettings: this.$store.state.users.currentUser.notification_settings,
+ newDomainToMute: ''
}
},
created () {
@@ -80,10 +88,12 @@ const UserSettings = {
ImageCropper,
BlockList,
MuteList,
+ DomainMuteList,
EmojiInput,
Autosuggest,
BlockCard,
MuteCard,
+ DomainMuteCard,
ProgressButton,
Importer,
Exporter,
@@ -297,7 +307,7 @@ const UserSettings = {
newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2]
}
- this.$store.state.api.backendInteractor.changePassword({ params })
+ this.$store.state.api.backendInteractor.changePassword(params)
.then((res) => {
if (res.status === 'success') {
this.changedPassword = true
@@ -314,7 +324,7 @@ const UserSettings = {
email: this.newEmail,
password: this.changeEmailPassword
}
- this.$store.state.api.backendInteractor.changeEmail({ params })
+ this.$store.state.api.backendInteractor.changeEmail(params)
.then((res) => {
if (res.status === 'success') {
this.changedEmail = true
@@ -365,6 +375,13 @@ const UserSettings = {
unmuteUsers (ids) {
return this.$store.dispatch('unmuteUsers', ids)
},
+ unmuteDomains (domains) {
+ return this.$store.dispatch('unmuteDomains', domains)
+ },
+ muteDomain () {
+ return this.$store.dispatch('muteDomain', this.newDomainToMute)
+ .then(() => { this.newDomainToMute = '' })
+ },
identity (value) {
return value
}
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 3f1982a6..2222c293 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -509,59 +509,114 @@
</div>
<div :label="$t('settings.mutes_tab')">
- <div class="profile-edit-usersearch-wrapper">
- <Autosuggest
- :filter="filterUnMutedUsers"
- :query="queryUserIds"
- :placeholder="$t('settings.search_user_to_mute')"
- >
- <MuteCard
- slot-scope="row"
- :user-id="row.item"
- />
- </Autosuggest>
- </div>
- <MuteList
- :refresh="true"
- :get-key="identity"
- >
- <template
- slot="header"
- slot-scope="{selected}"
- >
- <div class="profile-edit-bulk-actions">
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => muteUsers(selected)"
+ <tab-switcher>
+ <div label="Users">
+ <div class="profile-edit-usersearch-wrapper">
+ <Autosuggest
+ :filter="filterUnMutedUsers"
+ :query="queryUserIds"
+ :placeholder="$t('settings.search_user_to_mute')"
+ >
+ <MuteCard
+ slot-scope="row"
+ :user-id="row.item"
+ />
+ </Autosuggest>
+ </div>
+ <MuteList
+ :refresh="true"
+ :get-key="identity"
+ >
+ <template
+ slot="header"
+ slot-scope="{selected}"
+ >
+ <div class="profile-edit-bulk-actions">
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => muteUsers(selected)"
+ >
+ {{ $t('user_card.mute') }}
+ <template slot="progress">
+ {{ $t('user_card.mute_progress') }}
+ </template>
+ </ProgressButton>
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => unmuteUsers(selected)"
+ >
+ {{ $t('user_card.unmute') }}
+ <template slot="progress">
+ {{ $t('user_card.unmute_progress') }}
+ </template>
+ </ProgressButton>
+ </div>
+ </template>
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <MuteCard :user-id="item" />
+ </template>
+ <template slot="empty">
+ {{ $t('settings.no_mutes') }}
+ </template>
+ </MuteList>
+ </div>
+
+ <div :label="$t('settings.domain_mutes')">
+ <div class="profile-edit-domain-mute-form">
+ <input
+ v-model="newDomainToMute"
+ :placeholder="$t('settings.type_domains_to_mute')"
+ type="text"
+ @keyup.enter="muteDomain"
>
- {{ $t('user_card.mute') }}
- <template slot="progress">
- {{ $t('user_card.mute_progress') }}
- </template>
- </ProgressButton>
<ProgressButton
- v-if="selected.length > 0"
class="btn btn-default"
- :click="() => unmuteUsers(selected)"
+ :click="muteDomain"
>
- {{ $t('user_card.unmute') }}
+ {{ $t('domain_mute_card.mute') }}
<template slot="progress">
- {{ $t('user_card.unmute_progress') }}
+ {{ $t('domain_mute_card.mute_progress') }}
</template>
</ProgressButton>
</div>
- </template>
- <template
- slot="item"
- slot-scope="{item}"
- >
- <MuteCard :user-id="item" />
- </template>
- <template slot="empty">
- {{ $t('settings.no_mutes') }}
- </template>
- </MuteList>
+ <DomainMuteList
+ :refresh="true"
+ :get-key="identity"
+ >
+ <template
+ slot="header"
+ slot-scope="{selected}"
+ >
+ <div class="profile-edit-bulk-actions">
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => unmuteDomains(selected)"
+ >
+ {{ $t('domain_mute_card.unmute') }}
+ <template slot="progress">
+ {{ $t('domain_mute_card.unmute_progress') }}
+ </template>
+ </ProgressButton>
+ </div>
+ </template>
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <DomainMuteCard :domain="item" />
+ </template>
+ <template slot="empty">
+ {{ $t('settings.no_mutes') }}
+ </template>
+ </DomainMuteList>
+ </div>
+ </tab-switcher>
</div>
</tab-switcher>
</div>
@@ -639,6 +694,18 @@
}
}
+ &-domain-mute-form {
+ padding: 1em;
+ display: flex;
+ flex-direction: column;
+
+ button {
+ align-self: flex-end;
+ margin-top: 1em;
+ width: 10em;
+ }
+ }
+
.setting-subitem {
margin-left: 1.75em;
}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 06cc12f0..4efa41c3 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -21,6 +21,12 @@
"chat": {
"title": "Chat"
},
+ "domain_mute_card": {
+ "mute": "Mute",
+ "mute_progress": "Muting...",
+ "unmute": "Unmute",
+ "unmute_progress": "Unmuting..."
+ },
"exporter": {
"export": "Export",
"processing": "Processing, you'll soon be asked to download your file"
@@ -111,7 +117,8 @@
"notifications": "Notifications",
"read": "Read!",
"repeated_you": "repeated your status",
- "no_more_notifications": "No more notifications"
+ "no_more_notifications": "No more notifications",
+ "migrated_to": "migrated to"
},
"polls": {
"add_poll": "Add Poll",
@@ -141,6 +148,7 @@
"interactions": {
"favs_repeats": "Repeats and Favorites",
"follows": "New follows",
+ "moves": "User migrates",
"load_older": "Load older interactions"
},
"post_status": {
@@ -263,6 +271,7 @@
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
"delete_account_instructions": "Type your password in the input below to confirm account deletion.",
"discoverable": "Allow discovery of this account in search results and other services",
+ "domain_mutes": "Domains",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
"pad_emoji": "Pad emoji with spaces when adding from picker",
"export_theme": "Save preset",
@@ -313,6 +322,7 @@
"notification_visibility_likes": "Likes",
"notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats",
+ "notification_visibility_moves": "User Migrates",
"no_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks",
"no_mutes": "No mutes",
@@ -360,6 +370,7 @@
"post_status_content_type": "Post status content type",
"stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
+ "user_mutes": "Users",
"useStreamingApi": "Receive posts and notifications real-time",
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
"text": "Text",
@@ -368,6 +379,7 @@
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
"tooltipRadius": "Tooltips/alerts",
+ "type_domains_to_mute": "Type in domains to mute",
"upload_a_photo": "Upload a photo",
"user_settings": "User Settings",
"values": {
@@ -667,6 +679,7 @@
"repeat": "Repeat",
"reply": "Reply",
"favorite": "Favorite",
+ "add_reaction": "Add Reaction",
"user_settings": "User Settings"
},
"upload":{
diff --git a/src/lib/event_target_polyfill.js b/src/lib/event_target_polyfill.js
new file mode 100644
index 00000000..2042c770
--- /dev/null
+++ b/src/lib/event_target_polyfill.js
@@ -0,0 +1,9 @@
+import EventTargetPolyfill from '@ungap/event-target'
+
+try {
+ /* eslint-disable no-new */
+ new EventTarget()
+ /* eslint-enable no-new */
+} catch (e) {
+ window.EventTarget = EventTargetPolyfill
+}
diff --git a/src/main.js b/src/main.js
index a9db1cff..baf73ac8 100644
--- a/src/main.js
+++ b/src/main.js
@@ -2,6 +2,9 @@ import Vue from 'vue'
import VueRouter from 'vue-router'
import Vuex from 'vuex'
+import 'custom-event-polyfill'
+import './lib/event_target_polyfill.js'
+
import interfaceModule from './modules/interface.js'
import instanceModule from './modules/instance.js'
import statusesModule from './modules/statuses.js'
diff --git a/src/modules/api.js b/src/modules/api.js
index 9c296275..748570e5 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -146,6 +146,7 @@ const api = {
startFetchingFollowRequests (store) {
if (store.state.fetchers['followRequests']) return
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
+
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
},
stopFetchingFollowRequests (store) {
diff --git a/src/modules/config.js b/src/modules/config.js
index ee474b16..eb763c10 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -31,7 +31,8 @@ export const defaultState = {
follows: true,
mentions: true,
likes: true,
- repeats: true
+ repeats: true,
+ moves: true
},
webPushNotifications: false,
muteWords: [],
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 7d88761c..ea0c1749 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,4 +1,17 @@
-import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
+import {
+ remove,
+ slice,
+ each,
+ findIndex,
+ find,
+ maxBy,
+ minBy,
+ merge,
+ first,
+ last,
+ isArray,
+ omitBy
+} from 'lodash'
import { set } from 'vue'
import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js'
@@ -67,7 +80,8 @@ const visibleNotificationTypes = (rootState) => {
rootState.config.notificationVisibility.likes && 'like',
rootState.config.notificationVisibility.mentions && 'mention',
rootState.config.notificationVisibility.repeats && 'repeat',
- rootState.config.notificationVisibility.follows && 'follow'
+ rootState.config.notificationVisibility.follows && 'follow',
+ rootState.config.notificationVisibility.moves && 'move'
].filter(_ => _)
}
@@ -306,7 +320,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
each(notifications, (notification) => {
- if (notification.type !== 'follow') {
+ if (notification.type !== 'follow' && notification.type !== 'move') {
notification.action = addStatusToGlobalStorage(state, notification.action).item
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
}
@@ -339,6 +353,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
case 'follow':
i18nString = 'followed_you'
break
+ case 'move':
+ i18nString = 'migrated_to'
+ break
}
if (i18nString) {
@@ -514,6 +531,50 @@ export const mutations = {
newStatus.fave_num = newStatus.favoritedBy.length
newStatus.favorited = !!newStatus.favoritedBy.find(({ id }) => currentUser.id === id)
},
+ addEmojiReactionsBy (state, { id, emojiReactions, currentUser }) {
+ const status = state.allStatusesObject[id]
+ set(status, 'emoji_reactions', emojiReactions)
+ },
+ addOwnReaction (state, { id, emoji, currentUser }) {
+ const status = state.allStatusesObject[id]
+ const reactionIndex = findIndex(status.emoji_reactions, { emoji })
+ const reaction = status.emoji_reactions[reactionIndex] || { emoji, count: 0, accounts: [] }
+
+ const newReaction = {
+ ...reaction,
+ count: reaction.count + 1,
+ accounts: [
+ ...reaction.accounts,
+ currentUser
+ ]
+ }
+
+ // Update count of existing reaction if it exists, otherwise append at the end
+ if (reactionIndex >= 0) {
+ set(status.emoji_reactions, reactionIndex, newReaction)
+ } else {
+ set(status, 'emoji_reactions', [...status.emoji_reactions, newReaction])
+ }
+ },
+ removeOwnReaction (state, { id, emoji, currentUser }) {
+ const status = state.allStatusesObject[id]
+ const reactionIndex = findIndex(status.emoji_reactions, { emoji })
+ if (reactionIndex < 0) return
+
+ const reaction = status.emoji_reactions[reactionIndex]
+
+ const newReaction = {
+ ...reaction,
+ count: reaction.count - 1,
+ accounts: reaction.accounts.filter(acc => acc.id === currentUser.id)
+ }
+
+ if (newReaction.count > 0) {
+ set(status.emoji_reactions, reactionIndex, newReaction)
+ } else {
+ set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.emoji !== emoji))
+ }
+ },
updateStatusWithPoll (state, { id, poll }) {
const status = state.allStatusesObject[id]
status.poll = poll
@@ -618,6 +679,31 @@ const statuses = {
commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
})
},
+ reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
+ const currentUser = rootState.users.currentUser
+ commit('addOwnReaction', { id, emoji, currentUser })
+ rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(
+ status => {
+ dispatch('fetchEmojiReactionsBy', id)
+ }
+ )
+ },
+ unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
+ const currentUser = rootState.users.currentUser
+ commit('removeOwnReaction', { id, emoji, currentUser })
+ rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then(
+ status => {
+ dispatch('fetchEmojiReactionsBy', id)
+ }
+ )
+ },
+ fetchEmojiReactionsBy ({ rootState, commit }, id) {
+ rootState.api.backendInteractor.fetchEmojiReactions({ id }).then(
+ emojiReactions => {
+ commit('addEmojiReactionsBy', { id, emojiReactions, currentUser: rootState.users.currentUser })
+ }
+ )
+ },
fetchFavs ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchFavoritedByUsers({ id })
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
diff --git a/src/modules/users.js b/src/modules/users.js
index e54588df..ce3e595d 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -72,6 +72,16 @@ const showReblogs = (store, userId) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
+const muteDomain = (store, domain) => {
+ return store.rootState.api.backendInteractor.muteDomain({ domain })
+ .then(() => store.commit('addDomainMute', domain))
+}
+
+const unmuteDomain = (store, domain) => {
+ return store.rootState.api.backendInteractor.unmuteDomain({ domain })
+ .then(() => store.commit('removeDomainMute', domain))
+}
+
export const mutations = {
setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id]
@@ -177,6 +187,20 @@ export const mutations = {
state.currentUser.muteIds.push(muteId)
}
},
+ saveDomainMutes (state, domainMutes) {
+ state.currentUser.domainMutes = domainMutes
+ },
+ addDomainMute (state, domain) {
+ if (state.currentUser.domainMutes.indexOf(domain) === -1) {
+ state.currentUser.domainMutes.push(domain)
+ }
+ },
+ removeDomainMute (state, domain) {
+ const index = state.currentUser.domainMutes.indexOf(domain)
+ if (index !== -1) {
+ state.currentUser.domainMutes.splice(index, 1)
+ }
+ },
setPinnedToUser (state, status) {
const user = state.usersObject[status.user.id]
const index = user.pinnedStatusIds.indexOf(status.id)
@@ -297,6 +321,25 @@ const users = {
unmuteUsers (store, ids = []) {
return Promise.all(ids.map(id => unmuteUser(store, id)))
},
+ fetchDomainMutes (store) {
+ return store.rootState.api.backendInteractor.fetchDomainMutes()
+ .then((domainMutes) => {
+ store.commit('saveDomainMutes', domainMutes)
+ return domainMutes
+ })
+ },
+ muteDomain (store, domain) {
+ return muteDomain(store, domain)
+ },
+ unmuteDomain (store, domain) {
+ return unmuteDomain(store, domain)
+ },
+ muteDomains (store, domains = []) {
+ return Promise.all(domains.map(domain => muteDomain(store, domain)))
+ },
+ unmuteDomains (store, domain = []) {
+ return Promise.all(domain.map(domain => unmuteDomain(store, domain)))
+ },
fetchFriends ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.friendIds)
@@ -373,8 +416,10 @@ const users = {
},
addNewNotifications (store, { notifications }) {
const users = map(notifications, 'from_profile')
+ const targetUsers = map(notifications, 'target')
const notificationIds = notifications.map(_ => _.id)
store.commit('addNewUsers', users)
+ store.commit('addNewUsers', targetUsers)
const notificationsObject = store.rootState.statuses.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject)
@@ -399,7 +444,9 @@ const users = {
let rootState = store.rootState
try {
- let data = await rootState.api.backendInteractor.register({ ...userInfo })
+ let data = await rootState.api.backendInteractor.register(
+ { params: { ...userInfo } }
+ )
store.commit('signUpSuccess')
store.commit('setToken', data.access_token)
store.dispatch('loginUser', data.access_token)
@@ -456,6 +503,7 @@ const users = {
user.credentials = accessToken
user.blockIds = []
user.muteIds = []
+ user.domainMutes = []
commit('setCurrentUser', user)
commit('addNewUsers', [user])
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index ef0267aa..11aa0675 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -72,7 +72,11 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
+const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
const MASTODON_STREAMING = '/api/v1/streaming'
+const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/emoji_reactions_by`
+const PLEROMA_EMOJI_REACT_URL = id => `/api/v1/pleroma/statuses/${id}/react_with_emoji`
+const PLEROMA_EMOJI_UNREACT_URL = id => `/api/v1/pleroma/statuses/${id}/unreact_with_emoji`
const oldfetch = window.fetch
@@ -880,6 +884,28 @@ const fetchRebloggedByUsers = ({ id }) => {
return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
}
+const fetchEmojiReactions = ({ id }) => {
+ return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id) })
+}
+
+const reactWithEmoji = ({ id, emoji, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_EMOJI_REACT_URL(id),
+ method: 'POST',
+ credentials,
+ payload: { emoji }
+ }).then(parseStatus)
+}
+
+const unreactWithEmoji = ({ id, emoji, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_EMOJI_UNREACT_URL(id),
+ method: 'POST',
+ credentials,
+ payload: { emoji }
+ }).then(parseStatus)
+}
+
const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
return promisedRequest({
url: MASTODON_REPORT_USER_URL,
@@ -948,6 +974,28 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
})
}
+const fetchDomainMutes = ({ credentials }) => {
+ return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
+}
+
+const muteDomain = ({ domain, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_DOMAIN_BLOCKS_URL,
+ method: 'POST',
+ payload: { domain },
+ credentials
+ })
+}
+
+const unmuteDomain = ({ domain, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_DOMAIN_BLOCKS_URL,
+ method: 'DELETE',
+ payload: { domain },
+ credentials
+ })
+}
+
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({
...(credentials
@@ -1107,10 +1155,16 @@ const apiService = {
fetchPoll,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
+ fetchEmojiReactions,
+ reactWithEmoji,
+ unreactWithEmoji,
reportUser,
updateNotificationSettings,
search2,
- searchUsers
+ searchUsers,
+ fetchDomainMutes,
+ muteDomain,
+ unmuteDomain
}
export default apiService
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index b7372ed0..e1c32860 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -16,7 +16,7 @@ const backendInteractorService = credentials => ({
return notificationsFetcher.fetchAndUpdate({ store, credentials })
},
- startFetchingFollowRequest ({ store }) {
+ startFetchingFollowRequests ({ store }) {
return followRequestFetcher.startFetching({ store, credentials })
},
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index ca79df6f..a3d0b782 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -242,6 +242,7 @@ export const parseStatus = (data) => {
output.is_local = pleroma.local
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
output.thread_muted = pleroma.thread_muted
+ output.emoji_reactions = pleroma.emoji_reactions
} else {
output.text = data.content
output.summary = data.spoiler_text
@@ -341,10 +342,13 @@ export const parseNotification = (data) => {
if (masto) {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
- output.status = output.type === 'follow'
+ output.status = output.type === 'follow' || output.type === 'move'
? null
: parseStatus(data.status)
output.action = output.status // TODO: Refactor, this is unneeded
+ output.target = output.type !== 'move'
+ ? null
+ : parseUser(data.target)
output.from_profile = parseUser(data.account)
} else {
const parsedNotice = parseStatus(data.notice)
diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js
index 590552da..d4cf9132 100644
--- a/src/services/errors/errors.js
+++ b/src/services/errors/errors.js
@@ -32,12 +32,18 @@ export class RegistrationError extends Error {
}
if (typeof error === 'object') {
+ const errorContents = JSON.parse(error.error)
+ // keys will have the property that has the error, for example 'ap_id',
+ // 'email' or 'captcha', the value will be an array of its error
+ // like "ap_id": ["has been taken"] or "captcha": ["Invalid CAPTCHA"]
+
// replace ap_id with username
- if (error.ap_id) {
- error.username = error.ap_id
- delete error.ap_id
+ if (errorContents.ap_id) {
+ errorContents.username = errorContents.ap_id
+ delete errorContents.ap_id
}
- this.message = humanizeErrors(error)
+
+ this.message = humanizeErrors(errorContents)
} else {
this.message = error
}
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index 7021adbd..860620fc 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -6,7 +6,8 @@ export const visibleTypes = store => ([
store.state.config.notificationVisibility.likes && 'like',
store.state.config.notificationVisibility.mentions && 'mention',
store.state.config.notificationVisibility.repeats && 'repeat',
- store.state.config.notificationVisibility.follows && 'follow'
+ store.state.config.notificationVisibility.follows && 'follow',
+ store.state.config.notificationVisibility.moves && 'move'
].filter(_ => _))
const sortById = (a, b) => {
@@ -25,7 +26,7 @@ const sortById = (a, b) => {
}
}
-export const visibleNotificationsFromStore = (store, types) => {
+export const filteredNotificationsFromStore = (store, types) => {
// map is just to clone the array since sort mutates it and it causes some issues
let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
sortedNotifications = sortBy(sortedNotifications, 'seen')
@@ -35,4 +36,4 @@ export const visibleNotificationsFromStore = (store, types) => {
}
export const unseenNotificationsFromStore = store =>
- filter(visibleNotificationsFromStore(store), ({ seen }) => !seen)
+ filter(filteredNotificationsFromStore(store), ({ seen }) => !seen)
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 47008026..64499a1b 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -2,7 +2,6 @@ import apiService from '../api/api.service.js'
const update = ({ store, notifications, older }) => {
store.dispatch('setNotificationsError', { value: false })
-
store.dispatch('addNewNotifications', { notifications, older })
}
@@ -30,9 +29,9 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
// load unread notifications repeatedly to provide consistency between browser tabs
const notifications = timelineData.data
- const unread = notifications.filter(n => !n.seen).map(n => n.id)
- if (unread.length) {
- args['since'] = Math.min(...unread)
+ const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
+ if (readNotifsIds.length) {
+ args['since'] = Math.max(...readNotifsIds)
fetchNotifications({ store, args, older })
}
diff --git a/src/services/push/push.js b/src/services/push/push.js
index 1b189a29..5836fc26 100644
--- a/src/services/push/push.js
+++ b/src/services/push/push.js
@@ -65,7 +65,8 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility)
follow: notificationVisibility.follows,
favourite: notificationVisibility.likes,
mention: notificationVisibility.mentions,
- reblog: notificationVisibility.repeats
+ reblog: notificationVisibility.repeats,
+ move: notificationVisibility.moves
}
}
})
diff --git a/static/fontello.json b/static/fontello.json
index c1ed3393..829241b5 100755
--- a/static/fontello.json
+++ b/static/fontello.json
@@ -333,6 +333,12 @@
"css": "login",
"code": 59424,
"src": "fontawesome"
+ },
+ {
+ "uid": "f3ebd6751c15a280af5cc5f4a764187d",
+ "css": "arrow-curved",
+ "code": 59426,
+ "src": "iconic"
}
]
} \ No newline at end of file
diff --git a/static/styles.json b/static/styles.json
index 2bddc8d8..54c0335f 100644
--- a/static/styles.json
+++ b/static/styles.json
@@ -1,6 +1,7 @@
{
"pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
+ "pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
"classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js
index f794997b..e53aa388 100644
--- a/test/unit/specs/modules/statuses.spec.js
+++ b/test/unit/specs/modules/statuses.spec.js
@@ -241,6 +241,51 @@ describe('Statuses module', () => {
})
})
+ describe('emojiReactions', () => {
+ it('increments count in existing reaction', () => {
+ const state = defaultState()
+ const status = makeMockStatus({ id: '1' })
+ status.emoji_reactions = [ { emoji: '😂', count: 1, accounts: [] } ]
+
+ mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+ mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
+ expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(2)
+ expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me')
+ })
+
+ it('adds a new reaction', () => {
+ const state = defaultState()
+ const status = makeMockStatus({ id: '1' })
+ status.emoji_reactions = []
+
+ mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+ mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
+ expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
+ expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me')
+ })
+
+ it('decreases count in existing reaction', () => {
+ const state = defaultState()
+ const status = makeMockStatus({ id: '1' })
+ status.emoji_reactions = [ { emoji: '😂', count: 2, accounts: [{ id: 'me' }] } ]
+
+ mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+ mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} })
+ expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
+ expect(state.allStatusesObject['1'].emoji_reactions[0].accounts).to.eql([])
+ })
+
+ it('removes a reaction', () => {
+ const state = defaultState()
+ const status = makeMockStatus({ id: '1' })
+ status.emoji_reactions = [{ emoji: '😂', count: 1, accounts: [{ id: 'me' }] }]
+
+ mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
+ mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} })
+ expect(state.allStatusesObject['1'].emoji_reactions.length).to.eql(0)
+ })
+ })
+
describe('showNewStatuses', () => {
it('resets the minId to the min of the visible statuses when adding new to visible statuses', () => {
const state = defaultState()
diff --git a/test/unit/specs/services/notification_utils/notification_utils.spec.js b/test/unit/specs/services/notification_utils/notification_utils.spec.js
index 1baa5fc9..00628d83 100644
--- a/test/unit/specs/services/notification_utils/notification_utils.spec.js
+++ b/test/unit/specs/services/notification_utils/notification_utils.spec.js
@@ -1,7 +1,7 @@
import * as NotificationUtils from 'src/services/notification_utils/notification_utils.js'
describe('NotificationUtils', () => {
- describe('visibleNotificationsFromStore', () => {
+ describe('filteredNotificationsFromStore', () => {
it('should return sorted notifications with configured types', () => {
const store = {
state: {
@@ -47,7 +47,7 @@ describe('NotificationUtils', () => {
type: 'like'
}
]
- expect(NotificationUtils.visibleNotificationsFromStore(store)).to.eql(expected)
+ expect(NotificationUtils.filteredNotificationsFromStore(store)).to.eql(expected)
})
})
diff --git a/yarn.lock b/yarn.lock
index 4b20a6a1..1a5d4cef 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -710,6 +710,11 @@
dependencies:
qrcode "^1.3.0"
+"@ungap/event-target@^0.1.0":
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b"
+ integrity sha512-W2oyj0Fe1w/XhPZjkI3oUcDUAmu5P4qsdT2/2S8aMhtAWM/CE/jYWtji0pKNPDfxLI75fa5gWSEmnynKMNP/oA==
+
"@vue/babel-helper-vue-jsx-merge-props@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040"
@@ -2281,6 +2286,11 @@ currently-unhandled@^0.4.1:
dependencies:
array-find-index "^1.0.1"
+custom-event-polyfill@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
+ integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
+
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"