aboutsummaryrefslogtreecommitdiff
path: root/src/modules
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules')
-rw-r--r--src/modules/api.js31
-rw-r--r--src/modules/chats.js22
-rw-r--r--src/modules/config.js70
-rw-r--r--src/modules/editStatus.js25
-rw-r--r--src/modules/errors.js4
-rw-r--r--src/modules/instance.js187
-rw-r--r--src/modules/interface.js50
-rw-r--r--src/modules/lists.js130
-rw-r--r--src/modules/media_viewer.js9
-rw-r--r--src/modules/oauth.js4
-rw-r--r--src/modules/polls.js13
-rw-r--r--src/modules/reports.js44
-rw-r--r--src/modules/serverSideConfig.js140
-rw-r--r--src/modules/serverSideStorage.js427
-rw-r--r--src/modules/shout.js15
-rw-r--r--src/modules/statusHistory.js25
-rw-r--r--src/modules/statuses.js46
-rw-r--r--src/modules/users.js96
18 files changed, 1209 insertions, 129 deletions
diff --git a/src/modules/api.js b/src/modules/api.js
index 54f94356..0acc03f1 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -15,6 +15,9 @@ const api = {
mastoUserSocketStatus: null,
followRequests: []
},
+ getters: {
+ followRequestCount: state => state.followRequests.length
+ },
mutations: {
setBackendInteractor (state, backendInteractor) {
state.backendInteractor = backendInteractor
@@ -100,6 +103,13 @@ const api = {
showImmediately: timelineData.visibleStatuses.length === 0,
timeline: 'friends'
})
+ } else if (message.event === 'status.update') {
+ dispatch('addNewStatuses', {
+ statuses: [message.status],
+ userId: false,
+ showImmediately: message.status.id in timelineData.visibleStatusesObject,
+ timeline: 'friends'
+ })
} else if (message.event === 'delete') {
dispatch('deleteStatusById', message.id)
} else if (message.event === 'pleroma:chat_update') {
@@ -191,12 +201,13 @@ const api = {
startFetchingTimeline (store, {
timeline = 'friends',
tag = false,
- userId = false
+ userId = false,
+ listId = false
}) {
if (store.state.fetchers[timeline]) return
const fetcher = store.state.backendInteractor.startFetchingTimeline({
- timeline, store, userId, tag
+ timeline, store, userId, listId, tag
})
store.commit('addFetcher', { fetcherName: timeline, fetcher })
},
@@ -233,7 +244,7 @@ const api = {
// Follow requests
startFetchingFollowRequests (store) {
- if (store.state.fetchers['followRequests']) return
+ if (store.state.fetchers.followRequests) return
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
@@ -244,10 +255,22 @@ const api = {
store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher })
},
removeFollowRequest (store, request) {
- let requests = store.state.followRequests.filter((it) => it !== request)
+ const requests = store.state.followRequests.filter((it) => it !== request)
store.commit('setFollowRequests', requests)
},
+ // Lists
+ startFetchingLists (store) {
+ if (store.state.fetchers.lists) return
+ const fetcher = store.state.backendInteractor.startFetchingLists({ store })
+ store.commit('addFetcher', { fetcherName: 'lists', fetcher })
+ },
+ stopFetchingLists (store) {
+ const fetcher = store.state.fetchers.lists
+ if (!fetcher) return
+ store.commit('removeFetcher', { fetcherName: 'lists', fetcher })
+ },
+
// Pleroma websocket
setWsToken (store, token) {
store.commit('setWsToken', token)
diff --git a/src/modules/chats.js b/src/modules/chats.js
index 69d683bd..f28c2603 100644
--- a/src/modules/chats.js
+++ b/src/modules/chats.js
@@ -1,4 +1,4 @@
-import Vue from 'vue'
+import { reactive } from 'vue'
import { find, omitBy, orderBy, sumBy } from 'lodash'
import chatService from '../services/chat_service/chat_service.js'
import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
@@ -13,8 +13,8 @@ const emptyChatList = () => ({
const defaultState = {
chatList: emptyChatList(),
chatListFetcher: null,
- openedChats: {},
- openedChatMessageServices: {},
+ openedChats: reactive({}),
+ openedChatMessageServices: reactive({}),
fetcher: undefined,
currentChatId: null,
lastReadMessageId: null
@@ -137,10 +137,10 @@ const chats = {
},
addOpenedChat (state, { _dispatch, chat }) {
state.currentChatId = chat.id
- Vue.set(state.openedChats, chat.id, chat)
+ state.openedChats[chat.id] = chat
if (!state.openedChatMessageServices[chat.id]) {
- Vue.set(state.openedChatMessageServices, chat.id, chatService.empty(chat.id))
+ state.openedChatMessageServices[chat.id] = chatService.empty(chat.id)
}
},
setCurrentChatId (state, { chatId }) {
@@ -160,7 +160,7 @@ const chats = {
}
} else {
state.chatList.data.push(updatedChat)
- Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+ state.chatList.idStore[updatedChat.id] = updatedChat
}
})
},
@@ -172,7 +172,7 @@ const chats = {
chat.updated_at = updatedChat.updated_at
}
if (!chat) { state.chatList.data.unshift(updatedChat) }
- Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+ state.chatList.idStore[updatedChat.id] = updatedChat
},
deleteChat (state, { _dispatch, id, _rootGetters }) {
state.chats.data = state.chats.data.filter(conversation =>
@@ -186,8 +186,8 @@ const chats = {
commit('setChatListFetcher', { fetcher: undefined })
for (const chatId in state.openedChats) {
chatService.clear(state.openedChatMessageServices[chatId])
- Vue.delete(state.openedChats, chatId)
- Vue.delete(state.openedChatMessageServices, chatId)
+ delete state.openedChats[chatId]
+ delete state.openedChatMessageServices[chatId]
}
},
setChatsLoading (state, { value }) {
@@ -215,8 +215,8 @@ const chats = {
for (const chatId in state.openedChats) {
if (currentChatId !== chatId) {
chatService.clear(state.openedChatMessageServices[chatId])
- Vue.delete(state.openedChats, chatId)
- Vue.delete(state.openedChatMessageServices, chatId)
+ delete state.openedChats[chatId]
+ delete state.openedChatMessageServices[chatId]
}
}
},
diff --git a/src/modules/config.js b/src/modules/config.js
index 33e2cb50..c966602e 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,6 +1,9 @@
-import { set, delete as del } from 'vue'
-import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
+import Cookies from 'js-cookie'
+import { setPreset, applyTheme, applyConfig } from '../services/style_setter/style_setter.js'
import messages from '../i18n/messages'
+import localeService from '../services/locale/locale.service.js'
+
+const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
const browserLocale = (window.navigator.language || 'en').split('-')[0]
@@ -11,10 +14,15 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
*/
export const multiChoiceProperties = [
'postContentType',
- 'subjectLineBehavior'
+ 'subjectLineBehavior',
+ 'conversationDisplay', // tree | linear
+ 'conversationOtherRepliesButton', // below | inside
+ 'mentionLinkDisplay', // short | full_for_remote | full
+ 'userPopoverAvatarAction' // close | zoom | open
]
export const defaultState = {
+ expertLevel: 0, // used to track which settings to show and hide
colors: {},
theme: undefined,
customTheme: undefined,
@@ -24,6 +32,9 @@ export const defaultState = {
hideShoutbox: false,
// bad name: actually hides posts of muted USERS
hideMutedPosts: undefined, // instance default
+ hideMutedThreads: undefined, // instance default
+ hideWordFilteredPosts: undefined, // instance default
+ muteBotStatuses: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default
padEmoji: true,
hideAttachments: false,
@@ -35,10 +46,12 @@ export const defaultState = {
loopVideoSilentOnly: true,
streaming: false,
emojiReactionsOnTimeline: true,
+ alwaysShowNewPostButton: false,
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
- stopGifs: false,
+ stopGifs: true,
replyVisibility: 'all',
+ thirdColumnMode: 'notifications',
notificationVisibility: {
follows: true,
mentions: true,
@@ -47,7 +60,9 @@ export const defaultState = {
moves: true,
emojiReactions: true,
followRequest: true,
- chatMention: true
+ reports: true,
+ chatMention: true,
+ polls: true
},
webPushNotifications: false,
muteWords: [],
@@ -65,12 +80,33 @@ export const defaultState = {
hideFilteredStatuses: undefined, // instance default
playVideosInModal: false,
useOneClickNsfw: false,
- useContainFit: false,
+ useContainFit: true,
+ disableStickyHeaders: false,
+ showScrollbars: false,
+ userPopoverAvatarAction: 'close',
+ userPopoverOverlay: true,
+ sidebarColumnWidth: '25rem',
+ contentColumnWidth: '45rem',
+ notifsColumnWidth: '25rem',
+ navbarColumnStretch: false,
greentext: undefined, // instance default
+ useAtIcon: undefined, // instance default
+ mentionLinkDisplay: undefined, // instance default
+ mentionLinkShowTooltip: undefined, // instance default
+ mentionLinkShowAvatar: undefined, // instance default
+ mentionLinkFadeDomain: undefined, // instance default
+ mentionLinkShowYous: undefined, // instance default
+ mentionLinkBoldenYou: undefined, // instance default
hidePostStats: undefined, // instance default
+ hideBotIndication: undefined, // instance default
hideUserStats: undefined, // instance default
virtualScrolling: undefined, // instance default
- sensitiveByDefault: undefined // instance default
+ sensitiveByDefault: undefined, // instance default
+ conversationDisplay: undefined, // instance default
+ conversationTreeAdvanced: undefined, // instance default
+ conversationOtherRepliesButton: undefined, // instance default
+ conversationTreeFadeAncestors: undefined, // instance default
+ maxDepthInThread: undefined // instance default
}
// caching the instance default properties
@@ -101,14 +137,14 @@ const config = {
},
mutations: {
setOption (state, { name, value }) {
- set(state, name, value)
+ state[name] = value
},
setHighlight (state, { user, color, type }) {
const data = this.state.config.highlight[user]
if (color || type) {
- set(state.highlight, user, { color: color || data.color, type: type || data.type })
+ state.highlight[user] = { color: color || data.color, type: type || data.type }
} else {
- del(state.highlight, user)
+ delete state.highlight[user]
}
}
},
@@ -117,7 +153,7 @@ const config = {
const knownKeys = new Set(Object.keys(defaultState))
const presentKeys = new Set(Object.keys(data))
const intersection = new Set()
- for (let elem of presentKeys) {
+ for (const elem of presentKeys) {
if (knownKeys.has(elem)) {
intersection.add(elem)
}
@@ -130,18 +166,28 @@ const config = {
setHighlight ({ commit, dispatch }, { user, color, type }) {
commit('setHighlight', { user, color, type })
},
- setOption ({ commit, dispatch }, { name, value }) {
+ setOption ({ commit, dispatch, state }, { name, value }) {
commit('setOption', { name, value })
switch (name) {
case 'theme':
setPreset(value)
break
+ case 'sidebarColumnWidth':
+ case 'contentColumnWidth':
+ case 'notifsColumnWidth':
+ applyConfig(state)
+ break
case 'customTheme':
case 'customThemeSource':
applyTheme(value)
break
case 'interfaceLanguage':
messages.setLanguage(this.getters.i18n, value)
+ dispatch('loadUnicodeEmojiData', value)
+ Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
+ break
+ case 'thirdColumnMode':
+ dispatch('setLayoutWidth', undefined)
break
}
}
diff --git a/src/modules/editStatus.js b/src/modules/editStatus.js
new file mode 100644
index 00000000..fd316519
--- /dev/null
+++ b/src/modules/editStatus.js
@@ -0,0 +1,25 @@
+const editStatus = {
+ state: {
+ params: null,
+ modalActivated: false
+ },
+ mutations: {
+ openEditStatusModal (state, params) {
+ state.params = params
+ state.modalActivated = true
+ },
+ closeEditStatusModal (state) {
+ state.modalActivated = false
+ }
+ },
+ actions: {
+ openEditStatusModal ({ commit }, params) {
+ commit('openEditStatusModal', params)
+ },
+ closeEditStatusModal ({ commit }) {
+ commit('closeEditStatusModal')
+ }
+ }
+}
+
+export default editStatus
diff --git a/src/modules/errors.js b/src/modules/errors.js
index ca89dc0f..d2e24100 100644
--- a/src/modules/errors.js
+++ b/src/modules/errors.js
@@ -2,8 +2,8 @@ import { capitalize } from 'lodash'
export function humanizeErrors (errors) {
return Object.entries(errors).reduce((errs, [k, val]) => {
- let message = val.reduce((acc, message) => {
- let key = capitalize(k.replace(/_/g, ' '))
+ const message = val.reduce((acc, message) => {
+ const key = capitalize(k.replace(/_/g, ' '))
return acc + [key, message].join(' ') + '. '
}, '')
return [...errs, message]
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 539b9c66..3b15e62e 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -1,8 +1,42 @@
-import { set } from 'vue'
import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import apiService from '../services/api/api.service.js'
import { instanceDefaultProperties } from './config.js'
+import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js'
+
+const SORTED_EMOJI_GROUP_IDS = [
+ 'smileys-and-emotion',
+ 'people-and-body',
+ 'animals-and-nature',
+ 'food-and-drink',
+ 'travel-and-places',
+ 'activities',
+ 'objects',
+ 'symbols',
+ 'flags'
+]
+
+const REGIONAL_INDICATORS = (() => {
+ const start = 0x1F1E6
+ const end = 0x1F1FF
+ const A = 'A'.codePointAt(0)
+ const res = new Array(end - start + 1)
+ for (let i = start; i <= end; ++i) {
+ const letter = String.fromCodePoint(A + i - start)
+ res[i - start] = {
+ replacement: String.fromCodePoint(i),
+ imageUrl: false,
+ displayText: 'regional_indicator_' + letter,
+ displayTextI18n: {
+ key: 'emoji.regional_indicator',
+ args: { letter }
+ }
+ }
+ }
+ return res
+})()
+
+const REMOTE_INTERACTION_URL = '/main/ostatus'
const defaultState = {
// Stuff from apiConfig
@@ -20,16 +54,29 @@ const defaultState = {
background: '/static/aurora_borealis.jpg',
collapseMessageWithSubject: false,
greentext: false,
+ useAtIcon: false,
+ mentionLinkDisplay: 'short',
+ mentionLinkShowTooltip: true,
+ mentionLinkShowAvatar: false,
+ mentionLinkFadeDomain: true,
+ mentionLinkShowYous: false,
+ mentionLinkBoldenYou: true,
hideFilteredStatuses: false,
+ // bad name: actually hides posts of muted USERS
hideMutedPosts: false,
+ hideMutedThreads: true,
+ hideWordFilteredPosts: false,
hidePostStats: false,
+ hideBotIndication: false,
hideSitename: false,
hideUserStats: false,
+ muteBotStatuses: false,
loginMethod: 'password',
logo: '/static/logo.svg',
logoMargin: '.2em',
logoMask: true,
logoLeft: false,
+ disableUpdateNotification: false,
minimalScopesMode: false,
nsfwCensorImage: undefined,
postContentType: 'text/plain',
@@ -43,12 +90,18 @@ const defaultState = {
theme: 'pleroma-dark',
virtualScrolling: true,
sensitiveByDefault: false,
+ conversationDisplay: 'linear',
+ conversationTreeAdvanced: false,
+ conversationOtherRepliesButton: 'below',
+ conversationTreeFadeAncestors: false,
+ maxDepthInThread: 6,
// Nasty stuff
customEmoji: [],
customEmojiFetched: false,
- emoji: [],
+ emoji: {},
emojiFetched: false,
+ unicodeEmojiAnnotations: {},
pleromaBackend: true,
postFormats: [],
restrictedNicknames: [],
@@ -80,16 +133,44 @@ const defaultState = {
}
}
+const loadAnnotations = (lang) => {
+ return import(
+ /* webpackChunkName: "emoji-annotations/[request]" */
+ `@kazvmoe-infra/unicode-emoji-json/annotations/${langCodeToCldrName(lang)}.json`
+ )
+ .then(k => k.default)
+}
+
+const injectAnnotations = (emoji, annotations) => {
+ const availableLangs = Object.keys(annotations)
+
+ return {
+ ...emoji,
+ annotations: availableLangs.reduce((acc, cur) => {
+ acc[cur] = annotations[cur][emoji.replacement]
+ return acc
+ }, {})
+ }
+}
+
+const injectRegionalIndicators = groups => {
+ groups.symbols.push(...REGIONAL_INDICATORS)
+ return groups
+}
+
const instance = {
state: defaultState,
mutations: {
setInstanceOption (state, { name, value }) {
if (typeof value !== 'undefined') {
- set(state, name, value)
+ state[name] = value
}
},
setKnownDomains (state, domains) {
state.knownDomains = domains
+ },
+ setUnicodeEmojiAnnotations (state, { lang, annotations }) {
+ state.unicodeEmojiAnnotations[lang] = annotations
}
},
getters: {
@@ -97,6 +178,56 @@ const instance = {
return instanceDefaultProperties
.map(key => [key, state[key]])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
+ },
+ groupedCustomEmojis (state) {
+ const packsOf = emoji => {
+ return emoji.tags
+ .filter(k => k.startsWith('pack:'))
+ .map(k => k.slice(5)) // remove 'pack:' prefix
+ }
+
+ return state.customEmoji
+ .reduce((res, emoji) => {
+ packsOf(emoji).forEach(packName => {
+ const packId = `custom-${packName}`
+ if (!res[packId]) {
+ res[packId] = ({
+ id: packId,
+ text: packName,
+ image: emoji.imageUrl,
+ emojis: []
+ })
+ }
+ res[packId].emojis.push(emoji)
+ })
+ return res
+ }, {})
+ },
+ standardEmojiList (state) {
+ return SORTED_EMOJI_GROUP_IDS
+ .map(groupId => (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations)))
+ .reduce((a, b) => a.concat(b), [])
+ },
+ standardEmojiGroupList (state) {
+ return SORTED_EMOJI_GROUP_IDS.map(groupId => ({
+ id: groupId,
+ emojis: (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations))
+ }))
+ },
+ instanceDomain (state) {
+ return new URL(state.server).hostname
+ },
+ remoteInteractionLink (state) {
+ const server = state.server.endsWith('/') ? state.server.slice(0, -1) : state.server
+ const link = server + REMOTE_INTERACTION_URL
+
+ return ({ statusId, nickname }) => {
+ if (statusId) {
+ return `${link}?status_id=${statusId}`
+ } else {
+ return `${link}?nickname=${nickname}`
+ }
+ }
}
},
actions: {
@@ -118,32 +249,52 @@ const instance = {
},
async getStaticEmoji ({ commit }) {
try {
- const res = await window.fetch('/static/emoji.json')
- if (res.ok) {
- const values = await res.json()
- const emoji = Object.keys(values).map((key) => {
- return {
- displayText: key,
- imageUrl: false,
- replacement: values[key]
- }
- }).sort((a, b) => a.name > b.name ? 1 : -1)
- commit('setInstanceOption', { name: 'emoji', value: emoji })
- } else {
- throw (res)
- }
+ const values = (await import(/* webpackChunkName: 'emoji' */ '../../static/emoji.json')).default
+
+ const emoji = Object.keys(values).reduce((res, groupId) => {
+ res[groupId] = values[groupId].map(e => ({
+ displayText: e.slug,
+ imageUrl: false,
+ replacement: e.emoji
+ }))
+ return res
+ }, {})
+ commit('setInstanceOption', { name: 'emoji', value: injectRegionalIndicators(emoji) })
} catch (e) {
console.warn("Can't load static emoji")
console.warn(e)
}
},
+ loadUnicodeEmojiData ({ commit, state }, language) {
+ const langList = ensureFinalFallback(language)
+
+ return Promise.all(
+ langList
+ .map(async lang => {
+ if (!state.unicodeEmojiAnnotations[lang]) {
+ const annotations = await loadAnnotations(lang)
+ commit('setUnicodeEmojiAnnotations', { lang, annotations })
+ }
+ }))
+ },
+
async getCustomEmoji ({ commit, state }) {
try {
const res = await window.fetch('/api/pleroma/emoji.json')
if (res.ok) {
const result = await res.json()
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
+ const caseInsensitiveStrCmp = (a, b) => {
+ const la = a.toLowerCase()
+ const lb = b.toLowerCase()
+ return la > lb ? 1 : (la < lb ? -1 : 0)
+ }
+ const byPackThenByName = (a, b) => {
+ const packOf = emoji => (emoji.tags.filter(k => k.startsWith('pack:'))[0] || '').slice(5)
+ return caseInsensitiveStrCmp(packOf(a), packOf(b)) || caseInsensitiveStrCmp(a.displayText, b.displayText)
+ }
+
const emoji = Object.entries(values).map(([key, value]) => {
const imageUrl = value.image_url
return {
@@ -154,7 +305,7 @@ const instance = {
}
// Technically could use tags but those are kinda useless right now,
// should have been "pack" field, that would be more useful
- }).sort((a, b) => a.displayText.toLowerCase() > b.displayText.toLowerCase() ? 1 : -1)
+ }).sort(byPackThenByName)
commit('setInstanceOption', { name: 'customEmoji', value: emoji })
} else {
throw (res)
diff --git a/src/modules/interface.js b/src/modules/interface.js
index d6db32fd..a86193ea 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -1,5 +1,3 @@
-import { set, delete as del } from 'vue'
-
const defaultState = {
settingsModalState: 'hidden',
settingsModalLoaded: false,
@@ -15,7 +13,7 @@ const defaultState = {
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
)
},
- mobileLayout: false,
+ layoutType: 'normal',
globalNotices: [],
layoutHeight: 0,
lastTimeline: null
@@ -29,18 +27,17 @@ const interfaceMod = {
if (state.noticeClearTimeout) {
clearTimeout(state.noticeClearTimeout)
}
- set(state.settings, 'currentSaveStateNotice', { error: false, data: success })
- set(state.settings, 'noticeClearTimeout',
- setTimeout(() => del(state.settings, 'currentSaveStateNotice'), 2000))
+ state.settings.currentSaveStateNotice = { error: false, data: success }
+ state.settings.noticeClearTimeout = setTimeout(() => delete state.settings.currentSaveStateNotice, 2000)
} else {
- set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
+ state.settings.currentSaveStateNotice = { error: true, errorData: error }
}
},
setNotificationPermission (state, permission) {
state.notificationPermission = permission
},
- setMobileLayout (state, value) {
- state.mobileLayout = value
+ setLayoutType (state, value) {
+ state.layoutType = value
},
closeSettingsModal (state) {
state.settingsModalState = 'hidden'
@@ -75,6 +72,9 @@ const interfaceMod = {
setLayoutHeight (state, value) {
state.layoutHeight = value
},
+ setLayoutWidth (state, value) {
+ state.layoutWidth = value
+ },
setLastTimeline (state, value) {
state.lastTimeline = value
}
@@ -89,9 +89,6 @@ const interfaceMod = {
setNotificationPermission ({ commit }, permission) {
commit('setNotificationPermission', permission)
},
- setMobileLayout ({ commit }, value) {
- commit('setMobileLayout', value)
- },
closeSettingsModal ({ commit }) {
commit('closeSettingsModal')
},
@@ -109,7 +106,7 @@ const interfaceMod = {
commit('openSettingsModal')
},
pushGlobalNotice (
- { commit, dispatch },
+ { commit, dispatch, state },
{
messageKey,
messageArgs = {},
@@ -121,11 +118,14 @@ const interfaceMod = {
messageArgs,
level
}
+ commit('pushGlobalNotice', notice)
+ // Adding a new element to array wraps it in a Proxy, which breaks the comparison
+ // TODO: Generate UUID or something instead or relying on !== operator?
+ const newNotice = state.globalNotices[state.globalNotices.length - 1]
if (timeout) {
- setTimeout(() => dispatch('removeGlobalNotice', notice), timeout)
+ setTimeout(() => dispatch('removeGlobalNotice', newNotice), timeout)
}
- commit('pushGlobalNotice', notice)
- return notice
+ return newNotice
},
removeGlobalNotice ({ commit }, notice) {
commit('removeGlobalNotice', notice)
@@ -133,6 +133,24 @@ const interfaceMod = {
setLayoutHeight ({ commit }, value) {
commit('setLayoutHeight', value)
},
+ // value is optional, assuming it was cached prior
+ setLayoutWidth ({ commit, state, rootGetters, rootState }, value) {
+ let width = value
+ if (value !== undefined) {
+ commit('setLayoutWidth', value)
+ } else {
+ width = state.layoutWidth
+ }
+ const mobileLayout = width <= 800
+ const normalOrMobile = mobileLayout ? 'mobile' : 'normal'
+ const { thirdColumnMode } = rootGetters.mergedConfig
+ if (thirdColumnMode === 'none' || !rootState.users.currentUser) {
+ commit('setLayoutType', normalOrMobile)
+ } else {
+ const wideLayout = width >= 1300
+ commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile)
+ }
+ },
setLastTimeline ({ commit }, value) {
commit('setLastTimeline', value)
}
diff --git a/src/modules/lists.js b/src/modules/lists.js
new file mode 100644
index 00000000..22fed800
--- /dev/null
+++ b/src/modules/lists.js
@@ -0,0 +1,130 @@
+import { remove, find } from 'lodash'
+
+export const defaultState = {
+ allLists: [],
+ allListsObject: {}
+}
+
+export const mutations = {
+ setLists (state, value) {
+ state.allLists = value
+ },
+ setList (state, { listId, title }) {
+ if (!state.allListsObject[listId]) {
+ state.allListsObject[listId] = { accountIds: [] }
+ }
+ state.allListsObject[listId].title = title
+
+ const entry = find(state.allLists, { id: listId })
+ if (!entry) {
+ state.allLists.push({ id: listId, title })
+ } else {
+ entry.title = title
+ }
+ },
+ setListAccounts (state, { listId, accountIds }) {
+ if (!state.allListsObject[listId]) {
+ state.allListsObject[listId] = { accountIds: [] }
+ }
+ state.allListsObject[listId].accountIds = accountIds
+ },
+ addListAccount (state, { listId, accountId }) {
+ if (!state.allListsObject[listId]) {
+ state.allListsObject[listId] = { accountIds: [] }
+ }
+ state.allListsObject[listId].accountIds.push(accountId)
+ },
+ removeListAccount (state, { listId, accountId }) {
+ if (!state.allListsObject[listId]) {
+ state.allListsObject[listId] = { accountIds: [] }
+ }
+ const { accountIds } = state.allListsObject[listId]
+ const set = new Set(accountIds)
+ set.delete(accountId)
+ state.allListsObject[listId].accountIds = [...set]
+ },
+ deleteList (state, { listId }) {
+ delete state.allListsObject[listId]
+ remove(state.allLists, list => list.id === listId)
+ }
+}
+
+const actions = {
+ setLists ({ commit }, value) {
+ commit('setLists', value)
+ },
+ createList ({ rootState, commit }, { title }) {
+ return rootState.api.backendInteractor.createList({ title })
+ .then((list) => {
+ commit('setList', { listId: list.id, title })
+ return list
+ })
+ },
+ fetchList ({ rootState, commit }, { listId }) {
+ return rootState.api.backendInteractor.getList({ listId })
+ .then((list) => commit('setList', { listId: list.id, title: list.title }))
+ },
+ fetchListAccounts ({ rootState, commit }, { listId }) {
+ return rootState.api.backendInteractor.getListAccounts({ listId })
+ .then((accountIds) => commit('setListAccounts', { listId, accountIds }))
+ },
+ setList ({ rootState, commit }, { listId, title }) {
+ rootState.api.backendInteractor.updateList({ listId, title })
+ commit('setList', { listId, title })
+ },
+ setListAccounts ({ rootState, commit }, { listId, accountIds }) {
+ const saved = rootState.lists.allListsObject[listId].accountIds || []
+ const added = accountIds.filter(id => !saved.includes(id))
+ const removed = saved.filter(id => !accountIds.includes(id))
+ commit('setListAccounts', { listId, accountIds })
+ if (added.length > 0) {
+ rootState.api.backendInteractor.addAccountsToList({ listId, accountIds: added })
+ }
+ if (removed.length > 0) {
+ rootState.api.backendInteractor.removeAccountsFromList({ listId, accountIds: removed })
+ }
+ },
+ addListAccount ({ rootState, commit }, { listId, accountId }) {
+ return rootState
+ .api
+ .backendInteractor
+ .addAccountsToList({ listId, accountIds: [accountId] })
+ .then((result) => {
+ commit('addListAccount', { listId, accountId })
+ return result
+ })
+ },
+ removeListAccount ({ rootState, commit }, { listId, accountId }) {
+ return rootState
+ .api
+ .backendInteractor
+ .removeAccountsFromList({ listId, accountIds: [accountId] })
+ .then((result) => {
+ commit('removeListAccount', { listId, accountId })
+ return result
+ })
+ },
+ deleteList ({ rootState, commit }, { listId }) {
+ rootState.api.backendInteractor.deleteList({ listId })
+ commit('deleteList', { listId })
+ }
+}
+
+export const getters = {
+ findListTitle: state => id => {
+ if (!state.allListsObject[id]) return
+ return state.allListsObject[id].title
+ },
+ findListAccounts: state => id => {
+ return [...state.allListsObject[id].accountIds]
+ }
+}
+
+const lists = {
+ state: defaultState,
+ mutations,
+ actions,
+ getters
+}
+
+export default lists
diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js
index 721c25e6..ebcba01d 100644
--- a/src/modules/media_viewer.js
+++ b/src/modules/media_viewer.js
@@ -1,4 +1,5 @@
import fileTypeService from '../services/file_type/file_type.service.js'
+const supportedTypes = new Set(['image', 'video', 'audio', 'flash'])
const mediaViewer = {
state: {
@@ -10,7 +11,7 @@ const mediaViewer = {
setMedia (state, media) {
state.media = media
},
- setCurrent (state, index) {
+ setCurrentMedia (state, index) {
state.activated = true
state.currentIndex = index
},
@@ -22,13 +23,13 @@ const mediaViewer = {
setMedia ({ commit }, attachments) {
const media = attachments.filter(attachment => {
const type = fileTypeService.fileType(attachment.mimetype)
- return type === 'image' || type === 'video' || type === 'audio'
+ return supportedTypes.has(type)
})
commit('setMedia', media)
},
- setCurrent ({ commit, state }, current) {
+ setCurrentMedia ({ commit, state }, current) {
const index = state.media.indexOf(current)
- commit('setCurrent', index || 0)
+ commit('setCurrentMedia', index || 0)
},
closeMediaViewer ({ commit }) {
commit('close')
diff --git a/src/modules/oauth.js b/src/modules/oauth.js
index a2a83450..038bc3f3 100644
--- a/src/modules/oauth.js
+++ b/src/modules/oauth.js
@@ -1,5 +1,3 @@
-import { delete as del } from 'vue'
-
const oauth = {
state: {
clientId: false,
@@ -29,7 +27,7 @@ const oauth = {
state.userToken = false
// state.token is userToken with older name, coming from persistent state
// let's clear it as well, since it is being used as a fallback of state.userToken
- del(state, 'token')
+ delete state.token
}
},
getters: {
diff --git a/src/modules/polls.js b/src/modules/polls.js
index 92b89a06..1c4f98a4 100644
--- a/src/modules/polls.js
+++ b/src/modules/polls.js
@@ -1,5 +1,4 @@
import { merge } from 'lodash'
-import { set } from 'vue'
const polls = {
state: {
@@ -13,25 +12,25 @@ const polls = {
// Make expired-state change trigger re-renders properly
poll.expired = Date.now() > Date.parse(poll.expires_at)
if (existingPoll) {
- set(state.pollsObject, poll.id, merge(existingPoll, poll))
+ state.pollsObject[poll.id] = merge(existingPoll, poll)
} else {
- set(state.pollsObject, poll.id, poll)
+ state.pollsObject[poll.id] = poll
}
},
trackPoll (state, pollId) {
const currentValue = state.trackedPolls[pollId]
if (currentValue) {
- set(state.trackedPolls, pollId, currentValue + 1)
+ state.trackedPolls[pollId] = currentValue + 1
} else {
- set(state.trackedPolls, pollId, 1)
+ state.trackedPolls[pollId] = 1
}
},
untrackPoll (state, pollId) {
const currentValue = state.trackedPolls[pollId]
if (currentValue) {
- set(state.trackedPolls, pollId, currentValue - 1)
+ state.trackedPolls[pollId] = currentValue - 1
} else {
- set(state.trackedPolls, pollId, 0)
+ state.trackedPolls[pollId] = 0
}
}
},
diff --git a/src/modules/reports.js b/src/modules/reports.js
index fea83e5f..925792c0 100644
--- a/src/modules/reports.js
+++ b/src/modules/reports.js
@@ -2,20 +2,29 @@ import filter from 'lodash/filter'
const reports = {
state: {
- userId: null,
- statuses: [],
- preTickedIds: [],
- modalActivated: false
+ reportModal: {
+ userId: null,
+ statuses: [],
+ preTickedIds: [],
+ activated: false
+ },
+ reports: {}
},
mutations: {
openUserReportingModal (state, { userId, statuses, preTickedIds }) {
- state.userId = userId
- state.statuses = statuses
- state.preTickedIds = preTickedIds
- state.modalActivated = true
+ state.reportModal.userId = userId
+ state.reportModal.statuses = statuses
+ state.reportModal.preTickedIds = preTickedIds
+ state.reportModal.activated = true
},
closeUserReportingModal (state) {
- state.modalActivated = false
+ state.reportModal.activated = false
+ },
+ setReportState (reportsState, { id, state }) {
+ reportsState.reports[id].state = state
+ },
+ addReport (state, report) {
+ state.reports[report.id] = report
}
},
actions: {
@@ -31,6 +40,23 @@ const reports = {
},
closeUserReportingModal ({ commit }) {
commit('closeUserReportingModal')
+ },
+ setReportState ({ commit, dispatch, rootState }, { id, state }) {
+ const oldState = rootState.reports.reports[id].state
+ commit('setReportState', { id, state })
+ rootState.api.backendInteractor.setReportState({ id, state }).catch(e => {
+ console.error('Failed to set report state', e)
+ dispatch('pushGlobalNotice', {
+ level: 'error',
+ messageKey: 'general.generic_error_message',
+ messageArgs: [e.message],
+ timeout: 5000
+ })
+ commit('setReportState', { id, state: oldState })
+ })
+ },
+ addReport ({ commit }, report) {
+ commit('addReport', report)
}
}
}
diff --git a/src/modules/serverSideConfig.js b/src/modules/serverSideConfig.js
new file mode 100644
index 00000000..476263bc
--- /dev/null
+++ b/src/modules/serverSideConfig.js
@@ -0,0 +1,140 @@
+import { get, set } from 'lodash'
+
+const defaultApi = ({ rootState, commit }, { path, value }) => {
+ const params = {}
+ set(params, path, value)
+ return rootState
+ .api
+ .backendInteractor
+ .updateProfile({ params })
+ .then(result => {
+ commit('addNewUsers', [result])
+ commit('setCurrentUser', result)
+ })
+}
+
+const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => {
+ const settings = {}
+ set(settings, path, value)
+ return rootState
+ .api
+ .backendInteractor
+ .updateNotificationSettings({ settings })
+ .then(result => {
+ if (result.status === 'success') {
+ commit('confirmServerSideOption', { name, value })
+ } else {
+ commit('confirmServerSideOption', { name, value: oldValue })
+ }
+ })
+}
+
+/**
+ * Map that stores relation between path for reading (from user profile),
+ * for writing (into API) an what API to use.
+ *
+ * Shorthand - instead of { get, set, api? } object it's possible to use string
+ * in case default api is used and get = set
+ *
+ * If no api is specified, defaultApi is used (see above)
+ */
+export const settingsMap = {
+ defaultScope: 'source.privacy',
+ defaultNSFW: 'source.sensitive', // BROKEN: pleroma/pleroma#2837
+ stripRichContent: {
+ get: 'source.pleroma.no_rich_text',
+ set: 'no_rich_text'
+ },
+ // Privacy
+ locked: 'locked',
+ acceptChatMessages: {
+ get: 'pleroma.accepts_chat_messages',
+ set: 'accepts_chat_messages'
+ },
+ allowFollowingMove: {
+ get: 'pleroma.allow_following_move',
+ set: 'allow_following_move'
+ },
+ discoverable: {
+ get: 'source.pleroma.discoverable',
+ set: 'discoverable'
+ },
+ hideFavorites: {
+ get: 'pleroma.hide_favorites',
+ set: 'hide_favorites'
+ },
+ hideFollowers: {
+ get: 'pleroma.hide_followers',
+ set: 'hide_followers'
+ },
+ hideFollows: {
+ get: 'pleroma.hide_follows',
+ set: 'hide_follows'
+ },
+ hideFollowersCount: {
+ get: 'pleroma.hide_followers_count',
+ set: 'hide_followers_count'
+ },
+ hideFollowsCount: {
+ get: 'pleroma.hide_follows_count',
+ set: 'hide_follows_count'
+ },
+ // NotificationSettingsAPIs
+ webPushHideContents: {
+ get: 'pleroma.notification_settings.hide_notification_contents',
+ set: 'hide_notification_contents',
+ api: notificationsApi
+ },
+ blockNotificationsFromStrangers: {
+ get: 'pleroma.notification_settings.block_from_strangers',
+ set: 'block_from_strangers',
+ api: notificationsApi
+ }
+}
+
+export const defaultState = Object.fromEntries(Object.keys(settingsMap).map(key => [key, null]))
+
+const serverSideConfig = {
+ state: { ...defaultState },
+ mutations: {
+ confirmServerSideOption (state, { name, value }) {
+ set(state, name, value)
+ },
+ wipeServerSideOption (state, { name }) {
+ set(state, name, null)
+ },
+ wipeAllServerSideOptions (state) {
+ Object.keys(settingsMap).forEach(key => {
+ set(state, key, null)
+ })
+ },
+ // Set the settings based on their path location
+ setCurrentUser (state, user) {
+ Object.entries(settingsMap).forEach((map) => {
+ const [name, value] = map
+ const { get: path = value } = value
+ set(state, name, get(user._original, path))
+ })
+ }
+ },
+ actions: {
+ setServerSideOption ({ rootState, state, commit, dispatch }, { name, value }) {
+ const oldValue = get(state, name)
+ const map = settingsMap[name]
+ if (!map) throw new Error('Invalid server-side setting')
+ const { set: path = map, api = defaultApi } = map
+ commit('wipeServerSideOption', { name })
+
+ api({ rootState, commit }, { path, value, oldValue })
+ .catch((e) => {
+ console.warn('Error setting server-side option:', e)
+ commit('confirmServerSideOption', { name, value: oldValue })
+ })
+ },
+ logout ({ commit }) {
+ commit('wipeAllServerSideOptions')
+ }
+ }
+}
+
+export default serverSideConfig
diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js
new file mode 100644
index 00000000..56164be7
--- /dev/null
+++ b/src/modules/serverSideStorage.js
@@ -0,0 +1,427 @@
+import { toRaw } from 'vue'
+import { isEqual, cloneDeep, set, get, clamp, flatten, groupBy, findLastIndex, takeRight } from 'lodash'
+import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
+
+export const VERSION = 1
+export const NEW_USER_DATE = new Date('2022-08-04') // date of writing this, basically
+
+export const COMMAND_TRIM_FLAGS = 1000
+export const COMMAND_TRIM_FLAGS_AND_RESET = 1001
+
+export const defaultState = {
+ // do we need to update data on server?
+ dirty: false,
+ // storage of flags - stuff that can only be set and incremented
+ flagStorage: {
+ updateCounter: 0, // Counter for most recent update notification seen
+ reset: 0 // special flag that can be used to force-reset all flags, debug purposes only
+ // special reset codes:
+ // 1000: trim keys to those known by currently running FE
+ // 1001: same as above + reset everything to 0
+ },
+ prefsStorage: {
+ _journal: [],
+ simple: {
+ dontShowUpdateNotifs: false,
+ collapseNav: false
+ },
+ collections: {
+ pinnedNavItems: ['home', 'dms', 'chats']
+ }
+ },
+ // raw data
+ raw: null,
+ // local cache
+ cache: null
+}
+
+export const newUserFlags = {
+ ...defaultState.flagStorage,
+ updateCounter: CURRENT_UPDATE_COUNTER // new users don't need to see update notification
+}
+
+export const _moveItemInArray = (array, value, movement) => {
+ const oldIndex = array.indexOf(value)
+ const newIndex = oldIndex + movement
+ const newArray = [...array]
+ // remove old
+ newArray.splice(oldIndex, 1)
+ // add new
+ newArray.splice(clamp(newIndex, 0, newArray.length + 1), 0, value)
+ return newArray
+}
+
+const _wrapData = (data, userName) => ({
+ ...data,
+ _user: userName,
+ _timestamp: Date.now(),
+ _version: VERSION
+})
+
+const _checkValidity = (data) => data._timestamp > 0 && data._version > 0
+
+const _verifyPrefs = (state) => {
+ state.prefsStorage = state.prefsStorage || {
+ simple: {},
+ collections: {}
+ }
+ Object.entries(defaultState.prefsStorage.simple).forEach(([k, v]) => {
+ if (typeof v === 'number' || typeof v === 'boolean') return
+ console.warn(`Preference simple.${k} as invalid type, reinitializing`)
+ set(state.prefsStorage.simple, k, defaultState.prefsStorage.simple[k])
+ })
+ Object.entries(defaultState.prefsStorage.collections).forEach(([k, v]) => {
+ if (Array.isArray(v)) return
+ console.warn(`Preference collections.${k} as invalid type, reinitializing`)
+ set(state.prefsStorage.collections, k, defaultState.prefsStorage.collections[k])
+ })
+}
+
+export const _getRecentData = (cache, live) => {
+ const result = { recent: null, stale: null, needUpload: false }
+ const cacheValid = _checkValidity(cache || {})
+ const liveValid = _checkValidity(live || {})
+ if (!liveValid && cacheValid) {
+ result.needUpload = true
+ console.debug('Nothing valid stored on server, assuming cache to be source of truth')
+ result.recent = cache
+ result.stale = live
+ } else if (!cacheValid && liveValid) {
+ console.debug('Valid storage on server found, no local cache found, using live as source of truth')
+ result.recent = live
+ result.stale = cache
+ } else if (cacheValid && liveValid) {
+ console.debug('Both sources have valid data, figuring things out...')
+ if (live._timestamp === cache._timestamp && live._version === cache._version) {
+ console.debug('Same version/timestamp on both source, source of truth irrelevant')
+ result.recent = cache
+ result.stale = live
+ } else {
+ console.debug('Different timestamp, figuring out which one is more recent')
+ if (live._timestamp < cache._timestamp) {
+ result.recent = cache
+ result.stale = live
+ } else {
+ result.recent = live
+ result.stale = cache
+ }
+ }
+ } else {
+ console.debug('Both sources are invalid, start from scratch')
+ result.needUpload = true
+ }
+ return result
+}
+
+export const _getAllFlags = (recent, stale) => {
+ return Array.from(new Set([
+ ...Object.keys(toRaw((recent || {}).flagStorage || {})),
+ ...Object.keys(toRaw((stale || {}).flagStorage || {}))
+ ]))
+}
+
+export const _mergeFlags = (recent, stale, allFlagKeys) => {
+ if (!stale.flagStorage) return recent.flagStorage
+ if (!recent.flagStorage) return stale.flagStorage
+ return Object.fromEntries(allFlagKeys.map(flag => {
+ const recentFlag = recent.flagStorage[flag]
+ const staleFlag = stale.flagStorage[flag]
+ // use flag that is of higher value
+ return [flag, Number((recentFlag > staleFlag ? recentFlag : staleFlag) || 0)]
+ }))
+}
+
+const _mergeJournal = (...journals) => {
+ // Ignore invalid journal entries
+ const allJournals = flatten(
+ journals.map(j => Array.isArray(j) ? j : [])
+ ).filter(entry =>
+ Object.prototype.hasOwnProperty.call(entry, 'path') &&
+ Object.prototype.hasOwnProperty.call(entry, 'operation') &&
+ Object.prototype.hasOwnProperty.call(entry, 'args') &&
+ Object.prototype.hasOwnProperty.call(entry, 'timestamp')
+ )
+ const grouped = groupBy(allJournals, 'path')
+ const trimmedGrouped = Object.entries(grouped).map(([path, journal]) => {
+ // side effect
+ journal.sort((a, b) => a.timestamp > b.timestamp ? 1 : -1)
+
+ if (path.startsWith('collections')) {
+ const lastRemoveIndex = findLastIndex(journal, ({ operation }) => operation === 'removeFromCollection')
+ // everything before last remove is unimportant
+ if (lastRemoveIndex > 0) {
+ return journal.slice(lastRemoveIndex)
+ } else {
+ // everything else doesn't need trimming
+ return journal
+ }
+ } else if (path.startsWith('simple')) {
+ // Only the last record is important
+ return takeRight(journal)
+ } else {
+ return journal
+ }
+ })
+ return flatten(trimmedGrouped)
+ .sort((a, b) => a.timestamp > b.timestamp ? 1 : -1)
+}
+
+export const _mergePrefs = (recent, stale, allFlagKeys) => {
+ if (!stale) return recent
+ if (!recent) return stale
+ const { _journal: recentJournal, ...recentData } = recent
+ const { _journal: staleJournal } = stale
+ /** Journal entry format:
+ * path: path to entry in prefsStorage
+ * timestamp: timestamp of the change
+ * operation: operation type
+ * arguments: array of arguments, depends on operation type
+ *
+ * currently only supported operation type is "set" which just sets the value
+ * to requested one. Intended only to be used with simple preferences (boolean, number)
+ * shouldn't be used with collections!
+ */
+ const resultOutput = { ...recentData }
+ const totalJournal = _mergeJournal(staleJournal, recentJournal)
+ totalJournal.forEach(({ path, timestamp, operation, command, args }) => {
+ if (path.startsWith('_')) {
+ console.error(`journal contains entry to edit internal (starts with _) field '${path}', something is incorrect here, ignoring.`)
+ return
+ }
+ switch (operation) {
+ case 'set':
+ set(resultOutput, path, args[0])
+ break
+ case 'addToCollection':
+ set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).add(args[0])))
+ break
+ case 'removeFromCollection': {
+ const newSet = new Set(get(resultOutput, path))
+ newSet.delete(args[0])
+ set(resultOutput, path, Array.from(newSet))
+ break
+ }
+ case 'reorderCollection': {
+ const [value, movement] = args
+ set(resultOutput, path, _moveItemInArray(get(resultOutput, path), value, movement))
+ break
+ }
+ default:
+ console.error(`Unknown journal operation: '${operation}', did we forget to run reverse migrations beforehand?`)
+ }
+ })
+ return { ...resultOutput, _journal: totalJournal }
+}
+
+export const _resetFlags = (totalFlags, knownKeys = defaultState.flagStorage) => {
+ let result = { ...totalFlags }
+ const allFlagKeys = Object.keys(totalFlags)
+ // flag reset functionality
+ if (totalFlags.reset >= COMMAND_TRIM_FLAGS && totalFlags.reset <= COMMAND_TRIM_FLAGS_AND_RESET) {
+ console.debug('Received command to trim the flags')
+ const knownKeysSet = new Set(Object.keys(knownKeys))
+
+ // Trim
+ result = {}
+ allFlagKeys.forEach(flag => {
+ if (knownKeysSet.has(flag)) {
+ result[flag] = totalFlags[flag]
+ }
+ })
+
+ // Reset
+ if (totalFlags.reset === COMMAND_TRIM_FLAGS_AND_RESET) {
+ // 1001 - and reset everything to 0
+ console.debug('Received command to reset the flags')
+ Object.keys(knownKeys).forEach(flag => { result[flag] = 0 })
+ }
+ } else if (totalFlags.reset > 0 && totalFlags.reset < 9000) {
+ console.debug('Received command to reset the flags')
+ allFlagKeys.forEach(flag => { result[flag] = 0 })
+ }
+ result.reset = 0
+ return result
+}
+
+export const _doMigrations = (cache) => {
+ if (!cache) return cache
+
+ if (cache._version < VERSION) {
+ console.debug('Local cached data has older version, seeing if there any migrations that can be applied')
+
+ // no migrations right now since we only have one version
+ console.debug('No migrations found')
+ }
+
+ if (cache._version > VERSION) {
+ console.debug('Local cached data has newer version, seeing if there any reverse migrations that can be applied')
+
+ // no reverse migrations right now but we leave a possibility of loading a hotpatch if need be
+ if (window._PLEROMA_HOTPATCH) {
+ if (window._PLEROMA_HOTPATCH.reverseMigrations) {
+ console.debug('Found hotpatch migration, applying')
+ return window._PLEROMA_HOTPATCH.reverseMigrations.call({}, 'serverSideStorage', { from: cache._version, to: VERSION }, cache)
+ }
+ }
+ }
+
+ return cache
+}
+
+export const mutations = {
+ clearServerSideStorage (state, userData) {
+ state = { ...cloneDeep(defaultState) }
+ },
+ setServerSideStorage (state, userData) {
+ const live = userData.storage
+ state.raw = live
+ let cache = state.cache
+ if (cache && cache._user !== userData.fqn) {
+ console.warn('cache belongs to another user! reinitializing local cache!')
+ cache = null
+ }
+
+ cache = _doMigrations(cache)
+
+ let { recent, stale, needsUpload } = _getRecentData(cache, live)
+
+ const userNew = userData.created_at > NEW_USER_DATE
+ const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage
+ let dirty = false
+
+ if (recent === null) {
+ console.debug(`Data is empty, initializing for ${userNew ? 'new' : 'existing'} user`)
+ recent = _wrapData({
+ flagStorage: { ...flagsTemplate },
+ prefsStorage: { ...defaultState.prefsStorage }
+ })
+ }
+
+ if (!needsUpload && recent && stale) {
+ console.debug('Checking if data needs merging...')
+ // discarding timestamps and versions
+ const { _timestamp: _0, _version: _1, ...recentData } = recent
+ const { _timestamp: _2, _version: _3, ...staleData } = stale
+ dirty = !isEqual(recentData, staleData)
+ console.debug(`Data ${dirty ? 'needs' : 'doesn\'t need'} merging`)
+ }
+
+ const allFlagKeys = _getAllFlags(recent, stale)
+ let totalFlags
+ let totalPrefs
+ if (dirty) {
+ // Merge the flags
+ console.debug('Merging the data...')
+ totalFlags = _mergeFlags(recent, stale, allFlagKeys)
+ _verifyPrefs(recent)
+ _verifyPrefs(stale)
+ totalPrefs = _mergePrefs(recent.prefsStorage, stale.prefsStorage)
+ } else {
+ totalFlags = recent.flagStorage
+ totalPrefs = recent.prefsStorage
+ }
+
+ totalFlags = _resetFlags(totalFlags)
+
+ recent.flagStorage = { ...flagsTemplate, ...totalFlags }
+ recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
+
+ state.dirty = dirty || needsUpload
+ state.cache = recent
+ // set local timestamp to smaller one if we don't have any changes
+ if (stale && recent && !state.dirty) {
+ state.cache._timestamp = Math.min(stale._timestamp, recent._timestamp)
+ }
+ state.flagStorage = state.cache.flagStorage
+ state.prefsStorage = state.cache.prefsStorage
+ },
+ setFlag (state, { flag, value }) {
+ state.flagStorage[flag] = value
+ state.dirty = true
+ },
+ setPreference (state, { path, value }) {
+ if (path.startsWith('_')) {
+ console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
+ return
+ }
+ set(state.prefsStorage, path, value)
+ state.prefsStorage._journal = [
+ ...state.prefsStorage._journal,
+ { operation: 'set', path, args: [value], timestamp: Date.now() }
+ ]
+ state.dirty = true
+ },
+ addCollectionPreference (state, { path, value }) {
+ if (path.startsWith('_')) {
+ console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
+ return
+ }
+ const collection = new Set(get(state.prefsStorage, path))
+ collection.add(value)
+ set(state.prefsStorage, path, [...collection])
+ state.prefsStorage._journal = [
+ ...state.prefsStorage._journal,
+ { operation: 'addToCollection', path, args: [value], timestamp: Date.now() }
+ ]
+ state.dirty = true
+ },
+ removeCollectionPreference (state, { path, value }) {
+ if (path.startsWith('_')) {
+ console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
+ return
+ }
+ const collection = new Set(get(state.prefsStorage, path))
+ collection.delete(value)
+ set(state.prefsStorage, path, [...collection])
+ state.prefsStorage._journal = [
+ ...state.prefsStorage._journal,
+ { operation: 'removeFromCollection', path, args: [value], timestamp: Date.now() }
+ ]
+ state.dirty = true
+ },
+ reorderCollectionPreference (state, { path, value, movement }) {
+ if (path.startsWith('_')) {
+ console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`)
+ return
+ }
+ const collection = get(state.prefsStorage, path)
+ const newCollection = _moveItemInArray(collection, value, movement)
+ set(state.prefsStorage, path, newCollection)
+ state.prefsStorage._journal = [
+ ...state.prefsStorage._journal,
+ { operation: 'arrangeCollection', path, args: [value], timestamp: Date.now() }
+ ]
+ state.dirty = true
+ },
+ updateCache (state, { username }) {
+ state.prefsStorage._journal = _mergeJournal(state.prefsStorage._journal)
+ state.cache = _wrapData({
+ flagStorage: toRaw(state.flagStorage),
+ prefsStorage: toRaw(state.prefsStorage)
+ }, username)
+ }
+}
+
+const serverSideStorage = {
+ state: {
+ ...cloneDeep(defaultState)
+ },
+ mutations,
+ actions: {
+ pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
+ const needPush = state.dirty || force
+ console.log(needPush)
+ if (!needPush) return
+ commit('updateCache', { username: rootState.users.currentUser.fqn })
+ const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } }
+ rootState.api.backendInteractor
+ .updateProfile({ params })
+ .then((user) => {
+ commit('setServerSideStorage', user)
+ state.dirty = false
+ })
+ }
+ }
+}
+
+export default serverSideStorage
diff --git a/src/modules/shout.js b/src/modules/shout.js
index 507a4d83..88aefbfe 100644
--- a/src/modules/shout.js
+++ b/src/modules/shout.js
@@ -1,7 +1,8 @@
const shout = {
state: {
messages: [],
- channel: { state: '' }
+ channel: { state: '' },
+ joined: false
},
mutations: {
setChannel (state, channel) {
@@ -13,11 +14,23 @@ const shout = {
},
setMessages (state, messages) {
state.messages = messages.slice(-19, 20)
+ },
+ setJoined (state, joined) {
+ state.joined = joined
}
},
actions: {
initializeShout (store, socket) {
const channel = socket.channel('chat:public')
+ channel.joinPush.receive('ok', () => {
+ store.commit('setJoined', true)
+ })
+ channel.onClose(() => {
+ store.commit('setJoined', false)
+ })
+ channel.onError(() => {
+ store.commit('setJoined', false)
+ })
channel.on('new_msg', (msg) => {
store.commit('addMessage', msg)
})
diff --git a/src/modules/statusHistory.js b/src/modules/statusHistory.js
new file mode 100644
index 00000000..db3d6d4b
--- /dev/null
+++ b/src/modules/statusHistory.js
@@ -0,0 +1,25 @@
+const statusHistory = {
+ state: {
+ params: {},
+ modalActivated: false
+ },
+ mutations: {
+ openStatusHistoryModal (state, params) {
+ state.params = params
+ state.modalActivated = true
+ },
+ closeStatusHistoryModal (state) {
+ state.modalActivated = false
+ }
+ },
+ actions: {
+ openStatusHistoryModal ({ commit }, params) {
+ commit('openStatusHistoryModal', params)
+ },
+ closeStatusHistoryModal ({ commit }) {
+ commit('closeStatusHistoryModal')
+ }
+ }
+}
+
+export default statusHistory
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 69f8af3a..5a5c7b1b 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -12,7 +12,6 @@ import {
isArray,
omitBy
} from 'lodash'
-import { set } from 'vue'
import {
isStatusNotification,
isValidNotification,
@@ -63,7 +62,8 @@ export const defaultState = () => ({
friends: emptyTl(),
tag: emptyTl(),
dms: emptyTl(),
- bookmarks: emptyTl()
+ bookmarks: emptyTl(),
+ list: emptyTl()
}
})
@@ -92,7 +92,7 @@ const mergeOrAdd = (arr, obj, item) => {
// This is a new item, prepare it
prepareStatus(item)
arr.push(item)
- set(obj, item.id, item)
+ obj[item.id] = item
return { item, new: true }
}
}
@@ -131,7 +131,7 @@ const addStatusToGlobalStorage = (state, data) => {
if (conversationsObject[conversationId]) {
conversationsObject[conversationId].push(status)
} else {
- set(conversationsObject, conversationId, [status])
+ conversationsObject[conversationId] = [status]
}
}
return result
@@ -246,10 +246,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
}
const processors = {
- 'status': (status) => {
+ status: (status) => {
addStatus(status, showImmediately)
},
- 'retweet': (status) => {
+ edit: (status) => {
+ addStatus(status, showImmediately)
+ },
+ retweet: (status) => {
// RetweetedStatuses are never shown immediately
const retweetedStatus = addStatus(status.retweeted_status, false, false)
@@ -271,7 +274,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
retweet.retweeted_status = retweetedStatus
},
- 'favorite': (favorite) => {
+ favorite: (favorite) => {
// Only update if this is a new favorite.
// Ignore our own favorites because we get info about likes as response to like request
if (!state.favorites.has(favorite.id)) {
@@ -279,7 +282,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
favoriteStatus(favorite)
}
},
- 'deletion': (deletion) => {
+ deletion: (deletion) => {
const uri = deletion.uri
const status = find(allStatuses, { uri })
if (!status) {
@@ -293,10 +296,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
remove(timelineObject.visibleStatuses, { uri })
}
},
- 'follow': (follow) => {
+ follow: (follow) => {
// NOOP, it is known status but we don't do anything about it for now
},
- 'default': (unknown) => {
+ default: (unknown) => {
console.log('unknown status type')
console.log(unknown)
}
@@ -304,7 +307,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
each(statuses, (status) => {
const type = status.type
- const processor = processors[type] || processors['default']
+ const processor = processors[type] || processors.default
processor(status)
})
@@ -337,11 +340,16 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
}
+ if (notification.type === 'pleroma:report') {
+ dispatch('addReport', notification.report)
+ }
+
if (notification.type === 'pleroma:emoji_reaction') {
dispatch('fetchEmojiReactionsBy', notification.status.id)
}
// Only add a new notification if we don't have one for the same action
+ // eslint-disable-next-line no-prototype-builtins
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
updateNotificationsMinMaxId(state, notification)
@@ -523,7 +531,7 @@ export const mutations = {
},
addEmojiReactionsBy (state, { id, emojiReactions, currentUser }) {
const status = state.allStatusesObject[id]
- set(status, 'emoji_reactions', emojiReactions)
+ status.emoji_reactions = emojiReactions
},
addOwnReaction (state, { id, emoji, currentUser }) {
const status = state.allStatusesObject[id]
@@ -542,9 +550,9 @@ export const mutations = {
// Update count of existing reaction if it exists, otherwise append at the end
if (reactionIndex >= 0) {
- set(status.emoji_reactions, reactionIndex, newReaction)
+ status.emoji_reactions[reactionIndex] = newReaction
} else {
- set(status, 'emoji_reactions', [...status.emoji_reactions, newReaction])
+ status.emoji_reactions = [...status.emoji_reactions, newReaction]
}
},
removeOwnReaction (state, { id, emoji, currentUser }) {
@@ -563,9 +571,9 @@ export const mutations = {
}
if (newReaction.count > 0) {
- set(status.emoji_reactions, reactionIndex, newReaction)
+ status.emoji_reactions[reactionIndex] = newReaction
} else {
- set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.name !== emoji))
+ status.emoji_reactions = status.emoji_reactions.filter(r => r.name !== emoji)
}
},
updateStatusWithPoll (state, { id, poll }) {
@@ -601,6 +609,12 @@ const statuses = {
return rootState.api.backendInteractor.fetchStatus({ id })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
+ fetchStatusSource ({ rootState, dispatch }, status) {
+ return apiService.fetchStatusSource({ id: status.id, credentials: rootState.users.currentUser.credentials })
+ },
+ fetchStatusHistory ({ rootState, dispatch }, status) {
+ return apiService.fetchStatusHistory({ status })
+ },
deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status })
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
diff --git a/src/modules/users.js b/src/modules/users.js
index 2b416f94..eef87c2c 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,7 +1,7 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
+import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import oauthApi from '../services/new_api/oauth.js'
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
-import { set } from 'vue'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
// TODO: Unify with mergeOrAdd in statuses.js
@@ -15,10 +15,7 @@ export const mergeOrAdd = (arr, obj, item) => {
} else {
// This is a new item, prepare it
arr.push(item)
- set(obj, item.id, item)
- if (item.screen_name && !item.screen_name.includes('@')) {
- set(obj, item.screen_name.toLowerCase(), item)
- }
+ obj[item.id] = item
return { item, new: true }
}
}
@@ -54,6 +51,11 @@ const unblockUser = (store, id) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
+const removeUserFromFollowers = (store, id) => {
+ return store.rootState.api.backendInteractor.removeUserFromFollowers({ id })
+ .then((relationship) => store.commit('updateUserRelationship', [relationship]))
+}
+
const muteUser = (store, id) => {
const predictedRelationship = store.state.relationships[id] || { id }
predictedRelationship.muting = true
@@ -103,23 +105,23 @@ export const mutations = {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.concat([tag])
- set(user, 'tags', newTags)
+ user.tags = newTags
},
untagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.filter(t => t !== tag)
- set(user, 'tags', newTags)
+ user.tags = newTags
},
updateRight (state, { user: { id }, right, value }) {
const user = state.usersObject[id]
- let newRights = user.rights
+ const newRights = user.rights
newRights[right] = value
- set(user, 'rights', newRights)
+ user.rights = newRights
},
updateActivationStatus (state, { user: { id }, deactivated }) {
const user = state.usersObject[id]
- set(user, 'deactivated', deactivated)
+ user.deactivated = deactivated
},
setCurrentUser (state, user) {
state.lastLoginName = user.screen_name
@@ -148,28 +150,35 @@ export const mutations = {
clearFriends (state, userId) {
const user = state.usersObject[userId]
if (user) {
- set(user, 'friendIds', [])
+ user.friendIds = []
}
},
clearFollowers (state, userId) {
const user = state.usersObject[userId]
if (user) {
- set(user, 'followerIds', [])
+ user.followerIds = []
}
},
addNewUsers (state, users) {
each(users, (user) => {
if (user.relationship) {
- set(state.relationships, user.relationship.id, user.relationship)
+ state.relationships[user.relationship.id] = user.relationship
+ }
+ const res = mergeOrAdd(state.users, state.usersObject, user)
+ const item = res.item
+ if (res.new && item.screen_name && !item.screen_name.includes('@')) {
+ state.usersByNameObject[item.screen_name.toLowerCase()] = item
}
- mergeOrAdd(state.users, state.usersObject, user)
})
},
updateUserRelationship (state, relationships) {
relationships.forEach((relationship) => {
- set(state.relationships, relationship.id, relationship)
+ state.relationships[relationship.id] = relationship
})
},
+ updateUserInLists (state, { id, inLists }) {
+ state.usersObject[id].inLists = inLists
+ },
saveBlockIds (state, blockIds) {
state.currentUser.blockIds = blockIds
},
@@ -222,7 +231,7 @@ export const mutations = {
},
setColor (state, { user: { id }, highlighted }) {
const user = state.usersObject[id]
- set(user, 'highlight', highlighted)
+ user.highlight = highlighted
},
signUpPending (state) {
state.signUpPending = true
@@ -239,12 +248,15 @@ export const mutations = {
export const getters = {
findUser: state => query => {
- const result = state.usersObject[query]
- // In case it's a screen_name, we can try searching case-insensitive
- if (!result && typeof query === 'string') {
- return state.usersObject[query.toLowerCase()]
- }
- return result
+ return state.usersObject[query]
+ },
+ findUserByName: state => query => {
+ return state.usersByNameObject[query.toLowerCase()]
+ },
+ findUserByUrl: state => query => {
+ return state.users
+ .find(u => u.statusnet_profile_url &&
+ u.statusnet_profile_url.toLowerCase() === query.toLowerCase())
},
relationship: state => id => {
const rel = id && state.relationships[id]
@@ -258,6 +270,7 @@ export const defaultState = {
currentUser: false,
users: [],
usersObject: {},
+ usersByNameObject: {},
signUpPending: false,
signUpErrors: [],
relationships: {}
@@ -280,12 +293,25 @@ const users = {
return user
})
},
+ fetchUserByName (store, name) {
+ return store.rootState.api.backendInteractor.fetchUserByName({ name })
+ .then((user) => {
+ store.commit('addNewUsers', [user])
+ return user
+ })
+ },
fetchUserRelationship (store, id) {
if (store.state.currentUser) {
store.rootState.api.backendInteractor.fetchUserRelationship({ id })
.then((relationships) => store.commit('updateUserRelationship', relationships))
}
},
+ fetchUserInLists (store, id) {
+ if (store.state.currentUser) {
+ store.rootState.api.backendInteractor.fetchUserInLists({ id })
+ .then((inLists) => store.commit('updateUserInLists', { id, inLists }))
+ }
+ },
fetchBlocks (store) {
return store.rootState.api.backendInteractor.fetchBlocks()
.then((blocks) => {
@@ -300,6 +326,9 @@ const users = {
unblockUser (store, id) {
return unblockUser(store, id)
},
+ removeUserFromFollowers (store, id) {
+ return removeUserFromFollowers(store, id)
+ },
blockUsers (store, ids = []) {
return Promise.all(ids.map(id => blockUser(store, id)))
},
@@ -388,7 +417,7 @@ const users = {
toggleActivationStatus ({ rootState, commit }, { user }) {
const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser
api({ user })
- .then(({ deactivated }) => commit('updateActivationStatus', { user, deactivated }))
+ .then((user) => { const deactivated = !user.is_active; commit('updateActivationStatus', { user, deactivated }) })
},
registerPushNotifications (store) {
const token = store.state.currentUser.credentials
@@ -452,17 +481,17 @@ const users = {
async signUp (store, userInfo) {
store.commit('signUpPending')
- let rootState = store.rootState
+ const rootState = store.rootState
try {
- let data = await rootState.api.backendInteractor.register(
+ const data = await rootState.api.backendInteractor.register(
{ params: { ...userInfo } }
)
store.commit('signUpSuccess')
store.commit('setToken', data.access_token)
store.dispatch('loginUser', data.access_token)
} catch (e) {
- let errors = e.message
+ const errors = e.message
store.commit('signUpFailure', errors)
throw e
}
@@ -497,11 +526,15 @@ const users = {
store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetchingNotifications')
+ store.dispatch('stopFetchingLists')
store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications')
store.commit('resetStatuses')
store.dispatch('resetChats')
store.dispatch('setLastTimeline', 'public-timeline')
+ store.dispatch('setLayoutWidth', windowWidth())
+ store.dispatch('setLayoutHeight', windowHeight())
+ store.commit('clearServerSideStorage')
})
},
loginUser (store, accessToken) {
@@ -518,6 +551,7 @@ const users = {
user.muteIds = []
user.domainMutes = []
commit('setCurrentUser', user)
+ commit('setServerSideStorage', user)
commit('addNewUsers', [user])
store.dispatch('fetchEmoji')
@@ -527,6 +561,7 @@ const users = {
// Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(accessToken))
+ store.dispatch('pushServerSideStorage')
if (user.token) {
store.dispatch('setWsToken', user.token)
@@ -546,6 +581,12 @@ const users = {
store.dispatch('startFetchingChats')
}
+ store.dispatch('startFetchingLists')
+
+ if (user.locked) {
+ store.dispatch('startFetchingFollowRequests')
+ }
+
if (store.getters.mergedConfig.useStreamingApi) {
store.dispatch('fetchTimeline', 'friends', { since: null })
store.dispatch('fetchNotifications', { since: null })
@@ -562,6 +603,9 @@ const users = {
// Get user mutes
store.dispatch('fetchMutes')
+ store.dispatch('setLayoutWidth', windowWidth())
+ store.dispatch('setLayoutHeight', windowHeight())
+
// Fetch our friends
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
.then((friends) => commit('addNewUsers', friends))