diff --git a/src/api/public.js b/src/api/public.js index 839196b9e..ef799566d 100644 --- a/src/api/public.js +++ b/src/api/public.js @@ -184,10 +184,7 @@ export const fetchStatusHistory = ({ id, credentials }) => return { ...rest, data: [...data].reverse().map((item) => { - // History data is missing a lot of stuff present in original - // but we're really only missing the id for the timeago, the - // rest seem to render just fine. - item.id = id + item.originalStatus = status return parseStatus(item) }), } diff --git a/src/components/chat_view/chat_view.js b/src/components/chat_view/chat_view.js index 75ab21dd5..29b6c51a3 100644 --- a/src/components/chat_view/chat_view.js +++ b/src/components/chat_view/chat_view.js @@ -127,7 +127,6 @@ const Chat = { if (this.testMode) return this.deactivate() - this.detachSocket() }, computed: { conversationId() { @@ -457,16 +456,10 @@ const Chat = { // Sanity check if (!this.isConversation && message.chat_id !== this.chat.id) { - // This is spammy, we get chat updates from a global chat update - // handler, which naturally receives updates for ALL chats. - // There is no way to subscribe to specific chat updates and listen - // to that in the API. - /* console.warn( `Chat message doesn't belong to current chat (id: ${this.chat.id})!!`, message, ) - */ return } diff --git a/src/components/list/list.js b/src/components/list/list.js index 8abf02fd6..56d3e2ec6 100644 --- a/src/components/list/list.js +++ b/src/components/list/list.js @@ -21,8 +21,8 @@ const List = { default: () => '', }, preSelect: { - type: Set, - default: new Set(), + type: Array, + default: [], }, nonInteractive: { type: Boolean, @@ -48,7 +48,7 @@ const List = { data() { return { items: [], - selected: new Set(this.preSelect), // clone + selected: new Set(this.preSelect), loading: false, bottomedOut: true, error: null, diff --git a/src/components/status/status.js b/src/components/status/status.js index a53812da1..387146db1 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -141,7 +141,6 @@ const Status = { return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId) }, repeatedStatus() { - if (this.status.retweeted_status === undefined) return undefined return useStatusesStore().allStatuses.get(this.status.retweeted_status.id) }, repeater() { @@ -199,7 +198,7 @@ const Status = { } }, isRepeat() { - return !!this.repeatedStatus + return !!this.status.retweeted_status }, repeaterName() { return this.status.user.name || this.status.user.screen_name_ui @@ -314,19 +313,19 @@ const Status = { return !this.unmuted && !this.shouldNotMute && this.muteReasons.length > 0 }, userIsMuted() { - if (!this.currentUser) return false - if (this.user === this.currentUser) return false - if (this.repeater === this.currentUser) return false - const relationship = useUsersStore().relationship(this.user.id) - const relationshipRepeat = useUsersStore().relationship(this.repeater?.id) + if (this.status.user.id === this.currentUser?.id) return false + const { reblog } = this.status + const relationship = useUsersStore().relationship(this.status.user.id) + const relationshipReblog = + reblog && useUsersStore().relationship(reblog.user.id) return ( - (this.status.muted && !this.status.thread_muted) || + (status.muted && !status.thread_muted) || // Reprööt of a muted post according to BE - (this.repeatedStatus?.muted && !this.repeatedStatus.thread_muted) || + (reblog?.muted && !reblog.thread_muted) || // Muted user relationship.muting || // Muted user of a reprööt - relationshipRepeat?.muting + relationshipReblog?.muting ) }, shouldNotMute() { diff --git a/src/components/status_action_buttons/buttons_definitions.js b/src/components/status_action_buttons/buttons_definitions.js index 762b5be6a..399fad395 100644 --- a/src/components/status_action_buttons/buttons_definitions.js +++ b/src/components/status_action_buttons/buttons_definitions.js @@ -174,7 +174,20 @@ export const BUTTONS = [ ) }, action({ status }) { - useStatusHistoryStore().openModal(status.id) + const originalStatus = { ...status } + const stripFieldsList = [ + 'attachments', + 'created_at', + 'emojis', + 'text', + 'raw_html', + 'nsfw', + 'poll', + 'summary', + 'summary_raw_html', + ] + stripFieldsList.forEach((p) => delete originalStatus[p]) + useStatusHistoryStore().openModal(originalStatus.id) return Promise.resolve() }, }, diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 9a987cd09..f60521ffa 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -87,6 +87,7 @@ v-if="favoritesTabVisible" key="favorites" :label="$t('user_card.favorites')" + :disabled="favorites.visibleStatusIds.size === 0" :title="$t('user_card.favorites')" :timeline-ref="{ name: 'favorites', argument: userId }" :argument="isUs ? undefined : userId" diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js index 1ae3f9b76..232305415 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.js +++ b/src/components/user_reporting_modal/user_reporting_modal.js @@ -56,7 +56,7 @@ const UserReportingModal = { // Reset state this.comment = '' this.forward = false - this.statusIdsToReport = new Set(this.reportModal.preTickedIds) // cloning + this.statusIdsToReport = new Set(this.reportModal.preTickedIds) this.processing = false this.error = false }, diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue index 7e58516c3..a028ebeb6 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.vue +++ b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -52,9 +52,8 @@
@@ -62,7 +61,7 @@ diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.js b/src/components/who_to_follow_panel/who_to_follow_panel.js index 107ec42f7..bd0eaf5e1 100644 --- a/src/components/who_to_follow_panel/who_to_follow_panel.js +++ b/src/components/who_to_follow_panel/who_to_follow_panel.js @@ -1,20 +1,58 @@ import { shuffle } from 'lodash' import { useInstanceStore } from 'src/stores/instance.js' +import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useOAuthStore } from 'src/stores/oauth.js' import { useUsersStore } from 'src/stores/users.js' import { fetchUser, suggestions } from 'src/api/public.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +function showWhoToFollow(panel, reply) { + const shuffled = shuffle(reply) + + panel.usersToFollow.forEach((toFollow, index) => { + const user = shuffled[index] + const img = user.avatar || useInstanceStore().instanceIdentity.defaultAvatar + const name = user.acct + + toFollow.img = img + toFollow.name = name + + fetchUser({ + id: name, + credentials: useOAuthStore().token, + }).then((result) => { + const { data: externalUser } = result + useUsersStore().addNewUsers(result) + toFollow.id = externalUser.id + }) + }) +} + +function getWhoToFollow(panel) { + const credentials = useOAuthStore().token + if (credentials) { + panel.usersToFollow.forEach((toFollow) => { + toFollow.name = 'Loading...' + }) + suggestions({ credentials }).then(({ data: reply }) => { + showWhoToFollow(panel, reply) + }) + } +} + const WhoToFollowPanel = { data: () => ({ usersToFollow: [], }), computed: { - user() { + user: function () { return useUsersStore().currentUser.screen_name }, + suggestionsEnabled() { + return useInstanceCapabilitiesStore().suggestionsEnabled + }, }, methods: { userProfileLink(id, name) { @@ -24,53 +62,23 @@ const WhoToFollowPanel = { useInstanceStore().restrictedNicknames, ) }, - getWhoToFollow() { - this.usersToFollow.forEach((toFollow) => { - toFollow.name = 'Loading...' - }) - - suggestions({ credentials: useOAuthStore().token }).then( - ({ data: reply }) => { - this.showWhoToFollow(reply) - }, - ) - }, - showWhoToFollow(reply) { - const shuffled = shuffle(reply) - - this.usersToFollow.forEach((toFollow, index) => { - const user = shuffled[index] - const img = - user.avatar || useInstanceStore().instanceIdentity.defaultAvatar - const name = user.acct - - toFollow.img = img - toFollow.name = name - - fetchUser({ - id: name, - credentials: useOAuthStore().token, - }).then((result) => { - const { data: externalUser } = result - useUsersStore().addNewUsers(result) - toFollow.id = externalUser.id - }) - }) - }, }, watch: { - user() { - this.getWhoToFollow() + user: function () { + if (this.suggestionsEnabled) { + getWhoToFollow() + } }, }, - mounted() { + mounted: function () { this.usersToFollow = new Array(3).fill().map(() => ({ img: useInstanceStore().instanceIdentity.defaultAvatar, name: '', id: 0, })) - - this.getWhoToFollow() + if (this.suggestionsEnabled) { + getWhoToFollow() + } }, } diff --git a/src/lib/push_notifications_plugin.js b/src/lib/push_notifications_plugin.js index b3f6885d7..951eb2c4b 100644 --- a/src/lib/push_notifications_plugin.js +++ b/src/lib/push_notifications_plugin.js @@ -6,7 +6,7 @@ import { useUsersStore } from 'src/stores/users.js' export const piniaPushNotificationsPlugin = ({ store }) => { const validActions = { sync_config: new Set(['setPreference']), - interface: new Set(['setNotificationPermission', 'onLogin', 'onLogout']), + interface: new Set(['setNotificationPermission', 'setLoginStatus']), } if (!validActions[store.$id]) return // Not applicable to the store @@ -21,7 +21,7 @@ export const piniaPushNotificationsPlugin = ({ store }) => { useInterfaceStore().notificationPermission === 'granted' let permissionPresent = useInterfaceStore().notificationPermission !== undefined - let user = useUsersStore().loggedIn + let user = !!useUsersStore().currentUser if (store.$id === 'instance') { if (actionName === 'set' && args[0].path === 'vapidPublicKey') { diff --git a/src/services/chat_utils/chat_utils.js b/src/services/chat_utils/chat_utils.js index e99958110..a5c0d67cd 100644 --- a/src/services/chat_utils/chat_utils.js +++ b/src/services/chat_utils/chat_utils.js @@ -4,7 +4,6 @@ import { useUsersStore } from 'src/stores/users.js' export const maybeShowChatNotification = (chat) => { if (!chat.lastMessage) return - if (chat.unread === 0) return if (useUsersStore().currentUser.id === chat.lastMessage.account_id) return const opts = { diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 9add0b702..196af90a8 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -342,6 +342,10 @@ export const parseStatus = (data) => { output.favoritedBy = [] output.rebloggedBy = [] + if (Object.hasOwn(data, 'originalStatus')) { + Object.assign(output, data.originalStatus) + } + return output } diff --git a/src/stores/admin_settings.js b/src/stores/admin_settings.js index f3b845f61..70850e5f6 100644 --- a/src/stores/admin_settings.js +++ b/src/stores/admin_settings.js @@ -430,9 +430,11 @@ export const useAdminSettingsStore = defineStore('adminSettings', { }) resultUserIds.data.forEach((userId) => { - useStatusesStore().wipeUserStatuses(status.user.id) - // Users are technically never deleted, just deactivated - // so there's no real need to delete them from store. + window.vuex.dispatch( + 'markStatusesAsDeleted', + (status) => userId === status.user.id, + ) + // TODO when migrated to pinia, also remove user }) return resultUserIds diff --git a/src/stores/chats.js b/src/stores/chats.js index 86ea310ce..2f7d4f0b6 100644 --- a/src/stores/chats.js +++ b/src/stores/chats.js @@ -1,4 +1,4 @@ -import { orderBy, sumBy } from 'lodash' +import { find, omitBy, orderBy, sumBy } from 'lodash' import { defineStore } from 'pinia' import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' @@ -10,19 +10,28 @@ import { useUsersStore } from 'src/stores/users.js' import { chats } from 'src/api/chats.js' +const emptyChatList = () => ({ + data: [], + idStore: {}, +}) + const defaultState = { - data: new Map(), - fetcher: null, + chatList: emptyChatList(), + chatListFetcher: null, +} + +const getChatById = (state, id) => { + return find(state.chatList.data, { id }) } export const useChatsStore = defineStore('chats', { state: () => ({ ...defaultState }), getters: { sortedChatList(state) { - return orderBy([...state.data.values()], ['updated_at'], ['desc']) + return orderBy(state.chatList.data, ['updated_at'], ['desc']) }, unreadChatsCount(state) { - return sumBy([...state.data.values()], 'unread') + return sumBy(state.chatList.data, 'unread') }, }, actions: { @@ -33,19 +42,16 @@ export const useChatsStore = defineStore('chats', { et, } - et.addEventListener('pleroma:chat_update', ({ data: { chatUpdate } }) => { - this.updateChat(chatUpdate) - }) + et.addEventListener('pleroma:chat_update', this.updateChat) useStreamingStore().addSubscriber(socket) }, startFetching() { - this.fetcher = () => promiseInterval(() => this.fetchChats(), 5000) - this.fetcher() + const fetcher = () => this.fetchChats() + this.setChatListFetcher(() => promiseInterval(fetcher, 5000)) }, stopFetching() { - this.fetcher?.stop() - this.fetcher = null + this.setChatListFetcher(null) }, async fetchChats() { this.addNewChats( @@ -54,10 +60,16 @@ export const useChatsStore = defineStore('chats', { }), ) }, + setChatListFetcher(fetcher) { + const prevFetcher = this.chatListFetcher + if (prevFetcher) { + prevFetcher.stop() + } + this.chatListFetcher = fetcher?.() + }, resetChats() { - this.data = new Map() - this.stopFetching() - this.startFetching() + this.chatList = emptyChatList() + this.setChatListFetcher(null) }, addNewChats(result) { useUsersStore().addNewUsers({ @@ -65,30 +77,45 @@ export const useChatsStore = defineStore('chats', { data: result.data.map((k) => k.account).filter(Boolean), }) - // We do unshift in update so we reverse the chat list here - result.data.forEach((chat) => this.updateChat(chat)) + result.data.forEach((updatedChat) => { + const chat = getChatById(this, updatedChat.id) + + if (chat) { + chat.lastMessage = updatedChat.lastMessage + chat.unread = updatedChat.unread + chat.updated_at = updatedChat.updated_at + } else { + this.chatList.data.push(updatedChat) + this.chatList.idStore[updatedChat.id] = updatedChat + } + }) }, readChat(id) { - const chat = this.data.get(id) + const chat = getChatById(this, id) if (chat) { chat.unread = 0 - } else { - console.error(`Chat ${id} not found!`) } }, - updateChat(updatedChat) { - const chat = this.data.get(updatedChat.id) + updateChat({ data: { chatUpdate: updatedChat } }) { + const chat = getChatById(this, updatedChat.id) if (chat) { chat.lastMessage = updatedChat.lastMessage chat.unread = updatedChat.unread chat.updated_at = updatedChat.updated_at } else { - this.data.set(updatedChat.id, updatedChat) + this.chatList.data.unshift(updatedChat) } - maybeShowChatNotification(chat ?? updatedChat) + maybeShowChatNotification(chat) + this.chatList.idStore[updatedChat.id] = updatedChat }, deleteChat(id) { - this.data.delete(id) + this.chats.data = this.chats.data.filter( + (conversation) => conversation.last_status.id !== id, + ) + this.chats.idStore = omitBy( + this.chats.idStore, + (conversation) => conversation.last_status.id === id, + ) }, }, }) diff --git a/src/stores/fetchers/timeline_fetcher.js b/src/stores/fetchers/timeline_fetcher.js index 54250318f..ac2611607 100644 --- a/src/stores/fetchers/timeline_fetcher.js +++ b/src/stores/fetchers/timeline_fetcher.js @@ -77,7 +77,7 @@ const timelineFetcher = (timeline, argument, credentials) => { return { statuses, pagination } }) .catch((error) => { - if (error.statusCode === 403 && timeline.name === 'favorites') { + if (error.statusCode === 403 && timeline === 'favorites') { useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false return } diff --git a/src/stores/reports.js b/src/stores/reports.js index 2d9819442..1c4d89bca 100644 --- a/src/stores/reports.js +++ b/src/stores/reports.js @@ -1,3 +1,4 @@ +import { filter } from 'lodash' import { defineStore } from 'pinia' import { useInterfaceStore } from 'src/stores/interface.js' @@ -10,23 +11,28 @@ export const useReportsStore = defineStore('reports', { state: () => ({ reportModal: { userId: null, - statusIds: new Set(), - preTickedIds: new Set(), + statuses: [], + preTickedIds: [], activated: false, }, reports: {}, }), actions: { openUserReportingModal({ userId, statusIds = [] }) { - const preTickedIds = new Set(statusIds) - // There shouldn't be a case where this is undefined - const userAllStatusesIds = useStatusesStore().statusesPerUser.get(userId) - // Set constructor should take care of duplicated IDs and order, - // later duplicated IDs will be dropped in favor of earlier - const sortedIds = new Set([...preTickedIds, ...userAllStatusesIds]) + const preTickedStatuses = statusIds.map((id) => + useStatusesStore().allStatuses.get(id), + ) + const preTickedIds = statusIds + const statuses = preTickedStatuses.concat( + filter( + window.vuex.state.statuses.allStatuses, + (status) => + status.user.id === userId && !preTickedIds.includes(status.id), + ), + ) this.reportModal.userId = userId - this.reportModal.statusIds = sortedIds + this.reportModal.statuses = statuses this.reportModal.preTickedIds = preTickedIds this.reportModal.activated = true }, diff --git a/src/stores/statuses.js b/src/stores/statuses.js index 8a5938211..11a5b81bc 100644 --- a/src/stores/statuses.js +++ b/src/stores/statuses.js @@ -30,7 +30,6 @@ import { export const defaultState = () => ({ allStatuses: new Map(), - statusesPerUser: new Map(), timestamps: new WeakMap(), scrobblesNextFetch: {}, conversations: new Map(), @@ -84,12 +83,6 @@ export const useStatusesStore = defineStore('statuses', { // in case of likes (which are not statuses) it should return null const addStatus = (data) => { const [status] = this.mergeOrAdd(this.allStatuses, data, timestamp) - let userSet = this.statusesPerUser.get(status.user.id) - if (userSet === undefined) { - userSet = new Set() - this.statusesPerUser.set(status.user.id, userSet) - } - userSet.add(status.id) // Add to conversation const conversations = this.conversations @@ -534,19 +527,14 @@ export const useStatusesStore = defineStore('statuses', { // For when blocking a user wipeUserStatuses(userId) { - const removed = this.statusesPerUser.get(userId) - removed.forEach((statusId) => { - const status = this.allStatuses.get(statusId) - this.allStatuses.delete(statusId) - const conversationSet = this.conversations.get( - status.statusnet_conversation_id, - ) - conversationSet.delete(statusId) - if (conversationSet.size === 0) { - this.conversations.delete(status.statusnet_conversation_id) + const removed = new Set() + this.allStatuses.forEach((status) => { + if (status.user.id === userId) { + this.allStatuses.delete(status.id) + + removed.add(status.id) } }) - this.statusesPerUser.delete(userId) return removed }, }, diff --git a/src/stores/streaming.js b/src/stores/streaming.js index a360731bf..9233124b5 100644 --- a/src/stores/streaming.js +++ b/src/stores/streaming.js @@ -101,7 +101,7 @@ export const useStreamingStore = defineStore('streaming', { this.subscriptions.get(stream.name).delete(stream.argument) } - if (stream && this.state === WSConnectionStatus.JOINED) { + if (this.state === WSConnectionStatus.JOINED) { this.socket.unsubscribe(...this.getSubArgs(stream)) } }, @@ -131,6 +131,7 @@ export const useStreamingStore = defineStore('streaming', { }, getSubArgs(stream) { + if (stream === undefined) return [] const argumentKey = ARGUMENT_MAP[stream.name] const args = argumentKey ? { diff --git a/src/stores/timelines.js b/src/stores/timelines.js index 562bec21d..99864b162 100644 --- a/src/stores/timelines.js +++ b/src/stores/timelines.js @@ -80,7 +80,7 @@ export const useTimelinesStore = defineStore('timelines', { if (timeline.persistent && !persistent) return if ( - timelineName === 'favorites' && + timelineName === 'favourites' && !useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable ) { console.warn("Instance doesn't support public favorites timeline") diff --git a/src/stores/users.js b/src/stores/users.js index f41f75aa2..2a88e9295 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -561,7 +561,6 @@ export const useUsersStore = defineStore('users', { const ids = useStatusesStore().wipeUserStatuses(id) useTimelinesStore().wipeStatuses(ids) - useNotificationsStore().wipeStatuses(ids) }) }, blockUsers(data = []) { diff --git a/test/unit/specs/components/chat_view.spec.js b/test/unit/specs/components/chat_view.spec.js index ba06b6b4c..d03809e38 100644 --- a/test/unit/specs/components/chat_view.spec.js +++ b/test/unit/specs/components/chat_view.spec.js @@ -32,6 +32,10 @@ const global = { $store: { state: { api: {}, + users: {}, + statuses: { + allStatusesObject: {}, + }, }, }, $route: { diff --git a/test/unit/specs/stores/user_highlight.spec.js b/test/unit/specs/stores/user_highlight.spec.js index 865544035..e97f8f382 100644 --- a/test/unit/specs/stores/user_highlight.spec.js +++ b/test/unit/specs/stores/user_highlight.spec.js @@ -9,6 +9,15 @@ import { describe('The UserHighlight store', () => { beforeEach(() => { setActivePinia(createPinia()) + window.vuex = { + state: { + users: { + currentUser: { + fqn: 'foo@bar.tld', + }, + }, + }, + } }) describe('mutations', () => { diff --git a/test/unit/specs/stores/users.spec.js b/test/unit/specs/stores/users.spec.js index 285359c62..7e196565d 100644 --- a/test/unit/specs/stores/users.spec.js +++ b/test/unit/specs/stores/users.spec.js @@ -1124,12 +1124,6 @@ describe('Users store', () => { }, ) - vi.spyOn(useNotificationsStore(), 'wipeStatuses').mockImplementation( - async () => { - /* no-op */ - }, - ) - const store = useUsersStore() const { storeAction, apiUrl } = actionKeys(action) await store[storeAction](userId) @@ -1161,12 +1155,6 @@ describe('Users store', () => { }, ) - vi.spyOn(useNotificationsStore(), 'wipeStatuses').mockImplementation( - async () => { - /* no-op */ - }, - ) - const store = useUsersStore() const { storeAction, apiUrl } = actionKeys(action) await store[storeAction](userId, 20)