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 @@
+
+
+
+ {{ domain }}
+
+
+ {{ $t('domain_mute_card.unmute') }}
+
+ {{ $t('domain_mute_card.unmute_progress') }}
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
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')"
/>
+
{{ $t("nav.public_tl") }}
-
+
{{ $t("nav.twkn") }}
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 @@
{{ $t('notifications.followed_you') }}
+
+
+ {{ $t('notifications.migrated_to') }}
+
@@ -115,6 +119,14 @@
@{{ notification.from_profile.screen_name }}
+
+
+ @{{ notification.target.screen_name }}
+
+
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 @@
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 @@
+
+
+
+
+
+
+
+
+ {{ emoji }}
+
+
+
+ {{ emoji.replacement }}
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+ >{{ $t('registration.captcha') }}
+
+
+ {{ $t('settings.notification_visibility_moves') }}
+
+
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 @@
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 @@
+
+
+
$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 @@
-
-
-
-
-
+
+
+
- {{ $t('user_card.mute') }}
-
- {{ $t('user_card.mute_progress') }}
-
-
-
+
+
+
+
- {{ $t('user_card.unmute') }}
+
+
+ {{ $t('user_card.mute') }}
+
+ {{ $t('user_card.mute_progress') }}
+
+
+
+ {{ $t('user_card.unmute') }}
+
+ {{ $t('user_card.unmute_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
+
+
+
+
-
-
-
-
-
- {{ $t('settings.no_mutes') }}
-
-
+
+
+
+
+ {{ $t('domain_mute_card.unmute') }}
+
+ {{ $t('domain_mute_card.unmute_progress') }}
+
+
+
+
+
+
+
+
+ {{ $t('settings.no_mutes') }}
+
+
+
+
@@ -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"