Compare commits

..

No commits in common. "7bd01ca1168b9c8dabdfce3fe1cb5e15bcd55f72" and "97f2de913d4dca3f369be07037dbdaab9124898b" have entirely different histories.

23 changed files with 179 additions and 142 deletions

View file

@ -184,10 +184,7 @@ export const fetchStatusHistory = ({ id, credentials }) =>
return { return {
...rest, ...rest,
data: [...data].reverse().map((item) => { data: [...data].reverse().map((item) => {
// History data is missing a lot of stuff present in original item.originalStatus = status
// but we're really only missing the id for the timeago, the
// rest seem to render just fine.
item.id = id
return parseStatus(item) return parseStatus(item)
}), }),
} }

View file

@ -127,7 +127,6 @@ const Chat = {
if (this.testMode) return if (this.testMode) return
this.deactivate() this.deactivate()
this.detachSocket()
}, },
computed: { computed: {
conversationId() { conversationId() {
@ -457,16 +456,10 @@ const Chat = {
// Sanity check // Sanity check
if (!this.isConversation && message.chat_id !== this.chat.id) { 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( console.warn(
`Chat message doesn't belong to current chat (id: ${this.chat.id})!!`, `Chat message doesn't belong to current chat (id: ${this.chat.id})!!`,
message, message,
) )
*/
return return
} }

View file

@ -21,8 +21,8 @@ const List = {
default: () => '', default: () => '',
}, },
preSelect: { preSelect: {
type: Set, type: Array,
default: new Set(), default: [],
}, },
nonInteractive: { nonInteractive: {
type: Boolean, type: Boolean,
@ -48,7 +48,7 @@ const List = {
data() { data() {
return { return {
items: [], items: [],
selected: new Set(this.preSelect), // clone selected: new Set(this.preSelect),
loading: false, loading: false,
bottomedOut: true, bottomedOut: true,
error: null, error: null,

View file

@ -141,7 +141,6 @@ const Status = {
return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId) return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId)
}, },
repeatedStatus() { repeatedStatus() {
if (this.status.retweeted_status === undefined) return undefined
return useStatusesStore().allStatuses.get(this.status.retweeted_status.id) return useStatusesStore().allStatuses.get(this.status.retweeted_status.id)
}, },
repeater() { repeater() {
@ -199,7 +198,7 @@ const Status = {
} }
}, },
isRepeat() { isRepeat() {
return !!this.repeatedStatus return !!this.status.retweeted_status
}, },
repeaterName() { repeaterName() {
return this.status.user.name || this.status.user.screen_name_ui 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 return !this.unmuted && !this.shouldNotMute && this.muteReasons.length > 0
}, },
userIsMuted() { userIsMuted() {
if (!this.currentUser) return false if (this.status.user.id === this.currentUser?.id) return false
if (this.user === this.currentUser) return false const { reblog } = this.status
if (this.repeater === this.currentUser) return false const relationship = useUsersStore().relationship(this.status.user.id)
const relationship = useUsersStore().relationship(this.user.id) const relationshipReblog =
const relationshipRepeat = useUsersStore().relationship(this.repeater?.id) reblog && useUsersStore().relationship(reblog.user.id)
return ( return (
(this.status.muted && !this.status.thread_muted) || (status.muted && !status.thread_muted) ||
// Reprööt of a muted post according to BE // Reprööt of a muted post according to BE
(this.repeatedStatus?.muted && !this.repeatedStatus.thread_muted) || (reblog?.muted && !reblog.thread_muted) ||
// Muted user // Muted user
relationship.muting || relationship.muting ||
// Muted user of a reprööt // Muted user of a reprööt
relationshipRepeat?.muting relationshipReblog?.muting
) )
}, },
shouldNotMute() { shouldNotMute() {

View file

@ -174,7 +174,20 @@ export const BUTTONS = [
) )
}, },
action({ status }) { 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() return Promise.resolve()
}, },
}, },

View file

@ -87,6 +87,7 @@
v-if="favoritesTabVisible" v-if="favoritesTabVisible"
key="favorites" key="favorites"
:label="$t('user_card.favorites')" :label="$t('user_card.favorites')"
:disabled="favorites.visibleStatusIds.size === 0"
:title="$t('user_card.favorites')" :title="$t('user_card.favorites')"
:timeline-ref="{ name: 'favorites', argument: userId }" :timeline-ref="{ name: 'favorites', argument: userId }"
:argument="isUs ? undefined : userId" :argument="isUs ? undefined : userId"

View file

@ -56,7 +56,7 @@ const UserReportingModal = {
// Reset state // Reset state
this.comment = '' this.comment = ''
this.forward = false this.forward = false
this.statusIdsToReport = new Set(this.reportModal.preTickedIds) // cloning this.statusIdsToReport = new Set(this.reportModal.preTickedIds)
this.processing = false this.processing = false
this.error = false this.error = false
}, },

View file

@ -52,9 +52,8 @@
</div> </div>
<div class="user-reporting-panel-right"> <div class="user-reporting-panel-right">
<List <List
:external-items="reportModal.statusIds" :external-items="reportModal.statuses"
:pre-select="reportModal.preTickedIds" :pre-select="reportModal.preTickedIds"
:get-key="(item) => item"
selectable selectable
@select="onListSelect" @select="onListSelect"
> >
@ -62,7 +61,7 @@
<Status <Status
:in-conversation="false" :in-conversation="false"
:focused="false" :focused="false"
:status-id="item" :statusoid="item"
/> />
</template> </template>
</List> </List>

View file

@ -1,20 +1,58 @@
import { shuffle } from 'lodash' import { shuffle } from 'lodash'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { fetchUser, suggestions } from 'src/api/public.js' import { fetchUser, suggestions } from 'src/api/public.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' 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 = { const WhoToFollowPanel = {
data: () => ({ data: () => ({
usersToFollow: [], usersToFollow: [],
}), }),
computed: { computed: {
user() { user: function () {
return useUsersStore().currentUser.screen_name return useUsersStore().currentUser.screen_name
}, },
suggestionsEnabled() {
return useInstanceCapabilitiesStore().suggestionsEnabled
},
}, },
methods: { methods: {
userProfileLink(id, name) { userProfileLink(id, name) {
@ -24,53 +62,23 @@ const WhoToFollowPanel = {
useInstanceStore().restrictedNicknames, 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: { watch: {
user() { user: function () {
this.getWhoToFollow() if (this.suggestionsEnabled) {
getWhoToFollow()
}
}, },
}, },
mounted() { mounted: function () {
this.usersToFollow = new Array(3).fill().map(() => ({ this.usersToFollow = new Array(3).fill().map(() => ({
img: useInstanceStore().instanceIdentity.defaultAvatar, img: useInstanceStore().instanceIdentity.defaultAvatar,
name: '', name: '',
id: 0, id: 0,
})) }))
if (this.suggestionsEnabled) {
this.getWhoToFollow() getWhoToFollow()
}
}, },
} }

View file

@ -6,7 +6,7 @@ import { useUsersStore } from 'src/stores/users.js'
export const piniaPushNotificationsPlugin = ({ store }) => { export const piniaPushNotificationsPlugin = ({ store }) => {
const validActions = { const validActions = {
sync_config: new Set(['setPreference']), 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 if (!validActions[store.$id]) return // Not applicable to the store
@ -21,7 +21,7 @@ export const piniaPushNotificationsPlugin = ({ store }) => {
useInterfaceStore().notificationPermission === 'granted' useInterfaceStore().notificationPermission === 'granted'
let permissionPresent = let permissionPresent =
useInterfaceStore().notificationPermission !== undefined useInterfaceStore().notificationPermission !== undefined
let user = useUsersStore().loggedIn let user = !!useUsersStore().currentUser
if (store.$id === 'instance') { if (store.$id === 'instance') {
if (actionName === 'set' && args[0].path === 'vapidPublicKey') { if (actionName === 'set' && args[0].path === 'vapidPublicKey') {

View file

@ -4,7 +4,6 @@ import { useUsersStore } from 'src/stores/users.js'
export const maybeShowChatNotification = (chat) => { export const maybeShowChatNotification = (chat) => {
if (!chat.lastMessage) return if (!chat.lastMessage) return
if (chat.unread === 0) return
if (useUsersStore().currentUser.id === chat.lastMessage.account_id) return if (useUsersStore().currentUser.id === chat.lastMessage.account_id) return
const opts = { const opts = {

View file

@ -342,6 +342,10 @@ export const parseStatus = (data) => {
output.favoritedBy = [] output.favoritedBy = []
output.rebloggedBy = [] output.rebloggedBy = []
if (Object.hasOwn(data, 'originalStatus')) {
Object.assign(output, data.originalStatus)
}
return output return output
} }

View file

@ -430,9 +430,11 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
}) })
resultUserIds.data.forEach((userId) => { resultUserIds.data.forEach((userId) => {
useStatusesStore().wipeUserStatuses(status.user.id) window.vuex.dispatch(
// Users are technically never deleted, just deactivated 'markStatusesAsDeleted',
// so there's no real need to delete them from store. (status) => userId === status.user.id,
)
// TODO when migrated to pinia, also remove user
}) })
return resultUserIds return resultUserIds

View file

@ -1,4 +1,4 @@
import { orderBy, sumBy } from 'lodash' import { find, omitBy, orderBy, sumBy } from 'lodash'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' 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' import { chats } from 'src/api/chats.js'
const emptyChatList = () => ({
data: [],
idStore: {},
})
const defaultState = { const defaultState = {
data: new Map(), chatList: emptyChatList(),
fetcher: null, chatListFetcher: null,
}
const getChatById = (state, id) => {
return find(state.chatList.data, { id })
} }
export const useChatsStore = defineStore('chats', { export const useChatsStore = defineStore('chats', {
state: () => ({ ...defaultState }), state: () => ({ ...defaultState }),
getters: { getters: {
sortedChatList(state) { sortedChatList(state) {
return orderBy([...state.data.values()], ['updated_at'], ['desc']) return orderBy(state.chatList.data, ['updated_at'], ['desc'])
}, },
unreadChatsCount(state) { unreadChatsCount(state) {
return sumBy([...state.data.values()], 'unread') return sumBy(state.chatList.data, 'unread')
}, },
}, },
actions: { actions: {
@ -33,19 +42,16 @@ export const useChatsStore = defineStore('chats', {
et, et,
} }
et.addEventListener('pleroma:chat_update', ({ data: { chatUpdate } }) => { et.addEventListener('pleroma:chat_update', this.updateChat)
this.updateChat(chatUpdate)
})
useStreamingStore().addSubscriber(socket) useStreamingStore().addSubscriber(socket)
}, },
startFetching() { startFetching() {
this.fetcher = () => promiseInterval(() => this.fetchChats(), 5000) const fetcher = () => this.fetchChats()
this.fetcher() this.setChatListFetcher(() => promiseInterval(fetcher, 5000))
}, },
stopFetching() { stopFetching() {
this.fetcher?.stop() this.setChatListFetcher(null)
this.fetcher = null
}, },
async fetchChats() { async fetchChats() {
this.addNewChats( 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() { resetChats() {
this.data = new Map() this.chatList = emptyChatList()
this.stopFetching() this.setChatListFetcher(null)
this.startFetching()
}, },
addNewChats(result) { addNewChats(result) {
useUsersStore().addNewUsers({ useUsersStore().addNewUsers({
@ -65,30 +77,45 @@ export const useChatsStore = defineStore('chats', {
data: result.data.map((k) => k.account).filter(Boolean), data: result.data.map((k) => k.account).filter(Boolean),
}) })
// We do unshift in update so we reverse the chat list here result.data.forEach((updatedChat) => {
result.data.forEach((chat) => this.updateChat(chat)) 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) { readChat(id) {
const chat = this.data.get(id) const chat = getChatById(this, id)
if (chat) { if (chat) {
chat.unread = 0 chat.unread = 0
} else {
console.error(`Chat ${id} not found!`)
} }
}, },
updateChat(updatedChat) { updateChat({ data: { chatUpdate: updatedChat } }) {
const chat = this.data.get(updatedChat.id) const chat = getChatById(this, updatedChat.id)
if (chat) { if (chat) {
chat.lastMessage = updatedChat.lastMessage chat.lastMessage = updatedChat.lastMessage
chat.unread = updatedChat.unread chat.unread = updatedChat.unread
chat.updated_at = updatedChat.updated_at chat.updated_at = updatedChat.updated_at
} else { } 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) { 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,
)
}, },
}, },
}) })

View file

@ -77,7 +77,7 @@ const timelineFetcher = (timeline, argument, credentials) => {
return { statuses, pagination } return { statuses, pagination }
}) })
.catch((error) => { .catch((error) => {
if (error.statusCode === 403 && timeline.name === 'favorites') { if (error.statusCode === 403 && timeline === 'favorites') {
useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false
return return
} }

View file

@ -1,3 +1,4 @@
import { filter } from 'lodash'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
@ -10,23 +11,28 @@ export const useReportsStore = defineStore('reports', {
state: () => ({ state: () => ({
reportModal: { reportModal: {
userId: null, userId: null,
statusIds: new Set(), statuses: [],
preTickedIds: new Set(), preTickedIds: [],
activated: false, activated: false,
}, },
reports: {}, reports: {},
}), }),
actions: { actions: {
openUserReportingModal({ userId, statusIds = [] }) { openUserReportingModal({ userId, statusIds = [] }) {
const preTickedIds = new Set(statusIds) const preTickedStatuses = statusIds.map((id) =>
// There shouldn't be a case where this is undefined useStatusesStore().allStatuses.get(id),
const userAllStatusesIds = useStatusesStore().statusesPerUser.get(userId) )
// Set constructor should take care of duplicated IDs and order, const preTickedIds = statusIds
// later duplicated IDs will be dropped in favor of earlier const statuses = preTickedStatuses.concat(
const sortedIds = new Set([...preTickedIds, ...userAllStatusesIds]) filter(
window.vuex.state.statuses.allStatuses,
(status) =>
status.user.id === userId && !preTickedIds.includes(status.id),
),
)
this.reportModal.userId = userId this.reportModal.userId = userId
this.reportModal.statusIds = sortedIds this.reportModal.statuses = statuses
this.reportModal.preTickedIds = preTickedIds this.reportModal.preTickedIds = preTickedIds
this.reportModal.activated = true this.reportModal.activated = true
}, },

View file

@ -30,7 +30,6 @@ import {
export const defaultState = () => ({ export const defaultState = () => ({
allStatuses: new Map(), allStatuses: new Map(),
statusesPerUser: new Map(),
timestamps: new WeakMap(), timestamps: new WeakMap(),
scrobblesNextFetch: {}, scrobblesNextFetch: {},
conversations: new Map(), conversations: new Map(),
@ -84,12 +83,6 @@ export const useStatusesStore = defineStore('statuses', {
// in case of likes (which are not statuses) it should return null // in case of likes (which are not statuses) it should return null
const addStatus = (data) => { const addStatus = (data) => {
const [status] = this.mergeOrAdd(this.allStatuses, data, timestamp) 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 // Add to conversation
const conversations = this.conversations const conversations = this.conversations
@ -534,19 +527,14 @@ export const useStatusesStore = defineStore('statuses', {
// For when blocking a user // For when blocking a user
wipeUserStatuses(userId) { wipeUserStatuses(userId) {
const removed = this.statusesPerUser.get(userId) const removed = new Set()
removed.forEach((statusId) => { this.allStatuses.forEach((status) => {
const status = this.allStatuses.get(statusId) if (status.user.id === userId) {
this.allStatuses.delete(statusId) this.allStatuses.delete(status.id)
const conversationSet = this.conversations.get(
status.statusnet_conversation_id, removed.add(status.id)
)
conversationSet.delete(statusId)
if (conversationSet.size === 0) {
this.conversations.delete(status.statusnet_conversation_id)
} }
}) })
this.statusesPerUser.delete(userId)
return removed return removed
}, },
}, },

View file

@ -101,7 +101,7 @@ export const useStreamingStore = defineStore('streaming', {
this.subscriptions.get(stream.name).delete(stream.argument) 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)) this.socket.unsubscribe(...this.getSubArgs(stream))
} }
}, },
@ -131,6 +131,7 @@ export const useStreamingStore = defineStore('streaming', {
}, },
getSubArgs(stream) { getSubArgs(stream) {
if (stream === undefined) return []
const argumentKey = ARGUMENT_MAP[stream.name] const argumentKey = ARGUMENT_MAP[stream.name]
const args = argumentKey const args = argumentKey
? { ? {

View file

@ -80,7 +80,7 @@ export const useTimelinesStore = defineStore('timelines', {
if (timeline.persistent && !persistent) return if (timeline.persistent && !persistent) return
if ( if (
timelineName === 'favorites' && timelineName === 'favourites' &&
!useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable !useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable
) { ) {
console.warn("Instance doesn't support public favorites timeline") console.warn("Instance doesn't support public favorites timeline")

View file

@ -561,7 +561,6 @@ export const useUsersStore = defineStore('users', {
const ids = useStatusesStore().wipeUserStatuses(id) const ids = useStatusesStore().wipeUserStatuses(id)
useTimelinesStore().wipeStatuses(ids) useTimelinesStore().wipeStatuses(ids)
useNotificationsStore().wipeStatuses(ids)
}) })
}, },
blockUsers(data = []) { blockUsers(data = []) {

View file

@ -32,6 +32,10 @@ const global = {
$store: { $store: {
state: { state: {
api: {}, api: {},
users: {},
statuses: {
allStatusesObject: {},
},
}, },
}, },
$route: { $route: {

View file

@ -9,6 +9,15 @@ import {
describe('The UserHighlight store', () => { describe('The UserHighlight store', () => {
beforeEach(() => { beforeEach(() => {
setActivePinia(createPinia()) setActivePinia(createPinia())
window.vuex = {
state: {
users: {
currentUser: {
fqn: 'foo@bar.tld',
},
},
},
}
}) })
describe('mutations', () => { describe('mutations', () => {

View file

@ -1124,12 +1124,6 @@ describe('Users store', () => {
}, },
) )
vi.spyOn(useNotificationsStore(), 'wipeStatuses').mockImplementation(
async () => {
/* no-op */
},
)
const store = useUsersStore() const store = useUsersStore()
const { storeAction, apiUrl } = actionKeys(action) const { storeAction, apiUrl } = actionKeys(action)
await store[storeAction](userId) await store[storeAction](userId)
@ -1161,12 +1155,6 @@ describe('Users store', () => {
}, },
) )
vi.spyOn(useNotificationsStore(), 'wipeStatuses').mockImplementation(
async () => {
/* no-op */
},
)
const store = useUsersStore() const store = useUsersStore()
const { storeAction, apiUrl } = actionKeys(action) const { storeAction, apiUrl } = actionKeys(action)
await store[storeAction](userId, 20) await store[storeAction](userId, 20)