Merge pull request 'Users/Statuses/Notifications Pinia migration and refactor' (#3555) from users-statuses-pinia into develop

Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3555
This commit is contained in:
HJ 2026-09-01 16:32:31 +00:00
commit d603aac31b
217 changed files with 7961 additions and 5697 deletions

View file

@ -0,0 +1 @@
More timelines now can utilize WebSocket streaming

View file

@ -0,0 +1 @@
switches between streaming and polling should be more reliable now

View file

@ -0,0 +1,3 @@
Added an indicator next to instance's name showing WebSocket connection status (if enabled).
Timeline no longer show "loading" indicator at the bottom when fetching newer posts.
Added small indicator on top of timeline when new posts are being fetched

View file

@ -21,6 +21,7 @@ import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.j
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useShoutStore } from 'src/stores/shout.js' import { useShoutStore } from 'src/stores/shout.js'
import { useUsersStore } from 'src/stores/users.js'
// Helper to unwrap reactive proxies // Helper to unwrap reactive proxies
window.toValue = (x) => JSON.parse(JSON.stringify(x)) window.toValue = (x) => JSON.parse(JSON.stringify(x))
@ -153,11 +154,8 @@ export default {
...(navbarColumnStretch ? ['-column-stretch'] : []), ...(navbarColumnStretch ? ['-column-stretch'] : []),
] ]
}, },
currentUser() {
return this.$store.state.users.currentUser
},
userBackground() { userBackground() {
return this.currentUser.background_image return this.currentUser?.background_image
}, },
foreignProfileBackground() { foreignProfileBackground() {
return ( return (
@ -246,6 +244,7 @@ export default {
'styleDataUsed', 'styleDataUsed',
'layoutType', 'layoutType',
]), ]),
...mapState(useUsersStore, ['currentUser']),
...mapState(useInstanceStore, ['styleDataUsed']), ...mapState(useInstanceStore, ['styleDataUsed']),
...mapState(useInstanceCapabilitiesStore, [ ...mapState(useInstanceCapabilitiesStore, [
'suggestionsEnabled', 'suggestionsEnabled',

View file

@ -26,7 +26,7 @@
class="column -scrollable" class="column -scrollable"
:class="{ '-show-scrollbar': showScrollbars }" :class="{ '-show-scrollbar': showScrollbars }"
> >
<user-panel /> <UserPanel />
<template v-if="layoutType !== 'mobile'"> <template v-if="layoutType !== 'mobile'">
<NavPanel /> <NavPanel />
<InstanceSpecificPanel v-if="showInstanceSpecificPanel" /> <InstanceSpecificPanel v-if="showInstanceSpecificPanel" />

View file

@ -63,7 +63,7 @@ export const paramsString = (params = {}) => {
} }
export const promisedRequest = async ({ export const promisedRequest = async ({
method, method = 'GET',
url, url,
payload, payload,
formData, formData,
@ -124,7 +124,7 @@ export const promisedRequest = async ({
const { ok, status } = response const { ok, status } = response
if (ok) { if (ok) {
return { response, status, data } return { response, status, data, timestamp: Date.now() }
} else { } else {
throw new StatusCodeError(response.status, data, { url, options }, response) throw new StatusCodeError(response.status, data, { url, options }, response)
} }

View file

@ -13,12 +13,12 @@ const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
const MASTODON_PASSWORD_RESET_URL = ({ email }) => const MASTODON_PASSWORD_RESET_URL = ({ email }) =>
`/auth/password${paramsString({ email })}` `/auth/password${paramsString({ email })}`
const MASTODON_FOLLOWING_URL = ( export const MASTODON_FOLLOWING_URL = (
id, id,
{ minId, maxId, sinceId, limit, withRelationships }, { minId, maxId, sinceId, limit, withRelationships },
) => ) =>
`/api/v1/accounts/${id}/following${paramsString({ minId, maxId, sinceId, limit, withRelationships })}` `/api/v1/accounts/${id}/following${paramsString({ minId, maxId, sinceId, limit, withRelationships })}`
const MASTODON_FOLLOWERS_URL = ( export const MASTODON_FOLLOWERS_URL = (
id, id,
{ minId, maxId, sinceId, limit, withRelationships }, { minId, maxId, sinceId, limit, withRelationships },
) => ) =>
@ -26,15 +26,17 @@ const MASTODON_FOLLOWERS_URL = (
export const MASTODON_STATUS_URL = (id) => `/api/v1/statuses/${id}` export const MASTODON_STATUS_URL = (id) => `/api/v1/statuses/${id}`
const MASTODON_STATUS_CONTEXT_URL = (id) => `/api/v1/statuses/${id}/context` const MASTODON_STATUS_CONTEXT_URL = (id) => `/api/v1/statuses/${id}/context`
const MASTODON_STATUS_SOURCE_URL = (id) => `/api/v1/statuses/${id}/source` export const MASTODON_STATUS_SOURCE_URL = (id) =>
const MASTODON_STATUS_HISTORY_URL = (id) => `/api/v1/statuses/${id}/history` `/api/v1/statuses/${id}/source`
export const MASTODON_STATUS_HISTORY_URL = (id) =>
`/api/v1/statuses/${id}/history`
const MASTODON_USER_URL = '/api/v1/accounts' const MASTODON_USER_URL = '/api/v1/accounts'
const MASTODON_USER_LOOKUP_URL = ({ acct }) => const MASTODON_USER_LOOKUP_URL = ({ acct }) =>
`/api/v1/accounts/lookup${paramsString({ acct })}` `/api/v1/accounts/lookup${paramsString({ acct })}`
const MASTODON_POLL_URL = (id = '') => `/api/v1/polls/${id}` const MASTODON_POLL_URL = (id = '') => `/api/v1/polls/${id}`
const MASTODON_STATUS_FAVORITEDBY_URL = (id) => export const MASTODON_STATUS_FAVORITEDBY_URL = (id) =>
`/api/v1/statuses/${id}/favourited_by` `/api/v1/statuses/${id}/favourited_by`
const MASTODON_STATUS_REBLOGGEDBY_URL = (id) => export const MASTODON_STATUS_REBLOGGEDBY_URL = (id) =>
`/api/v1/statuses/${id}/reblogged_by` `/api/v1/statuses/${id}/reblogged_by`
const MASTODON_SEARCH_2 = ({ const MASTODON_SEARCH_2 = ({
q, q,
@ -51,7 +53,7 @@ const MASTODON_SEARCH_2 = ({
const MASTODON_USER_SEARCH_URL = ({ q, resolve }) => const MASTODON_USER_SEARCH_URL = ({ q, resolve }) =>
`/api/v1/accounts/search${paramsString({ q, resolve })}` `/api/v1/accounts/search${paramsString({ q, resolve })}`
const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers' const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
const PLEROMA_EMOJI_REACTIONS_URL = (id) => export const PLEROMA_EMOJI_REACTIONS_URL = (id) =>
`/api/v1/pleroma/statuses/${id}/reactions` `/api/v1/pleroma/statuses/${id}/reactions`
const PLEROMA_SCROBBLES_URL = (id, { maxId, sinceId, minId, limit, offset }) => const PLEROMA_SCROBBLES_URL = (id, { maxId, sinceId, minId, limit, offset }) =>
`/api/v1/pleroma/accounts/${id}/scrobbles${paramsString({ maxId, sinceId, minId, limit, offset })}` `/api/v1/pleroma/accounts/${id}/scrobbles${paramsString({ maxId, sinceId, minId, limit, offset })}`
@ -174,15 +176,21 @@ export const fetchStatusSource = ({ id, credentials }) =>
credentials, credentials,
}).then(({ data, ...rest }) => ({ ...rest, data: parseSource(data) })) }).then(({ data, ...rest }) => ({ ...rest, data: parseSource(data) }))
export const fetchStatusHistory = ({ status, credentials }) => export const fetchStatusHistory = ({ id, credentials }) =>
promisedRequest({ promisedRequest({
url: MASTODON_STATUS_HISTORY_URL(status.id), url: MASTODON_STATUS_HISTORY_URL(id),
credentials, credentials,
}).then(({ data, ...rest }) => { }).then(({ data, ...rest }) => {
return [...data].reverse().map((item) => { return {
item.originalStatus = status ...rest,
return { ...rest, data: parseStatus(item) } 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
return parseStatus(item)
}),
}
}) })
export const listEmojiPacks = ({ page, pageSize, credentials }) => export const listEmojiPacks = ({ page, pageSize, credentials }) =>

View file

@ -115,6 +115,7 @@ export const fetchTimeline = ({
publicAndExternal: MASTODON_PUBLIC_TIMELINE, publicAndExternal: MASTODON_PUBLIC_TIMELINE,
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL, dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
user: MASTODON_USER_TIMELINE_URL, user: MASTODON_USER_TIMELINE_URL,
userPinned: MASTODON_USER_TIMELINE_URL,
media: MASTODON_USER_TIMELINE_URL, media: MASTODON_USER_TIMELINE_URL,
list: MASTODON_LIST_TIMELINE_URL, list: MASTODON_LIST_TIMELINE_URL,
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
@ -130,6 +131,7 @@ export const fetchTimeline = ({
const twoArgs = new Set([ const twoArgs = new Set([
'user', 'user',
'userPinned',
'media', 'media',
'list', 'list',
'publicFavorites', 'publicFavorites',
@ -147,6 +149,7 @@ export const fetchTimeline = ({
const id = (() => { const id = (() => {
switch (timeline) { switch (timeline) {
case 'user': case 'user':
case 'userPinned':
case 'media': case 'media':
return userId return userId
case 'list': case 'list':
@ -163,6 +166,9 @@ export const fetchTimeline = ({
if (timeline === 'media') { if (timeline === 'media') {
params.onlyMedia = true params.onlyMedia = true
} }
if (timeline === 'userPinned') {
params.pinned = true
}
if (timeline === 'public') { if (timeline === 'public') {
params.local = true params.local = true
} }

View file

@ -18,7 +18,7 @@ const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account' const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
const ALIASES_URL = '/api/pleroma/aliases' const ALIASES_URL = '/api/pleroma/aliases'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings' const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read' export const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa' const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes' const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
@ -27,15 +27,16 @@ const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp'
const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp' const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp'
const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp' const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp'
const MASTODON_DISMISS_NOTIFICATION_URL = (id) => export const MASTODON_DISMISS_NOTIFICATION_URL = (id) =>
`/api/v1/notifications/${id}/dismiss` `/api/v1/notifications/${id}/dismiss`
const MASTODON_FAVORITE_URL = (id) => `/api/v1/statuses/${id}/favourite` export const MASTODON_FAVORITE_URL = (id) => `/api/v1/statuses/${id}/favourite`
const MASTODON_UNFAVORITE_URL = (id) => `/api/v1/statuses/${id}/unfavourite` export const MASTODON_UNFAVORITE_URL = (id) =>
const MASTODON_RETWEET_URL = (id) => `/api/v1/statuses/${id}/reblog` `/api/v1/statuses/${id}/unfavourite`
const MASTODON_UNRETWEET_URL = (id) => `/api/v1/statuses/${id}/unreblog` export const MASTODON_RETWEET_URL = (id) => `/api/v1/statuses/${id}/reblog`
const MASTODON_DELETE_URL = (id) => `/api/v1/statuses/${id}` export const MASTODON_UNRETWEET_URL = (id) => `/api/v1/statuses/${id}/unreblog`
const MASTODON_FOLLOW_URL = (id) => `/api/v1/accounts/${id}/follow` export const MASTODON_DELETE_URL = (id) => `/api/v1/statuses/${id}`
const MASTODON_UNFOLLOW_URL = (id) => `/api/v1/accounts/${id}/unfollow` export const MASTODON_FOLLOW_URL = (id) => `/api/v1/accounts/${id}/follow`
export const MASTODON_UNFOLLOW_URL = (id) => `/api/v1/accounts/${id}/unfollow`
const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests' const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests'
const MASTODON_APPROVE_USER_URL = (id) => const MASTODON_APPROVE_USER_URL = (id) =>
@ -43,49 +44,54 @@ const MASTODON_APPROVE_USER_URL = (id) =>
const MASTODON_DENY_USER_URL = (id) => `/api/v1/follow_requests/${id}/reject` const MASTODON_DENY_USER_URL = (id) => `/api/v1/follow_requests/${id}/reject`
const MASTODON_USER_RELATIONSHIPS_URL = ({ id, withSuspended }) => const MASTODON_USER_RELATIONSHIPS_URL = ({ id, withSuspended }) =>
`/api/v1/accounts/relationships/${paramsString({ id, withSuspended })}` `/api/v1/accounts/relationships/${paramsString({ id, withSuspended })}`
const MASTODON_USER_IN_LISTS = (id) => `/api/v1/accounts/${id}/lists` export const MASTODON_USER_IN_LISTS = (id) => `/api/v1/accounts/${id}/lists`
export const MASTODON_LIST_URL = (id = '') => `/api/v1/lists/${id}` export const MASTODON_LIST_URL = (id = '') => `/api/v1/lists/${id}`
export const MASTODON_LIST_ACCOUNTS_URL = (id) => `/api/v1/lists/${id}/accounts` export const MASTODON_LIST_ACCOUNTS_URL = (id) => `/api/v1/lists/${id}/accounts`
const MASTODON_USER_BLOCKS_URL = ({ export const MASTODON_USER_BLOCKS_URL = ({
maxId, maxId,
sinceId, sinceId,
limit, limit,
withRelationships, withRelationships,
}) => }) =>
`/api/v1/blocks/${paramsString({ maxId, sinceId, limit, withRelationships })}` `/api/v1/blocks/${paramsString({ maxId, sinceId, limit, withRelationships })}`
const MASTODON_USER_MUTES_URL = ({ export const MASTODON_USER_MUTES_URL = ({
maxId, maxId,
sinceId, sinceId,
limit, limit,
withRelationships, withRelationships,
}) => }) =>
`/api/v1/mutes/${paramsString({ maxId, sinceId, limit, withRelationships })}` `/api/v1/mutes/${paramsString({ maxId, sinceId, limit, withRelationships })}`
const MASTODON_BLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/block` export const MASTODON_BLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/block`
const MASTODON_UNBLOCK_USER_URL = (id) => `/api/v1/accounts/${id}/unblock` export const MASTODON_UNBLOCK_USER_URL = (id) =>
const MASTODON_MUTE_USER_URL = (id) => `/api/v1/accounts/${id}/mute` `/api/v1/accounts/${id}/unblock`
const MASTODON_UNMUTE_USER_URL = (id) => `/api/v1/accounts/${id}/unmute` export const MASTODON_MUTE_USER_URL = (id) => `/api/v1/accounts/${id}/mute`
const MASTODON_REMOVE_USER_FROM_FOLLOWERS = (id) => export const MASTODON_UNMUTE_USER_URL = (id) => `/api/v1/accounts/${id}/unmute`
export const MASTODON_REMOVE_USER_FROM_FOLLOWERS_URL = (id) =>
`/api/v1/accounts/${id}/remove_from_followers` `/api/v1/accounts/${id}/remove_from_followers`
const MASTODON_USER_NOTE_URL = (id) => `/api/v1/accounts/${id}/note` export const MASTODON_USER_NOTE_URL = (id) => `/api/v1/accounts/${id}/note`
const MASTODON_BOOKMARK_STATUS_URL = (id) => `/api/v1/statuses/${id}/bookmark` export const MASTODON_BOOKMARK_STATUS_URL = (id) =>
const MASTODON_UNBOOKMARK_STATUS_URL = (id) => `/api/v1/statuses/${id}/bookmark`
export const MASTODON_UNBOOKMARK_STATUS_URL = (id) =>
`/api/v1/statuses/${id}/unbookmark` `/api/v1/statuses/${id}/unbookmark`
const MASTODON_POST_STATUS_URL = '/api/v1/statuses' const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
const MASTODON_VOTE_URL = (id) => `/api/v1/polls/${id}/votes` const MASTODON_VOTE_URL = (id) => `/api/v1/polls/${id}/votes`
const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials' const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
const MASTODON_REPORT_USER_URL = '/api/v1/reports' const MASTODON_REPORT_USER_URL = '/api/v1/reports'
const MASTODON_PIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/pin` export const MASTODON_PIN_OWN_STATUS_URL = (id) => `/api/v1/statuses/${id}/pin`
const MASTODON_UNPIN_OWN_STATUS = (id) => `/api/v1/statuses/${id}/unpin` export const MASTODON_UNPIN_OWN_STATUS_URL = (id) =>
const MASTODON_MUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/mute` `/api/v1/statuses/${id}/unpin`
const MASTODON_UNMUTE_CONVERSATION = (id) => `/api/v1/statuses/${id}/unmute` export const MASTODON_MUTE_CONVERSATION_URL = (id) =>
const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks' `/api/v1/statuses/${id}/mute`
export const MASTODON_UNMUTE_CONVERSATION_URL = (id) =>
`/api/v1/statuses/${id}/unmute`
export const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements' const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements'
const MASTODON_ANNOUNCEMENTS_DISMISS_URL = (id) => const MASTODON_ANNOUNCEMENTS_DISMISS_URL = (id) =>
`/api/v1/announcements/${id}/dismiss` `/api/v1/announcements/${id}/dismiss`
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => export const PLEROMA_EMOJI_REACT_URL = (id, emoji) =>
`/api/v1/pleroma/statuses/${id}/reactions/${emoji}` `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => export const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) =>
`/api/v1/pleroma/statuses/${id}/reactions/${emoji}` `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups' const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders' const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders'
@ -143,39 +149,39 @@ export const bookmarkStatus = ({ id, credentials, ...options }) =>
payload: { payload: {
folder_id: options.folder_id, folder_id: options.folder_id,
}, },
}) }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
export const unbookmarkStatus = ({ id, credentials }) => export const unbookmarkStatus = ({ id, credentials }) =>
promisedRequest({ promisedRequest({
url: MASTODON_UNBOOKMARK_STATUS_URL(id), url: MASTODON_UNBOOKMARK_STATUS_URL(id),
credentials, credentials,
method: 'POST', method: 'POST',
}) }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
export const pinOwnStatus = ({ id, credentials }) => export const pinOwnStatus = ({ id, credentials }) =>
promisedRequest({ promisedRequest({
url: MASTODON_PIN_OWN_STATUS(id), url: MASTODON_PIN_OWN_STATUS_URL(id),
credentials, credentials,
method: 'POST', method: 'POST',
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
export const unpinOwnStatus = ({ id, credentials }) => export const unpinOwnStatus = ({ id, credentials }) =>
promisedRequest({ promisedRequest({
url: MASTODON_UNPIN_OWN_STATUS(id), url: MASTODON_UNPIN_OWN_STATUS_URL(id),
credentials, credentials,
method: 'POST', method: 'POST',
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
export const muteConversation = ({ id, credentials }) => export const muteConversation = ({ id, credentials }) =>
promisedRequest({ promisedRequest({
url: MASTODON_MUTE_CONVERSATION(id), url: MASTODON_MUTE_CONVERSATION_URL(id),
credentials, credentials,
method: 'POST', method: 'POST',
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
export const unmuteConversation = ({ id, credentials }) => export const unmuteConversation = ({ id, credentials }) =>
promisedRequest({ promisedRequest({
url: MASTODON_UNMUTE_CONVERSATION(id), url: MASTODON_UNMUTE_CONVERSATION_URL(id),
credentials, credentials,
method: 'POST', method: 'POST',
}).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) })) }).then(({ data, ...rest }) => ({ ...rest, data: parseStatus(data) }))
@ -656,7 +662,7 @@ export const fetchUserInLists = ({ id, credentials }) =>
export const removeUserFromFollowers = ({ id, credentials }) => export const removeUserFromFollowers = ({ id, credentials }) =>
promisedRequest({ promisedRequest({
url: MASTODON_REMOVE_USER_FROM_FOLLOWERS(id), url: MASTODON_REMOVE_USER_FROM_FOLLOWERS_URL(id),
credentials, credentials,
method: 'POST', method: 'POST',
}) })

View file

@ -26,8 +26,26 @@ const PLEROMA_STREAMING_EVENTS = new Set([
'pleroma:respond', 'pleroma:respond',
]) ])
export const WSConnectionStatus = Object.freeze({
JOINED: 1,
CLOSED: 2,
ERROR: 3,
DISABLED: 4,
STARTING: 5,
STARTING_INITIAL: 6,
})
export class WSEvent extends Event {
data
constructor(name, data, original) {
super(name)
this.data = data
}
}
// A thin wrapper around WebSocket API that allows adding a pre-processor to it // A thin wrapper around WebSocket API that allows adding a pre-processor to it
// Uses EventTarget and a CustomEvent to proxy events // Uses EventTarget and a WSEvent to proxy events
export const ProcessedWS = ({ export const ProcessedWS = ({
url, url,
preprocessor = handleMastoWS, preprocessor = handleMastoWS,
@ -39,9 +57,7 @@ export const ProcessedWS = ({
if (!socket) throw new Error(`Failed to create socket ${id}`) if (!socket) throw new Error(`Failed to create socket ${id}`)
const proxy = (original, eventName, processor = (a) => a) => { const proxy = (original, eventName, processor = (a) => a) => {
original.addEventListener(eventName, (eventData) => { original.addEventListener(eventName, (eventData) => {
eventTarget.dispatchEvent( eventTarget.dispatchEvent(new WSEvent(eventName, processor(eventData)))
new CustomEvent(eventName, { detail: processor(eventData) }),
)
}) })
} }
socket.addEventListener('open', (wsEvent) => { socket.addEventListener('open', (wsEvent) => {
@ -75,7 +91,7 @@ export const ProcessedWS = ({
/**/ /**/
const onAuthenticated = () => { const onAuthenticated = () => {
eventTarget.dispatchEvent(new CustomEvent('pleroma:authenticated')) eventTarget.dispatchEvent(new WSEvent('pleroma:authenticated'))
} }
proxy(socket, 'open') proxy(socket, 'open')
@ -126,14 +142,14 @@ export const handleMastoWS = (
const { data } = wsEvent const { data } = wsEvent
if (!data) return if (!data) return
const parsedEvent = JSON.parse(data) const parsedEvent = JSON.parse(data)
const { event, payload } = parsedEvent const { event, stream, payload } = parsedEvent
if ( if (
MASTODON_STREAMING_EVENTS.has(event) || MASTODON_STREAMING_EVENTS.has(event) ||
PLEROMA_STREAMING_EVENTS.has(event) PLEROMA_STREAMING_EVENTS.has(event)
) { ) {
// MastoBE and PleromaBE both send payload for delete as a PLAIN string // MastoBE and PleromaBE both send payload for delete as a PLAIN string
if (event === 'delete') { if (event === 'delete') {
return { event, id: payload } return { event, stream, id: payload }
} }
const data = payload ? JSON.parse(payload) : null const data = payload ? JSON.parse(payload) : null
if (event === 'pleroma:respond') { if (event === 'pleroma:respond') {
@ -150,25 +166,16 @@ export const handleMastoWS = (
} }
return null return null
} else if (event === 'update') { } else if (event === 'update') {
return { event, status: parseStatus(data) } return { event, stream, status: parseStatus(data) }
} else if (event === 'status.update') { } else if (event === 'status.update') {
return { event, status: parseStatus(data) } return { event, stream, status: parseStatus(data) }
} else if (event === 'notification') { } else if (event === 'notification') {
return { event, notification: parseNotification(data) } return { event, stream, notification: parseNotification(data) }
} else if (event === 'pleroma:chat_update') { } else if (event === 'pleroma:chat_update') {
return { event, chatUpdate: parseChat(data) } return { event, stream, chatUpdate: parseChat(data) }
} }
} else { } else {
console.warn('Unknown event', wsEvent) console.warn('Unknown event', wsEvent)
return null return null
} }
} }
export const WSConnectionStatus = Object.freeze({
JOINED: 1,
CLOSED: 2,
ERROR: 3,
DISABLED: 4,
STARTING: 5,
STARTING_INITIAL: 6,
})

View file

@ -29,6 +29,7 @@ import {
import routes from './routes' import routes from './routes'
import { useAuthFlowStore } from 'src/stores/auth_flow' import { useAuthFlowStore } from 'src/stores/auth_flow'
import { useChatsStore } from 'src/stores/chats.js'
import { useEmojiStore } from 'src/stores/emoji.js' import { useEmojiStore } from 'src/stores/emoji.js'
import { useI18nStore } from 'src/stores/i18n' import { useI18nStore } from 'src/stores/i18n'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
@ -36,9 +37,12 @@ import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.j
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useLocalConfigStore } from 'src/stores/local_config.js' import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js'
import { useUsersStore } from 'src/stores/users.js'
import VBodyScrollLock from 'src/directives/body_scroll_lock' import VBodyScrollLock from 'src/directives/body_scroll_lock'
import { import {
@ -454,7 +458,7 @@ const setConfig = async ({ store }) => {
const checkOAuthToken = async ({ store }) => { const checkOAuthToken = async ({ store }) => {
const oauth = useOAuthStore() const oauth = useOAuthStore()
if (oauth.userToken) { if (oauth.userToken) {
return store.dispatch('loginUser', oauth.userToken) return useUsersStore().loginUser(oauth.userToken)
} }
return return
} }
@ -541,7 +545,7 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
window.highlightConfig = useUserHighlightStore() window.highlightConfig = useUserHighlightStore()
FaviconService.initFaviconService() FaviconService.initFaviconService()
initServiceWorker(store) initServiceWorker(useNotificationsStore())
window.addEventListener('focus', () => updateFocus()) window.addEventListener('focus', () => updateFocus())
@ -589,6 +593,11 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
useI18nStore().setI18n(i18n) useI18nStore().setI18n(i18n)
// Global WS handlers
useChatsStore().attachSocket()
useInterfaceStore().attachSocket()
useStatusesStore().attachSocket()
app.use(router) app.use(router)
app.use(store) app.use(store)
app.use(i18n) app.use(i18n)

View file

@ -1,22 +1,16 @@
import AuthForm from 'src/components/auth_form/auth_form.js' import AuthForm from 'src/components/auth_form/auth_form.js'
import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue'
import BubbleTimeline from 'src/components/bubble_timeline/bubble_timeline.vue'
import ConversationPage from 'src/components/conversation-page/conversation-page.vue' import ConversationPage from 'src/components/conversation-page/conversation-page.vue'
import DMs from 'src/components/dm_timeline/dm_timeline.vue'
import FriendsTimeline from 'src/components/friends_timeline/friends_timeline.vue'
import NavPanel from 'src/components/nav_panel/nav_panel.vue' import NavPanel from 'src/components/nav_panel/nav_panel.vue'
import PublicAndExternalTimeline from 'src/components/public_and_external_timeline/public_and_external_timeline.vue'
import PublicTimeline from 'src/components/public_timeline/public_timeline.vue'
import QuotesTimeline from 'src/components/quotes_timeline/quotes_timeline.vue'
import RemoteUserResolver from 'src/components/remote_user_resolver/remote_user_resolver.vue' import RemoteUserResolver from 'src/components/remote_user_resolver/remote_user_resolver.vue'
import TagTimeline from 'src/components/tag_timeline/tag_timeline.vue' import Timeline from 'src/components/timeline/timeline.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useUsersStore } from 'src/stores/users.js'
export default (store) => { export default () => {
const validateAuthenticatedRoute = (to, from, next) => { const validateAuthenticatedRoute = (to, from, next) => {
if (store.state.users.currentUser) { if (useUsersStore().currentUser) {
next() next()
} else { } else {
next( next(
@ -31,7 +25,7 @@ export default (store) => {
path: '/', path: '/',
redirect: () => { redirect: () => {
return ( return (
(store.state.users.currentUser (useUsersStore().currentUser
? useInstanceStore().instanceIdentity.redirectRootLogin ? useInstanceStore().instanceIdentity.redirectRootLogin
: useInstanceStore().instanceIdentity.redirectRootNoLogin) || : useInstanceStore().instanceIdentity.redirectRootNoLogin) ||
'/main/all' '/main/all'
@ -41,22 +35,52 @@ export default (store) => {
{ {
name: 'public-external-timeline', name: 'public-external-timeline',
path: '/main/all', path: '/main/all',
component: PublicAndExternalTimeline, component: Timeline,
props: () => ({
timelineRef: { name: 'publicAndExternal' },
}),
}, },
{ {
name: 'public-timeline', name: 'public-timeline',
path: '/main/public', path: '/main/public',
component: PublicTimeline, component: Timeline,
props: () => ({
timelineRef: { name: 'public' },
}),
}, },
{ {
name: 'friends', name: 'friends',
path: '/main/friends', path: '/main/friends',
component: FriendsTimeline, component: Timeline,
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
props: () => ({
timelineRef: { name: 'friends' },
}),
},
{
name: 'tag-timeline',
path: '/tag/:id',
component: Timeline,
props: (route) => ({
timelineRef: { name: 'tag', argument: route.params.id },
}),
},
{
name: 'bookmarks',
path: '/bookmarks',
component: Timeline,
props: (route) => ({
timelineRef: { name: 'bookmarks', argument: null },
}),
},
{
name: 'bubble',
path: '/bubble',
component: Timeline,
props: () => ({
timelineRef: { name: 'bubble' },
}),
}, },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ name: 'bubble', path: '/bubble', component: BubbleTimeline },
{ {
name: 'conversation', name: 'conversation',
path: '/notice/:id', path: '/notice/:id',
@ -71,7 +95,14 @@ export default (store) => {
meta: { dontScroll: true }, meta: { dontScroll: true },
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
}, },
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline }, {
name: 'quotes',
path: '/notice/:id/quotes',
component: Timeline,
props: (route) => ({
timelineRef: { name: 'quotes', argument: route.params.id },
}),
},
{ {
name: 'remote-user-profile-acct', name: 'remote-user-profile-acct',
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)', path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
@ -104,8 +135,11 @@ export default (store) => {
{ {
name: 'dms', name: 'dms',
path: '/users/:username/dms', path: '/users/:username/dms',
component: DMs, component: Timeline,
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
props: () => ({
timelineRef: { name: 'dms' },
}),
}, },
{ {
name: 'registration', name: 'registration',
@ -202,8 +236,10 @@ export default (store) => {
{ {
name: 'lists-timeline', name: 'lists-timeline',
path: '/lists/:id', path: '/lists/:id',
component: () => component: Timeline,
import('src/components/lists_timeline/lists_timeline.vue'), props: (route) => ({
timelineRef: { name: 'list', argument: route.params.id },
}),
}, },
{ {
name: 'lists-edit', name: 'lists-edit',
@ -237,7 +273,10 @@ export default (store) => {
{ {
name: 'bookmark-folder', name: 'bookmark-folder',
path: '/bookmarks/:id', path: '/bookmarks/:id',
component: BookmarkTimeline, component: Timeline,
props: (route) => ({
timelineRef: { name: 'bookmarks', argument: route.params.id },
}),
}, },
{ {
name: 'bookmark-folder-edit', name: 'bookmark-folder-edit',

View file

@ -8,6 +8,7 @@ import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useReportsStore } from 'src/stores/reports' import { useReportsStore } from 'src/stores/reports'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons' import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
@ -47,10 +48,10 @@ const AccountActions = {
this.showingConfirmBlock = false this.showingConfirmBlock = false
}, },
showRepeats() { showRepeats() {
this.$store.dispatch('showReblogs', this.user.id) useUsersStore().showReblogs(this.user.id)
}, },
hideRepeats() { hideRepeats() {
this.$store.dispatch('hideReblogs', this.user.id) useUsersStore().hideReblogs(this.user.id)
}, },
blockUser() { blockUser() {
if (this.$refs.timedBlockDialog) { if (this.$refs.timedBlockDialog) {
@ -64,11 +65,11 @@ const AccountActions = {
} }
}, },
doBlockUser() { doBlockUser() {
this.$store.dispatch('blockUser', { id: this.user.id }) useUsersStore().blockUser(this.user.id)
this.hideConfirmBlock() this.hideConfirmBlock()
}, },
unblockUser() { unblockUser() {
this.$store.dispatch('unblockUser', this.user.id) useUsersStore().unblockUser(this.user.id)
}, },
removeUserFromFollowers() { removeUserFromFollowers() {
if (!this.shouldConfirmRemoveUserFromFollowers) { if (!this.shouldConfirmRemoveUserFromFollowers) {
@ -78,7 +79,7 @@ const AccountActions = {
} }
}, },
doRemoveUserFromFollowers() { doRemoveUserFromFollowers() {
this.$store.dispatch('removeUserFromFollowers', this.user.id) useUsersStore().removeUserFromFollowers(this.user.id)
this.hideConfirmRemoveUserFromFollowers() this.hideConfirmRemoveUserFromFollowers()
}, },
reportUser() { reportUser() {
@ -88,8 +89,8 @@ const AccountActions = {
this.$router.push({ this.$router.push({
name: 'chat', name: 'chat',
params: { params: {
username: this.$store.state.users.currentUser.screen_name, username: useUsersStore().currentUser.screen_name,
recipient_id: this.user.id, chatUserId: this.user.id,
}, },
}) })
}, },

View file

@ -1,9 +1,10 @@
import { mapState } from 'vuex' import { mapState } from 'pinia'
import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue' import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'
import localeService from '../../services/locale/locale.service.js' import localeService from '../../services/locale/locale.service.js'
import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useAnnouncementsStore } from 'src/stores/announcements.js'
import { useUsersStore } from 'src/stores/users.js'
const Announcement = { const Announcement = {
components: { components: {
@ -25,9 +26,7 @@ const Announcement = {
announcement: Object, announcement: Object,
}, },
computed: { computed: {
...mapState({ ...mapState(useUsersStore, ['currentUser']),
currentUser: (state) => state.users.currentUser,
}),
canEditAnnouncement() { canEditAnnouncement() {
return this.currentUser?.privileges.has( return this.currentUser?.privileges.has(
'announcements_manage_announcements', 'announcements_manage_announcements',

View file

@ -1,9 +1,10 @@
import { mapState } from 'vuex' import { mapState } from 'pinia'
import Announcement from 'src/components/announcement/announcement.vue' import Announcement from 'src/components/announcement/announcement.vue'
import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue' import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'
import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useAnnouncementsStore } from 'src/stores/announcements.js'
import { useUsersStore } from 'src/stores/users.js'
const AnnouncementsPage = { const AnnouncementsPage = {
components: { components: {
@ -26,9 +27,7 @@ const AnnouncementsPage = {
useAnnouncementsStore().fetchAnnouncements() useAnnouncementsStore().fetchAnnouncements()
}, },
computed: { computed: {
...mapState({ ...mapState(useUsersStore, ['currentUser']),
currentUser: (state) => state.users.currentUser,
}),
announcements() { announcements() {
return useAnnouncementsStore().announcements return useAnnouncementsStore().announcements
}, },

View file

@ -1,14 +1,19 @@
import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useUsersStore } from 'src/stores/users.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'
const AvatarList = { const AvatarList = {
props: ['users'], props: {
userIds: Set,
},
computed: { computed: {
slicedUsers() { slicedUsers() {
return this.users ? this.users.slice(0, 15) : [] return [...(this.userIds ?? [])]
.slice(0, 15)
.map((id) => useUsersStore().findUser(id))
}, },
}, },
components: { components: {

View file

@ -7,7 +7,7 @@
class="avatars-item" class="avatars-item"
> >
<UserAvatar <UserAvatar
:user="user" :user-id="user.id"
class="avatar-small" class="avatar-small"
/> />
</router-link> </router-link>

View file

@ -11,7 +11,7 @@
> >
<UserAvatar <UserAvatar
class="user-avatar avatar" class="user-avatar avatar"
:user="user" :user-id="user.id"
@click.prevent @click.prevent
/> />
</UserPopover> </UserPopover>
@ -41,7 +41,7 @@
{{ $t('admin_dash.users.labels.handle_colon') }} {{ $t('admin_dash.users.labels.handle_colon') }}
{{ ' ' }} {{ ' ' }}
</strong> </strong>
<user-link <UserLink
class="basic-user-card-screen-name" class="basic-user-card-screen-name"
:user="user" :user="user"
/> />

View file

@ -4,15 +4,16 @@ import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue' import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useUsersStore } from 'src/stores/users.js'
const BlockCard = { const BlockCard = {
props: ['userId'], props: ['userId'],
computed: { computed: {
user() { user() {
return this.$store.getters.findUser(this.userId) return useUsersStore().findUser(this.userId)
}, },
relationship() { relationship() {
return this.$store.getters.relationship(this.userId) return useUsersStore().relationship(this.userId)
}, },
blocked() { blocked() {
return this.relationship.blocking return this.relationship.blocking
@ -35,13 +36,13 @@ const BlockCard = {
}, },
methods: { methods: {
unblockUser() { unblockUser() {
this.$store.dispatch('unblockUser', this.user.id) useUsersStore().unblockUser(this.user.id)
}, },
blockUser() { blockUser() {
if (this.blockExpiration) { if (this.blockExpiration) {
this.$refs.timedBlockDialog.optionallyPrompt() this.$refs.timedBlockDialog.optionallyPrompt()
} else { } else {
this.$store.dispatch('blockUser', { id: this.user.id }) useUsersStore().blockUser(this.user.id)
} }
}, },
}, },

View file

@ -1,38 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const Bookmarks = {
created() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'bookmarks',
bookmarkFolderId: this.folderId || null,
})
},
components: {
Timeline,
},
computed: {
folderId() {
return this.$route.params.id
},
timeline() {
return this.$store.state.statuses.timelines.bookmarks
},
},
watch: {
folderId() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
this.$store.dispatch('startFetchingTimeline', {
timeline: 'bookmarks',
bookmarkFolderId: this.folderId || null,
})
},
},
unmounted() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
},
}
export default Bookmarks

View file

@ -1,10 +0,0 @@
<template>
<Timeline
:title="$t('nav.bookmarks')"
:timeline="timeline"
:timeline-name="'bookmarks'"
:bookmark-folder-id="folderId"
/>
</template>
<script src="./bookmark_timeline.js"></script>

View file

@ -1,20 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const BubbleTimeline = {
components: {
Timeline,
},
computed: {
timeline() {
return this.$store.state.statuses.timelines.bubble
},
},
created() {
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'bubble')
},
}
export default BubbleTimeline

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.bubble')"
:timeline="timeline"
:timeline-name="'bubble'"
/>
</template>
<script src="./bubble_timeline.js"></script>

View file

@ -1,11 +1,11 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { mapState } from 'vuex'
import ChatListItem from 'src/components/chat_list_item/chat_list_item.vue' import ChatListItem from 'src/components/chat_list_item/chat_list_item.vue'
import ChatNew from 'src/components/chat_new/chat_new.vue' import ChatNew from 'src/components/chat_new/chat_new.vue'
import List from 'src/components/list/list.vue' import List from 'src/components/list/list.vue'
import { useChatsStore } from 'src/stores/chats.js' import { useChatsStore } from 'src/stores/chats.js'
import { useUsersStore } from 'src/stores/users.js'
const ChatList = { const ChatList = {
components: { components: {
@ -14,10 +14,8 @@ const ChatList = {
ChatNew, ChatNew,
}, },
computed: { computed: {
...mapState({ ...mapState(useUsersStore, ['currentUser']),
currentUser: (state) => state.users.currentUser, ...mapState(useChatsStore, ['sortedChatList']),
}),
...mapPiniaState(useChatsStore, ['sortedChatList']),
}, },
data() { data() {
return { return {

View file

@ -1,4 +1,4 @@
import { mapState } from 'vuex' import { mapState } from 'pinia'
import AvatarList from 'src/components/avatar_list/avatar_list.vue' import AvatarList from 'src/components/avatar_list/avatar_list.vue'
import ChatTitle from 'src/components/chat_title/chat_title.vue' import ChatTitle from 'src/components/chat_title/chat_title.vue'
@ -6,6 +6,8 @@ import StatusBody from 'src/components/status_content/status_content.vue'
import Timeago from 'src/components/timeago/timeago.vue' import Timeago from 'src/components/timeago/timeago.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
import { useUsersStore } from 'src/stores/users.js'
const ChatListItem = { const ChatListItem = {
name: 'ChatListItem', name: 'ChatListItem',
props: ['chat'], props: ['chat'],
@ -17,9 +19,7 @@ const ChatListItem = {
StatusBody, StatusBody,
}, },
computed: { computed: {
...mapState({ ...mapState(useUsersStore, ['currentUser']),
currentUser: (state) => state.users.currentUser,
}),
attachmentInfo() { attachmentInfo() {
if (this.chat.lastMessage.attachments.length === 0) { if (this.chat.lastMessage.attachments.length === 0) {
return return

View file

@ -5,7 +5,7 @@
> >
<div class="chat-list-item-left"> <div class="chat-list-item-left">
<UserAvatar <UserAvatar
:user="chat.account" :user-id="chat.account.id"
height="48px" height="48px"
width="48px" width="48px"
/> />

View file

@ -1,6 +1,5 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import { mapState } from 'vuex'
import Attachment from 'src/components/attachment/attachment.vue' import Attachment from 'src/components/attachment/attachment.vue'
import ChatMessageDate from 'src/components/chat_message_date/chat_message_date.vue' import ChatMessageDate from 'src/components/chat_message_date/chat_message_date.vue'
@ -20,6 +19,8 @@ import UserPopover from 'src/components/user_popover/user_popover.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -79,7 +80,7 @@ const ChatMessage = {
return this.isStatus ? this.message.user.id : this.message.account_id return this.isStatus ? this.message.user.id : this.message.account_id
}, },
author() { author() {
return this.$store.getters.findUser(this.authorId) return useUsersStore().findUser(this.authorId)
}, },
isCurrentUser() { isCurrentUser() {
// mini-hack/optimizaiton: // mini-hack/optimizaiton:
@ -100,25 +101,21 @@ const ChatMessage = {
return !this.message.in_reply_to_status_id return !this.message.in_reply_to_status_id
}, },
customReplyTo() { customReplyTo() {
return this.$store.state.statuses.allStatusesObject[ return useStatusesStore().allStatuses.get(
this.message.in_reply_to_status_id this.message.in_reply_to_status_id,
] )
}, },
replyToName() { replyToName() {
if (this.message.in_reply_to_screen_name) { if (this.message.in_reply_to_screen_name) {
return this.message.in_reply_to_screen_name return this.message.in_reply_to_screen_name
} else { } else {
const user = this.$store.getters.findUser( const user = useUsersStore().findUser(this.message.in_reply_to_user_id)
this.message.in_reply_to_user_id,
)
return user?.screen_name_ui return user?.screen_name_ui
} }
}, },
replyProfileLink() { replyProfileLink() {
if (this.isCustomReply) { if (this.isCustomReply) {
const user = this.$store.getters.findUser( const user = useUsersStore().findUser(this.message.in_reply_to_user_id)
this.message.in_reply_to_user_id,
)
// FIXME Why user not found sometimes??? // FIXME Why user not found sometimes???
return user ? user.statusnet_profile_url : 'NOT_FOUND' return user ? user.statusnet_profile_url : 'NOT_FOUND'
} }
@ -167,14 +164,12 @@ const ChatMessage = {
}, },
// Global stuff // Global stuff
...mapPiniaState(useInterfaceStore, { ...mapState(useInterfaceStore, {
betterShadow: (store) => store.browserSupport.cssFilter, betterShadow: (store) => store.browserSupport.cssFilter,
}), }),
...mapState({ ...mapState(useUsersStore, ['currentUser']),
currentUser: (state) => state.users.currentUser, ...mapState(useInstanceStore, ['restrictedNicknames']),
restrictedNicknames: (state) => useInstanceStore().restrictedNicknames, ...mapState(useMergedConfigStore, ['mergedConfig']),
}),
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
}, },
data() { data() {
return { return {

View file

@ -83,7 +83,7 @@
<UserAvatar <UserAvatar
v-if="author" v-if="author"
:compact="true" :compact="true"
:user="author" :user-id="author.id"
/> />
</UserPopover> </UserPopover>
<div <div

View file

@ -1,9 +1,11 @@
import { mapGetters, mapState } from 'vuex' import { mapState } from 'pinia'
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useSearchStore } from 'src/stores/search.js'
import { useUsersStore } from 'src/stores/users.js'
import { chats } from 'src/api/chats.js' import { chats } from 'src/api/chats.js'
@ -42,17 +44,14 @@ const chatNew = {
return this.suggestions return this.suggestions
} }
}, },
...mapState({ ...mapState(useUsersStore, ['currentUser', 'findUser']),
currentUser: (state) => state.users.currentUser,
}),
...mapGetters(['findUser']),
}, },
methods: { methods: {
goBack() { goBack() {
this.$emit('cancel') this.$emit('cancel')
}, },
goToChat(user) { goToChat(user) {
this.$router.push({ name: 'chat', params: { recipient_id: user.id } }) this.$router.push({ name: 'chat', params: { chatUserId: user.id } })
}, },
onInput() { onInput() {
this.search(this.query) this.search(this.query)
@ -73,7 +72,8 @@ const chatNew = {
this.loading = true this.loading = true
this.userIds = [] this.userIds = []
this.$store this.$store
.dispatch('search', { q: query, resolve: true, type: 'accounts' }) useSearchStore()
.search({ q: query, resolve: true, type: 'accounts' })
.then((data) => { .then((data) => {
this.loading = false this.loading = false
this.userIds = data.accounts.map((a) => a.id) this.userIds = data.accounts.map((a) => a.id)

View file

@ -10,7 +10,7 @@
> >
<UserAvatar <UserAvatar
class="titlebar-avatar" class="titlebar-avatar"
:user="user" :user-id="user.id"
/> />
</UserPopover> </UserPopover>
<RichContent <RichContent

View file

@ -1,7 +1,6 @@
import { get, maxBy, minBy, sortBy, throttle } from 'lodash' import { get, maxBy, minBy, sortBy, throttle } from 'lodash'
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import { mapState } from 'vuex'
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue' import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
import ChatTitle from 'src/components/chat_title/chat_title.vue' import ChatTitle from 'src/components/chat_title/chat_title.vue'
@ -19,6 +18,9 @@ import { useChatsStore } from 'src/stores/chats.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useStreamingStore } from 'src/stores/streaming.js'
import { useUsersStore } from 'src/stores/users.js'
import { import {
chatMessages, chatMessages,
@ -85,14 +87,18 @@ const Chat = {
// Internal network stuff // Internal network stuff
fetcher: null, fetcher: null,
socket: null,
streaming: false,
fetching: true,
errorLoadingChat: false, errorLoadingChat: false,
messageRetriers: {}, messageRetriers: {},
idempotencyKeyIndex: {}, idempotencyKeyIndex: {},
} }
}, },
created() { async created() {
if (this.testMode) return if (this.testMode) return
this.startFetching() await this.activate()
this.attachSocket()
}, },
mounted() { mounted() {
window.addEventListener('resize', this.handleResize) window.addEventListener('resize', this.handleResize)
@ -118,10 +124,14 @@ const Chat = {
this.handleVisibilityChange, this.handleVisibilityChange,
false, false,
) )
if (this.testMode) return
this.deactivate()
this.detachSocket()
}, },
computed: { computed: {
conversationId() { conversationId() {
const status = this.$store.state.statuses.allStatusesObject[this.statusId] const status = useStatusesStore().allStatuses.get(this.statusId)
return get( return get(
status, status,
'retweeted_status.statusnet_conversation_id', 'retweeted_status.statusnet_conversation_id',
@ -157,17 +167,14 @@ const Chat = {
if (this.isConversation) return false // Unsupported if (this.isConversation) return false // Unsupported
return ( return (
this.mergedConfig.useStreamingApi && this.mergedConfig.useStreamingApi &&
this.mastoUserSocketStatus === WSConnectionStatus.JOINED useStreamingStore().state === WSConnectionStatus.JOINED
) )
}, },
...mapPiniaState(useInterfaceStore, { ...mapState(useInterfaceStore, {
mobileLayout: (store) => store.layoutType === 'mobile', mobileLayout: (store) => store.layoutType === 'mobile',
}), }),
...mapPiniaState(useMergedConfigStore, ['mergedConfig']), ...mapState(useMergedConfigStore, ['mergedConfig']),
...mapState({ ...mapState(useUsersStore, ['currentUser']),
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
currentUser: (state) => state.users.currentUser,
}),
}, },
watch: { watch: {
messages(old, neu) { messages(old, neu) {
@ -226,16 +233,93 @@ const Chat = {
return return
} }
this.clear() this.deactivate()
this.startFetching() this.activate()
},
mastoUserSocketStatus(newValue) {
if (newValue === WSConnectionStatus.JOINED) {
this.fetchChat({ isFirstFetch: true })
}
}, },
}, },
methods: { methods: {
async activate() {
if (!this.isConversation) {
try {
const result = await getOrCreateChat({
accountId: this.chatUserId,
credentials: useOAuthStore().token,
})
const { data } = result
useUsersStore().addNewUsers({ ...result, data: data.account })
data.account = useUsersStore().findUser(data.account.id)
this.chat = data
this.maxId = this.chat.lastMessage?.id
} catch (e) {
console.error('Error creating or getting a chat', e)
this.errorLoadingChat = true
}
}
if (this.isConversation || this.chat) {
this.startFetching('Chat activated', true)
this.$nextTick(() => {
this.scrollDown({ forceRead: true })
})
}
},
deactivate() {
this.clear()
if (this.fetching) {
this.stopFetching('Chat deactivated')
}
},
attachSocket() {
const et = new EventTarget()
const socket = {
name: 'chatview',
et,
}
et.addEventListener('update', this.onStreamMessage)
et.addEventListener('pleroma:chat_update', this.onChatUpdate)
et.addEventListener('open', this.onStreamConnect)
et.addEventListener('close', this.onStreamDisconnect)
this.socket = socket
useStreamingStore().addSubscriber(this.socket)
},
detachSocket() {
const { et } = this.socket
et.removeEventListener('update', this.onStreamMessage)
et.removeEventListener('pleroma:chat_update', this.onChatUpdate)
et.removeEventListener('open', this.onStreamConnect)
et.removeEventListener('close', this.onStreamDisconnect)
useStreamingStore().removeSubscriber(this.socket)
},
// Poll & Push
onStreamConnect() {
this.streaming = true
this.stopFetching('Socket connected')
},
onStreamDisconnect(closeEvent) {
this.streaming = false
this.startFetching('Socket disconnected')
},
startFetching(reason, isFirstFetch) {
console.debug('[Chat View] Started fetching', 'Reason:', reason)
this.fetcher = promiseInterval(
() => this.fetchChat({ fetchLatest: true }),
5000,
)
this.fetchChat({ isFirstFetch })
this.fetching = true
},
stopFetching(reason) {
console.debug('[Chat View] Stopped fetching', 'Reason:', reason)
this.fetcher.stop()
this.fetcher = null
this.fetching = false
},
// Actions // Actions
async readChat() { async readChat() {
if (this.conversationId) return // Unsupported if (this.conversationId) return // Unsupported
@ -259,18 +343,8 @@ const Chat = {
this.lastReadMessageId = this.maxId this.lastReadMessageId = this.maxId
this.newMessageCount = 0 this.newMessageCount = 0
}, },
scrollDown(options = {}) {
const { behavior = 'auto', forceRead = false } = options // Clears
this.$nextTick(() => {
window.scrollTo({
top: document.documentElement.scrollHeight,
behavior,
})
})
if (forceRead) {
this.readChat()
}
},
cullOlder() { cullOlder() {
const maxIndex = this.messages.length const maxIndex = this.messages.length
const minIndex = maxIndex - 50 const minIndex = maxIndex - 50
@ -366,35 +440,16 @@ const Chat = {
}) })
} }
}, },
async startFetching() { onStreamMessage({ data }) {
if (!this.isConversation) { const messages = data.filter(
try { ({ statusnet_conversation_id }) =>
const { data } = await getOrCreateChat({ statusnet_conversation_id === this.conversationId,
accountId: this.chatUserId,
credentials: useOAuthStore().token,
})
this.$store.commit('addNewUsers', [data.account])
data.account = this.$store.getters.findUser(data.account.id)
this.chat = data
} catch (e) {
console.error('Error creating or getting a chat', e)
this.errorLoadingChat = true
}
}
if (this.isConversation || this.chat) {
this.$nextTick(() => {
this.scrollDown({ forceRead: true })
})
this.doStartFetching()
}
},
doStartFetching() {
this.fetcher = promiseInterval(
() => this.fetchChat({ fetchLatest: true }),
5000,
) )
this.fetchChat({ isFirstFetch: true }) this.addMessages({ messages })
},
onChatUpdate({ data: { chatUpdate } }) {
const messages = [chatUpdate.lastMessage]
this.addMessages({ messages })
}, },
addMessages({ messages: newMessages }) { addMessages({ messages: newMessages }) {
for (let i = 0; i < newMessages.length; i++) { for (let i = 0; i < newMessages.length; i++) {
@ -402,17 +457,23 @@ 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
} }
// Clear any known pending messages // Clear any known pending messages
if (message.idempotency_key) { if (message.idempotency_key) {
if (this.pendingMessagesIndex[message.idempotencyKeyIndex]) { if (this.pendingMessagesIndex[message.idempotency_key]) {
delete this.pendingMessagesIndex[message.idempotencyKeyIndex] delete this.pendingMessagesIndex[message.idempotency_key]
this.pendingMessages = this.pendingMessages.filter( this.pendingMessages = this.pendingMessages.filter(
({ idempotency_key }) => ({ idempotency_key }) =>
idempotency_key !== message.idempotency_key, idempotency_key !== message.idempotency_key,
@ -438,9 +499,6 @@ const Chat = {
} }
} }
}, },
goBack() {
this.$router.back()
},
// Optimistic posting (chats only) // Optimistic posting (chats only)
async sendMessage({ status, media, idempotencyKey }) { async sendMessage({ status, media, idempotencyKey }) {
@ -621,6 +679,23 @@ const Chat = {
}) })
}, },
// Misc
scrollDown(options = {}) {
const { behavior = 'auto', forceRead = false } = options
this.$nextTick(() => {
window.scrollTo({
top: document.documentElement.scrollHeight,
behavior,
})
})
if (forceRead) {
this.readChat()
}
},
goBack() {
this.$router.back()
},
// Ugly // Ugly
// TODO move to ChatMessage // TODO move to ChatMessage
async deleteChatMessage({ chatId, messageId }) { async deleteChatMessage({ chatId, messageId }) {

View file

@ -4,6 +4,8 @@ import { defineAsyncComponent } from 'vue'
import Select from 'src/components/select/select.vue' import Select from 'src/components/select/select.vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js'
export default { export default {
props: ['type', 'user', 'status'], props: ['type', 'user', 'status'],
@ -33,9 +35,7 @@ export default {
return this.status.conversation_muted return this.status.conversation_muted
}, },
domainIsMuted() { domainIsMuted() {
return new Set(this.$store.state.users.currentUser.domainMutes).has( return new Set(useUsersStore().currentUser.domainMutes).has(this.domain)
this.domain,
)
}, },
shouldConfirm() { shouldConfirm() {
switch (this.type) { switch (this.type) {
@ -70,17 +70,17 @@ export default {
switch (this.type) { switch (this.type) {
case 'domain': { case 'domain': {
if (!this.domainIsMuted) { if (!this.domainIsMuted) {
this.$store.dispatch('muteDomain', this.domain) useUsersStore().muteDomain(this.domain)
} else { } else {
this.$store.dispatch('unmuteDomain', this.domain) useUsersStore().unmuteDomain(this.domain)
} }
break break
} }
case 'conversation': { case 'conversation': {
if (!this.conversationIsMuted) { if (!this.conversationIsMuted) {
this.$store.dispatch('muteConversation', { id: this.status.id }) useStatusesStore().muteConversation(this.status.id)
} else { } else {
this.$store.dispatch('unmuteConversation', { id: this.status.id }) useStatusesStore().unmuteConversation(this.status.id)
} }
break break
} }

View file

@ -1,6 +1,5 @@
import { clone, filter, findIndex, get, reduce } from 'lodash' import { get, reduce } from 'lodash'
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { mapState } from 'vuex'
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue' import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
@ -9,9 +8,11 @@ import QuickViewSettings from 'src/components/quick_view_settings/quick_view_set
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import ThreadTree from 'src/components/thread_tree/thread_tree.vue' import ThreadTree from 'src/components/thread_tree/thread_tree.vue'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useStreamingStore } from 'src/stores/streaming.js'
import { fetchConversation, fetchStatus } from 'src/api/public.js' import { fetchConversation, fetchStatus } from 'src/api/public.js'
import { WSConnectionStatus } from 'src/api/websocket.js' import { WSConnectionStatus } from 'src/api/websocket.js'
@ -51,20 +52,6 @@ const sortById = (a, b) => {
} }
} }
const sortAndFilterConversation = (conversation, statusoid) => {
if (statusoid.type === 'retweet') {
conversation = filter(
conversation,
(status) =>
status.type === 'retweet' ||
status.id !== statusoid.retweeted_status.id,
)
} else {
conversation = filter(conversation, (status) => status.type !== 'retweet')
}
return conversation.filter(Boolean).sort(sortById)
}
const conversation = { const conversation = {
props: { props: {
statusId: { statusId: {
@ -106,6 +93,7 @@ const conversation = {
default: false, default: false,
}, },
}, },
emits: ['update:virtualHeight'],
data() { data() {
return { return {
focused: null, focused: null,
@ -114,6 +102,7 @@ const conversation = {
inlineDivePosition: null, inlineDivePosition: null,
loadStatusError: null, loadStatusError: null,
unsuspendibleIds: new Set(), unsuspendibleIds: new Set(),
virtualHeight: 120,
} }
}, },
created() { created() {
@ -121,7 +110,13 @@ const conversation = {
this.fetchConversation() this.fetchConversation()
} }
}, },
mounted() {
this.updateVirtualHeight()
},
computed: { computed: {
status() {
return useStatusesStore().allStatuses.get(this.statusId)
},
maxDepthToShowByDefault() { maxDepthToShowByDefault() {
// maxDepthInThread = max number of depths that is *visible* // maxDepthInThread = max number of depths that is *visible*
// since our depth starts with 0 and "showing" means "showing children" // since our depth starts with 0 and "showing" means "showing children"
@ -160,14 +155,11 @@ const conversation = {
return this.otherRepliesButtonPosition === 'inside' return this.otherRepliesButtonPosition === 'inside'
}, },
suspendable() { suspendable() {
return this.unsuspendibleIds.size > 0 return this.unsuspendibleIds.size === 0
}, },
hideStatus() { hide() {
return this.virtualHidden && this.suspendable return this.virtualHidden && this.suspendable
}, },
status() {
return this.$store.state.statuses.allStatusesObject[this.statusId]
},
originalStatusId() { originalStatusId() {
if (this.status.retweeted_status) { if (this.status.retweeted_status) {
return this.status.retweeted_status.id return this.status.retweeted_status.id
@ -187,15 +179,14 @@ const conversation = {
return [this.status] return [this.status]
} }
const conversation = clone( const conversation = useStatusesStore().conversations.get(
this.$store.state.statuses.conversationsObject[this.conversationId], this.conversationId,
) )
const statusIndex = findIndex(conversation, { id: this.originalStatusId })
if (statusIndex !== -1) {
conversation[statusIndex] = this.status
}
return sortAndFilterConversation(conversation, this.status) return [...conversation.keys()]
.map((k) => useStatusesStore().allStatuses.get(k))
.filter((status) => status.type != 'repeat') // Old backend behavior?
.toSorted(sortById)
}, },
statusMap() { statusMap() {
return this.conversation.reduce((res, s) => { return this.conversation.reduce((res, s) => {
@ -375,8 +366,7 @@ const conversation = {
return !!(this.expanded || this.isPage) return !!(this.expanded || this.isPage)
}, },
hiddenStyle() { hiddenStyle() {
const height = this.status?.virtualHeight || '120px' return { height: this.virtualHeight + 'px' }
return this.virtualHidden ? { height } : {}
}, },
threadDisplayStatus() { threadDisplayStatus() {
return this.conversation.reduce((a, k) => { return this.conversation.reduce((a, k) => {
@ -403,11 +393,11 @@ const conversation = {
maybeFocused() { maybeFocused() {
return this.isExpanded ? this.focused : null return this.isExpanded ? this.focused : null
}, },
...mapPiniaState(useMergedConfigStore, ['mergedConfig']), ...mapState(useMergedConfigStore, ['mergedConfig']),
...mapState({ ...mapState(useStreamingStore, {
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus, mastoUserSocketStatus: (state) => state.state,
}), }),
...mapPiniaState(useInterfaceStore, { ...mapState(useInterfaceStore, {
mobileLayout: (store) => store.layoutType === 'mobile', mobileLayout: (store) => store.layoutType === 'mobile',
}), }),
}, },
@ -441,10 +431,7 @@ const conversation = {
} }
}, },
virtualHidden() { virtualHidden() {
this.$store.dispatch('setVirtualHeight', { this.updateVirtualHeight()
statusId: this.statusId,
height: `${this.$el.clientHeight}px`,
})
}, },
}, },
methods: { methods: {
@ -453,9 +440,12 @@ const conversation = {
fetchConversation({ fetchConversation({
id: this.statusId, id: this.statusId,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}).then(({ data: { ancestors, descendants } }) => { }).then(({ data: { ancestors, descendants }, timestamp }) => {
this.$store.dispatch('addNewStatuses', { statuses: ancestors }) useStatusesStore().addNewStatuses({ statuses: ancestors, timestamp })
this.$store.dispatch('addNewStatuses', { statuses: descendants }) useStatusesStore().addNewStatuses({
statuses: descendants,
timestamp,
})
this.setFocused(this.originalStatusId) this.setFocused(this.originalStatusId)
}) })
} else { } else {
@ -465,7 +455,7 @@ const conversation = {
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}) })
.then(({ data: status }) => { .then(({ data: status }) => {
this.$store.dispatch('addNewStatuses', { statuses: [status] }) useStatusesStore().addNewStatuses({ statuses: [status] })
this.fetchConversation() this.fetchConversation()
}) })
.catch((error) => { .catch((error) => {
@ -482,17 +472,17 @@ const conversation = {
this.focused = id this.focused = id
if (!this.streamingEnabled) { if (!this.streamingEnabled) {
this.$store.dispatch('fetchStatus', id) useStatusesStore().fetchStatus(id)
} }
this.$store.dispatch('fetchFavsAndRepeats', id) useStatusesStore().fetchFavsAndRepeats(id)
this.$store.dispatch('fetchEmojiReactionsBy', id) useStatusesStore().fetchEmojiReactions(id)
}, },
toggleExpanded() { toggleExpanded() {
this.expanded = !this.expanded this.expanded = !this.expanded
}, },
getConversationId(statusId) { getConversationId(statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId] const status = useStatusesStore().allStatuses.get(statusId)
return get( return get(
status, status,
'retweeted_status.statusnet_conversation_id', 'retweeted_status.statusnet_conversation_id',
@ -630,6 +620,18 @@ const conversation = {
this.$router.push({ name: 'conversation', params: { id: data.id } }) this.$router.push({ name: 'conversation', params: { id: data.id } })
} }
}, },
updateVirtualHeight() {
if (this.hide) return // no updates when not rendering
if (!this.status) return // not loaded yet
this.$nextTick(() => {
this.virtualHeight = this.$refs.body.getBoundingClientRect().height
this.$emit('update:virtualHeight', {
id: this.status.id,
height: this.virtualHeight,
top: this.$el.clientTop,
})
})
},
}, },
} }

View file

@ -1,7 +1,6 @@
<template> <template>
<div <div
v-if="!hideStatus" v-if="!hide"
:style="hiddenStyle"
class="Conversation" class="Conversation"
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }" :class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
> >
@ -40,6 +39,7 @@
<div <div
v-if="isPage && !status" v-if="isPage && !status"
class="conversation-body" class="conversation-body"
ref="body"
:class="{ 'panel-body': isExpanded }" :class="{ 'panel-body': isExpanded }"
> >
<p v-if="!loadStatusError"> <p v-if="!loadStatusError">
@ -56,6 +56,7 @@
<div <div
v-else v-else
class="conversation-body" class="conversation-body"
ref="body"
:class="{ 'panel-body': isExpanded }" :class="{ 'panel-body': isExpanded }"
> >
<div <div
@ -99,7 +100,7 @@
ref="statusComponent" ref="statusComponent"
class="conversation-status status-fadein panel-body" class="conversation-status status-fadein panel-body"
:statusoid="status" :status-id="status.id"
:replies="getReplies(status.id)" :replies="getReplies(status.id)"
:expandable="!isExpanded" :expandable="!isExpanded"
@ -116,6 +117,7 @@
@goto="setFocused" @goto="setFocused"
@dive="() => diveIntoStatus(status.id)" @dive="() => diveIntoStatus(status.id)"
@suspendable-state-change="onStatusSuspendStateChange" @suspendable-state-change="onStatusSuspendStateChange"
@height-change="updateVirtualHeight"
/> />
<div <div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1" v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
@ -152,7 +154,7 @@
ref="statusComponent" ref="statusComponent"
:depth="0" :depth="0"
:status="status" :status-id="status.id"
:in-profile="inProfile" :in-profile="inProfile"
:conversation="conversation" :conversation="conversation"
:collapsable="collapsable" :collapsable="collapsable"
@ -174,6 +176,7 @@
@goto="setFocused" @goto="setFocused"
@dive="diveIntoStatus" @dive="diveIntoStatus"
@suspendable-state-change="onStatusSuspendStateChange" @suspendable-state-change="onStatusSuspendStateChange"
@height-change="updateVirtualHeight"
/> />
</div> </div>
<div <div
@ -186,7 +189,7 @@
:key="status.id" :key="status.id"
ref="statusComponent" ref="statusComponent"
class="conversation-status status-fadein panel-body" class="conversation-status status-fadein panel-body"
:statusoid="status" :status-id="status.id"
:replies="getReplies(status.id)" :replies="getReplies(status.id)"
:expandable="!isExpanded" :expandable="!isExpanded"
@ -200,6 +203,7 @@
@goto="setFocused" @goto="setFocused"
@toggle-expanded="toggleExpanded" @toggle-expanded="toggleExpanded"
@suspendable-state-change="onStatusSuspendStateChange" @suspendable-state-change="onStatusSuspendStateChange"
@height-change="updateVirtualHeight"
/> />
</article> </article>
</div> </div>

View file

@ -5,6 +5,10 @@ import { defineAsyncComponent } from 'vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStreamingStore } from 'src/stores/streaming.js'
import { useUsersStore } from 'src/stores/users.js'
import { WSConnectionStatus } from 'src/api/websocket.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -14,6 +18,8 @@ import {
faComments, faComments,
faHome, faHome,
faInfoCircle, faInfoCircle,
faPlug,
faPlugCircleXmark,
faSearch, faSearch,
faSignInAlt, faSignInAlt,
faSignOutAlt, faSignOutAlt,
@ -33,6 +39,8 @@ library.add(
faTachometerAlt, faTachometerAlt,
faCog, faCog,
faInfoCircle, faInfoCircle,
faPlug,
faPlugCircleXmark,
) )
export default { export default {
@ -91,11 +99,23 @@ export default {
sitename: (store) => store.instanceIdentity.name, sitename: (store) => store.instanceIdentity.name,
hideSitename: (store) => store.instanceIdentity.hideSitename, hideSitename: (store) => store.instanceIdentity.hideSitename,
}), }),
currentUser() { ...mapState(useUsersStore, ['currentUser']),
return this.$store.state.users.currentUser ...mapState(useStreamingStore, {
}, streamingConnected: (store) => store.state === WSConnectionStatus.JOINED,
}),
...mapState(useMergedConfigStore, ['mergedConfig']),
shouldConfirmLogout() { shouldConfirmLogout() {
return useMergedConfigStore().mergedConfig.modalOnLogout return this.mergedConfig.modalOnLogout
},
streamingEnabled() {
return this.mergedConfig.useStreamingApi
},
streamingTooltip() {
if (this.streamingConnected) {
return this.$t('timeline.socket_reconnected')
} else {
return this.$t('timeline.socket_disconnected')
}
}, },
}, },
methods: { methods: {
@ -115,9 +135,9 @@ export default {
this.showConfirmLogout() this.showConfirmLogout()
} }
}, },
doLogout() { async doLogout() {
await useUsersStore().logout()
this.$router.replace('/main/public') this.$router.replace('/main/public')
this.$store.dispatch('logout')
this.hideConfirmLogout() this.hideConfirmLogout()
}, },
onSearchBarToggled(hidden) { onSearchBarToggled(hidden) {

View file

@ -15,6 +15,24 @@
> >
{{ sitename }} {{ sitename }}
</router-link> </router-link>
<div
class="nav-icon"
v-if="streamingEnabled"
:title="streamingTooltip"
>
<FAIcon
v-if="streamingConnected"
fixed-width
class="fa-scale-110 fa-old-padding"
icon="plug"
/>
<FAIcon
v-else
fixed-width
class="fa-scale-110 fa-old-padding"
icon="plug-circle-xmark"
/>
</div>
</div> </div>
<router-link <router-link
class="logo" class="logo"

View file

@ -1,14 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const DMs = {
computed: {
timeline() {
return this.$store.state.statuses.timelines.dms
},
},
components: {
Timeline,
},
}
export default DMs

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.dms')"
:timeline="timeline"
:timeline-name="'dms'"
/>
</template>
<script src="./dm_timeline.js"></script>

View file

@ -1,5 +1,7 @@
import ProgressButton from 'src/components/progress_button/progress_button.vue' import ProgressButton from 'src/components/progress_button/progress_button.vue'
import { useUsersStore } from 'src/stores/users.js'
const DomainMuteCard = { const DomainMuteCard = {
props: ['domain'], props: ['domain'],
components: { components: {
@ -7,18 +9,18 @@ const DomainMuteCard = {
}, },
computed: { computed: {
user() { user() {
return this.$store.state.users.currentUser return useUsersStore().currentUser
}, },
muted() { muted() {
return this.user.domainMutes.includes(this.domain) return this.user.domainMutes.has(this.domain)
}, },
}, },
methods: { methods: {
unmuteDomain() { unmuteDomain() {
return this.$store.dispatch('unmuteDomain', this.domain) return useUsersStore().unmuteDomain(this.domain)
}, },
muteDomain() { muteDomain() {
return this.$store.dispatch('muteDomain', this.domain) return useUsersStore().muteDomain(this.domain)
}, },
}, },
} }

View file

@ -6,6 +6,7 @@ import PostStatusForm from 'src/components/post_status_form/post_status_form.vue
import StatusContent from 'src/components/status_content/status_content.vue' import StatusContent from 'src/components/status_content/status_content.vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faPollH } from '@fortawesome/free-solid-svg-icons' import { faPollH } from '@fortawesome/free-solid-svg-icons'
@ -65,7 +66,7 @@ const Draft = {
}, },
refStatus() { refStatus() {
return this.draft.refId return this.draft.refId
? this.$store.state.statuses.allStatusesObject[this.draft.refId] ? useStatusesStore().allStatuses.get(this.draft.refId)
: undefined : undefined
}, },
localCollapseSubjectDefault() { localCollapseSubjectDefault() {

View file

@ -1,9 +1,11 @@
import { get } from 'lodash' import { get } from 'lodash'
import { mapState } from 'pinia'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import Modal from 'src/components/modal/modal.vue' import Modal from 'src/components/modal/modal.vue'
import { useEditStatusStore } from 'src/stores/editStatus.js' import { useEditStatusStore } from 'src/stores/editStatus.js'
import { useUsersStore } from 'src/stores/users.js'
const EditStatusModal = { const EditStatusModal = {
components: { components: {
@ -18,18 +20,16 @@ const EditStatusModal = {
} }
}, },
computed: { computed: {
isLoggedIn() {
return !!this.$store.state.users.currentUser
},
modalActivated() { modalActivated() {
return useEditStatusStore().modalActivated return useEditStatusStore().modalActivated
}, },
isFormVisible() { isFormVisible() {
return this.isLoggedIn && !this.resettingForm && this.modalActivated return this.loggedIn && !this.resettingForm && this.modalActivated
}, },
params() { params() {
return useEditStatusStore().params || {} return useEditStatusStore().params || {}
}, },
...mapState(useUsersStore, ['loggedIn']),
}, },
watch: { watch: {
params(newVal, oldVal) { params(newVal, oldVal) {

View file

@ -1,3 +1,6 @@
import { useSearchStore } from 'src/stores/search.js'
import { useUsersStore } from 'src/stores/users.js'
/** /**
* suggest - generates a suggestor function to be used by emoji-input * suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions: * data: object providing source information for specific types of suggestions:
@ -69,7 +72,7 @@ export const suggestEmoji = (emojis) => (input, nameKeywordLocalizer) => {
}) })
} }
export const suggestUsers = ({ dispatch, state }) => { export const suggestUsers = () => {
// Keep some persistent values in closure, most importantly for the // Keep some persistent values in closure, most importantly for the
// custom debounce to work. Lodash debounce does not return a promise. // custom debounce to work. Lodash debounce does not return a promise.
let suggestions = [] let suggestions = []
@ -77,7 +80,7 @@ export const suggestUsers = ({ dispatch, state }) => {
let timeout = null let timeout = null
let cancelUserSearch = null let cancelUserSearch = null
const userSearch = (query) => dispatch('searchUsers', { query }) const userSearch = (query) => useSearchStore().searchUsers({ query })
const debounceUserSearch = (query) => { const debounceUserSearch = (query) => {
cancelUserSearch?.() cancelUserSearch?.()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -105,7 +108,7 @@ export const suggestUsers = ({ dispatch, state }) => {
await debounceUserSearch(noPrefix) await debounceUserSearch(noPrefix)
} }
const newSuggestions = state.users.users const newSuggestions = [...useUsersStore().users.values()]
.filter( .filter(
(user) => (user) =>
user.screen_name && user.screen_name &&

View file

@ -3,6 +3,8 @@ import UserListPopover from 'src/components/user_list_popover/user_list_popover.
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCheck, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons' import { faCheck, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
@ -35,12 +37,12 @@ const EmojiReactions = {
}, },
accountsForEmoji() { accountsForEmoji() {
return this.status.emoji_reactions.reduce((acc, reaction) => { return this.status.emoji_reactions.reduce((acc, reaction) => {
acc[reaction.name] = reaction.accounts || [] acc.set(reaction.name, new Set(reaction.account_ids))
return acc return acc
}, {}) }, new Map())
}, },
loggedIn() { loggedIn() {
return !!this.$store.state.users.currentUser return !!useUsersStore().currentUser
}, },
remoteInteractionLink() { remoteInteractionLink() {
return useInstanceStore().getRemoteInteractionLink({ return useInstanceStore().getRemoteInteractionLink({
@ -58,25 +60,22 @@ const EmojiReactions = {
reactedWith(emoji) { reactedWith(emoji) {
return this.status.emoji_reactions.find((r) => r.name === emoji).me return this.status.emoji_reactions.find((r) => r.name === emoji).me
}, },
async fetchEmojiReactionsByIfMissing() { async fetchEmojiReactionsIfMissing() {
const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts) const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
if (hasNoAccounts) { if (hasNoAccounts) {
return await this.$store.dispatch( return await useStatusesStore().fetchEmojiReactions(this.status.id)
'fetchEmojiReactionsBy',
this.status.id,
)
} }
}, },
reactWith(emoji) { reactWith(emoji) {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) useStatusesStore().reactWithEmoji(this.status.id, emoji)
}, },
unreact(emoji) { unreact(emoji) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) useStatusesStore().unreactWithEmoji(this.status.id, emoji)
}, },
async emojiOnClick(emoji) { async emojiOnClick(emoji) {
if (!this.loggedIn) return if (!this.loggedIn) return
await this.fetchEmojiReactionsByIfMissing() await this.fetchEmojiReactionsIfMissing()
if (this.reactedWith(emoji)) { if (this.reactedWith(emoji)) {
this.unreact(emoji) this.unreact(emoji)
} else { } else {

View file

@ -52,11 +52,11 @@
</FALayers> </FALayers>
</component> </component>
<UserListPopover <UserListPopover
:users="accountsForEmoji[reaction.name]" :user-ids="accountsForEmoji.get(reaction.name)"
class="emoji-reaction-popover" class="emoji-reaction-popover"
:normal-button="true" :normal-button="true"
:trigger-attrs="counterTriggerAttrs(reaction)" :trigger-attrs="counterTriggerAttrs(reaction)"
@show="fetchEmojiReactionsByIfMissing()" @show="fetchEmojiReactionsIfMissing()"
> >
<span class="emoji-reaction-counts">{{ reaction.count }}</span> <span class="emoji-reaction-counts">{{ reaction.count }}</span>
</UserListPopover> </UserListPopover>

View file

@ -6,6 +6,7 @@ import { useChatsStore } from 'src/stores/chats.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -52,7 +53,7 @@ const ExtraNotifications = {
) )
}, },
currentUser() { currentUser() {
return this.$store.state.users.currentUser return useUsersStore().currentUser
}, },
...mapGetters(['followRequestCount']), ...mapGetters(['followRequestCount']),
...mapState(useAnnouncementsStore, { ...mapState(useAnnouncementsStore, {

View file

@ -1,11 +1,8 @@
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import {
requestFollow,
requestUnfollow,
} from '../../services/follow_manipulate/follow_manipulate'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useUsersStore } from 'src/stores/users.js'
export default { export default {
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
components: { components: {
@ -64,9 +61,11 @@ export default {
}, },
follow() { follow() {
this.inProgress = true this.inProgress = true
requestFollow(this.relationship.id, this.$store).then(() => { useUsersStore()
this.inProgress = false .followUser(this.relationship.id)
}) .finally(() => {
this.inProgress = false
})
}, },
unfollow() { unfollow() {
if (this.shouldConfirmUnfollow) { if (this.shouldConfirmUnfollow) {
@ -76,15 +75,12 @@ export default {
} }
}, },
doUnfollow() { doUnfollow() {
const store = this.$store
this.inProgress = true this.inProgress = true
requestUnfollow(this.relationship.id, store).then(() => { useUsersStore()
this.inProgress = false .unfollowUser(this.relationship.id)
store.commit('removeStatus', { .finally(() => {
timeline: 'friends', this.inProgress = false
userId: this.relationship.id,
}) })
})
this.hideConfirmUnfollow() this.hideConfirmUnfollow()
}, },

View file

@ -3,6 +3,8 @@ import FollowButton from 'src/components/follow_button/follow_button.vue'
import RemoteFollow from 'src/components/remote_follow/remote_follow.vue' import RemoteFollow from 'src/components/remote_follow/remote_follow.vue'
import RemoveFollowerButton from 'src/components/remove_follower_button/remove_follower_button.vue' import RemoveFollowerButton from 'src/components/remove_follower_button/remove_follower_button.vue'
import { useUsersStore } from 'src/stores/users.js'
const FollowCard = { const FollowCard = {
props: ['user', 'noFollowsYou'], props: ['user', 'noFollowsYou'],
components: { components: {
@ -13,13 +15,13 @@ const FollowCard = {
}, },
computed: { computed: {
isMe() { isMe() {
return this.$store.state.users.currentUser.id === this.user.id return useUsersStore().currentUser?.id === this.user.id
}, },
loggedIn() { loggedIn() {
return this.$store.state.users.currentUser return useUsersStore().currentUser
}, },
relationship() { relationship() {
return this.$store.getters.relationship(this.user.id) return useUsersStore().relationships.get(this.user.id)
}, },
}, },
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<basic-user-card :user="user"> <BasicUserCard :user="user">
<div class="follow-card-content-container"> <div class="follow-card-content-container">
<span <span
v-if="isMe || (!noFollowsYou && relationship.followed_by)" v-if="isMe || (!noFollowsYou && relationship.followed_by)"
@ -30,7 +30,7 @@
/> />
</template> </template>
</div> </div>
</basic-user-card> </BasicUserCard>
</template> </template>
<script src="./follow_card.js"></script> <script src="./follow_card.js"></script>

View file

@ -1,9 +1,9 @@
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { approveUser, denyUser } from 'src/api/user.js' import { approveUser, denyUser } from 'src/api/user.js'
@ -24,7 +24,7 @@ const FollowRequestCard = {
}, },
methods: { methods: {
findFollowRequestNotificationId() { findFollowRequestNotificationId() {
const notif = notificationsFromStore(this.$store).find( const notif = useNotificationsStore().data.find(
(notif) => (notif) =>
notif.from_profile.id === this.user.id && notif.from_profile.id === this.user.id &&
notif.type === 'follow_request', notif.type === 'follow_request',
@ -55,16 +55,11 @@ const FollowRequestCard = {
id: this.user.id, id: this.user.id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}) })
// TODO fix
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
const notifId = this.findFollowRequestNotificationId() const notifId = this.findFollowRequestNotificationId()
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId }) useNotificationsStore().markSingleNotificationAsSeen(notifId)
this.$store.dispatch('updateNotification', {
id: notifId,
updater: (notification) => {
notification.type = 'follow'
},
})
this.hideApproveConfirmDialog() this.hideApproveConfirmDialog()
}, },
denyUser() { denyUser() {
@ -81,7 +76,8 @@ const FollowRequestCard = {
id: this.user.id, id: this.user.id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}).then(() => { }).then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId }) useNotificationsStore().dismissNotificationLocal(notifId)
// TODO fix
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.hideDenyConfirmDialog() this.hideDenyConfirmDialog()

View file

@ -1,14 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const FriendsTimeline = {
components: {
Timeline,
},
computed: {
timeline() {
return this.$store.state.statuses.timelines.friends
},
},
}
export default FriendsTimeline

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.timeline')"
:timeline="timeline"
:timeline-name="'friends'"
/>
</template>
<script src="./friends_timeline.js"></script>

View file

@ -1,6 +1,8 @@
import Notifications from 'src/components/notifications/notifications.vue' import Notifications from 'src/components/notifications/notifications.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import { useUsersStore } from 'src/stores/users.js'
const tabModeDict = { const tabModeDict = {
mentions: ['mention'], mentions: ['mention'],
statuses: ['status'], statuses: ['status'],
@ -14,10 +16,9 @@ const tabModeDict = {
const Interactions = { const Interactions = {
data() { data() {
return { return {
allowFollowingMove: allowFollowingMove: useUsersStore().currentUser.allow_following_move,
this.$store.state.users.currentUser.allow_following_move,
filterMode: tabModeDict.mentions, filterMode: tabModeDict.mentions,
canSeeReports: this.$store.state.users.currentUser.privileges.has( canSeeReports: useUsersStore().currentUser.privileges.has(
'reports_manage_reports', 'reports_manage_reports',
), ),
} }

View file

@ -21,8 +21,8 @@ const List = {
default: () => '', default: () => '',
}, },
preSelect: { preSelect: {
type: Array, type: Set,
default: [], default: new Set(),
}, },
nonInteractive: { nonInteractive: {
type: Boolean, type: Boolean,
@ -48,7 +48,7 @@ const List = {
data() { data() {
return { return {
items: [], items: [],
selected: new Set(this.preSelect), selected: new Set(this.preSelect), // clone
loading: false, loading: false,
bottomedOut: true, bottomedOut: true,
error: null, error: null,
@ -99,11 +99,11 @@ const List = {
this.fetchFunction(this.page) this.fetchFunction(this.page)
.then((result) => { .then((result) => {
this.loading = false this.loading = false
this.bottomedOut = isEmpty(result.items) this.bottomedOut = isEmpty(result)
if (this.externalItems) return if (this.externalItems) return
this.page += 1 this.page += 1
this.total = result.count this.total = result.length
this.items.push(...result.items) this.items.push(...result)
}) })
.catch((error) => { .catch((error) => {
this.loading = false this.loading = false

View file

@ -1,5 +1,4 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { mapGetters, mapState } from 'vuex'
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import ListsUserSearch from 'src/components/lists_user_search/lists_user_search.vue' import ListsUserSearch from 'src/components/lists_user_search/lists_user_search.vue'
@ -9,6 +8,7 @@ import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useListsStore } from 'src/stores/lists.js' import { useListsStore } from 'src/stores/lists.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons' import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
@ -47,8 +47,8 @@ const ListsNew = {
.fetchListAccounts({ listId: this.id }) .fetchListAccounts({ listId: this.id })
.then(() => { .then(() => {
this.membersUserIds = this.findListAccounts(this.id) this.membersUserIds = this.findListAccounts(this.id)
this.membersUserIds.forEach((userId) => { this.membersUserIds.forEach((id) => {
this.$store.dispatch('fetchUserIfMissing', userId) useUsersStore().fetchUserIfMissing({ id })
}) })
}) })
}, },
@ -66,11 +66,8 @@ const ListsNew = {
.map((userId) => this.findUser(userId)) .map((userId) => this.findUser(userId))
.filter(Boolean) .filter(Boolean)
}, },
...mapState({ ...mapState(useUsersStore, ['currentUser', 'findUser']),
currentUser: (state) => state.users.currentUser, ...mapState(useListsStore, ['findListTitle', 'findListAccounts']),
}),
...mapPiniaState(useListsStore, ['findListTitle', 'findListAccounts']),
...mapGetters(['findUser']),
}, },
methods: { methods: {
onInput() { onInput() {

View file

@ -1,10 +1,10 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { mapState } from 'vuex'
import { getListEntries } from 'src/components/navigation/filter.js' import { getListEntries } from 'src/components/navigation/filter.js'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue' import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { useListsStore } from 'src/stores/lists.js' import { useListsStore } from 'src/stores/lists.js'
import { useUsersStore } from 'src/stores/users.js'
export const ListsMenuContent = { export const ListsMenuContent = {
props: ['showPin'], props: ['showPin'],
@ -12,12 +12,10 @@ export const ListsMenuContent = {
NavigationEntry, NavigationEntry,
}, },
computed: { computed: {
...mapPiniaState(useListsStore, { ...mapState(useListsStore, {
lists: getListEntries, lists: getListEntries,
}), }),
...mapState({ ...mapState(useUsersStore, ['currentUser']),
currentUser: (state) => state.users.currentUser,
}),
}, },
} }

View file

@ -1,47 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
import { useListsStore } from 'src/stores/lists.js'
const ListsTimeline = {
data() {
return {
listId: null,
}
},
components: {
Timeline,
},
computed: {
timeline() {
return this.$store.state.statuses.timelines.list
},
},
watch: {
$route: function (route) {
if (route.name === 'lists-timeline' && route.params.id !== this.listId) {
this.listId = route.params.id
this.$store.dispatch('stopFetchingTimeline', 'list')
this.$store.commit('clearTimeline', { timeline: 'list' })
useListsStore().fetchList({ listId: this.listId })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'list',
listId: this.listId,
})
}
},
},
created() {
this.listId = this.$route.params.id
useListsStore().fetchList({ listId: this.listId })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'list',
listId: this.listId,
})
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'list')
this.$store.commit('clearTimeline', { timeline: 'list' })
},
}
export default ListsTimeline

View file

@ -1,10 +0,0 @@
<template>
<Timeline
title="list.name"
:timeline="timeline"
:list-id="listId"
timeline-name="list"
/>
</template>
<script src="./lists_timeline.js"></script>

View file

@ -2,6 +2,8 @@ import { debounce } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import { useSearchStore } from 'src/stores/search.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons' import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
@ -32,8 +34,8 @@ const ListsUserSearch = {
this.loading = true this.loading = true
this.$emit('loading') this.$emit('loading')
this.userIds = [] this.userIds = []
this.$store useSearchStore()
.dispatch('search', { .search({
q: query, q: query,
resolve: true, resolve: true,
type: 'accounts', type: 'accounts',

View file

@ -1,9 +1,9 @@
import { mapActions, mapState as mapPiniaState } from 'pinia' import { mapActions, mapState } from 'pinia'
import { mapState } from 'vuex'
import { useAuthFlowStore } from 'src/stores/auth_flow.js' import { useAuthFlowStore } from 'src/stores/auth_flow.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useUsersStore } from 'src/stores/users.js'
import { getLoginUrl, getTokenWithCredentials } from 'src/api/oauth.js' import { getLoginUrl, getTokenWithCredentials } from 'src/api/oauth.js'
@ -18,12 +18,10 @@ const LoginForm = {
error: false, error: false,
}), }),
computed: { computed: {
...mapState({ ...mapState(useUsersStore, ['loggingIn']),
loggingIn: (state) => state.users.loggingIn, ...mapState(useOAuthStore, ['clientId', 'clientSecret']),
}), ...mapState(useInstanceStore, ['server', 'registrationOpen']),
...mapPiniaState(useOAuthStore, ['clientId', 'clientSecret']), ...mapState(useAuthFlowStore, {
...mapPiniaState(useInstanceStore, ['server', 'registrationOpen']),
...mapPiniaState(useAuthFlowStore, {
isTokenAuth: (store) => store.requiredToken, isTokenAuth: (store) => store.requiredToken,
isPasswordAuth: (store) => !store.requiredToken, isPasswordAuth: (store) => !store.requiredToken,
}), }),

View file

@ -1,5 +1,4 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { mapState } from 'vuex'
import UnicodeDomainIndicator from 'src/components/unicode_domain_indicator/unicode_domain_indicator.vue' import UnicodeDomainIndicator from 'src/components/unicode_domain_indicator/unicode_domain_indicator.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
@ -12,6 +11,7 @@ import {
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js'
import { useUsersStore } from 'src/stores/users.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'
@ -29,7 +29,7 @@ const MentionLink = {
}, },
props: { props: {
url: { url: {
required: true, required: false,
type: String, type: String,
}, },
content: { content: {
@ -75,11 +75,11 @@ const MentionLink = {
}, },
computed: { computed: {
user() { user() {
return this.url && this.$store?.getters.findUserByUrl(this.url) return this.url ? useUsersStore().findUserByUrl(this.url) : null
}, },
isYou() { isYou() {
// FIXME why user !== currentUser??? if (!this.currentUser) return false
return this.user?.id === this.currentUser.id return this.user === this.currentUser
}, },
userName() { userName() {
return this.user && this.userNameFullUi.split('@')[0] return this.user && this.userNameFullUi.split('@')[0]
@ -156,11 +156,9 @@ const MentionLink = {
shouldFadeDomain() { shouldFadeDomain() {
return this.mergedConfig.mentionLinkFadeDomain return this.mergedConfig.mentionLinkFadeDomain
}, },
...mapPiniaState(useMergedConfigStore, ['mergedConfig']), ...mapState(useMergedConfigStore, ['mergedConfig']),
...mapPiniaState(useUserHighlightStore, ['highlight']), ...mapState(useUserHighlightStore, ['highlight']),
...mapState({ ...mapState(useUsersStore, ['currentUser']),
currentUser: (state) => state.users.currentUser,
}),
}, },
} }

View file

@ -31,7 +31,7 @@
<UserAvatar <UserAvatar
v-if="shouldShowAvatar" v-if="shouldShowAvatar"
class="mention-avatar" class="mention-avatar"
:user="user" :user-id="user.id"
/><span /><span
class="shortName" class="shortName"
>@<span >@<span

View file

@ -1,14 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const Mentions = {
computed: {
timeline() {
return this.$store.state.statuses.timelines.mentions
},
},
components: {
Timeline,
},
}
export default Mentions

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.interactions')"
:timeline="timeline"
:timeline-name="'mentions'"
/>
</template>
<script src="./mentions.js"></script>

View file

@ -5,13 +5,15 @@ import NavigationPins from 'src/components/navigation/navigation_pins.vue'
import GestureService from '../../services/gesture_service/gesture_service' import GestureService from '../../services/gesture_service/gesture_service'
import { import {
countExtraNotifications, countExtraNotifications,
unseenNotificationsFromStore, unseenNotifications,
} from '../../services/notification_utils/notification_utils' } from '../../services/notification_utils/notification_utils'
import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useAnnouncementsStore } from 'src/stores/announcements.js'
import { useChatsStore } from 'src/stores/chats.js' import { useChatsStore } from 'src/stores/chats.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -53,11 +55,10 @@ const MobileNav = {
}, },
computed: { computed: {
currentUser() { currentUser() {
return this.$store.state.users.currentUser return useUsersStore().currentUser
}, },
unseenNotifications() { unseenNotifications() {
return unseenNotificationsFromStore( return unseenNotifications(
this.$store,
useMergedConfigStore().mergedConfig.notificationVisibility, useMergedConfigStore().mergedConfig.notificationVisibility,
useMergedConfigStore().mergedConfig.ignoreInactionableSeen, useMergedConfigStore().mergedConfig.ignoreInactionableSeen,
) )
@ -144,12 +145,12 @@ const MobileNav = {
} }
}, },
doLogout() { doLogout() {
this.$router.replace('/main/public') useUsersStore().logout()
this.$store.dispatch('logout')
this.hideConfirmLogout() this.hideConfirmLogout()
this.$router.replace('/main/public')
}, },
markNotificationsAsSeen() { markNotificationsAsSeen() {
this.$store.dispatch('markNotificationsAsSeen') useNotificationsStore().markNotificationsAsSeen()
}, },
onScroll({ target: { scrollTop, clientHeight, scrollHeight } }) { onScroll({ target: { scrollTop, clientHeight, scrollHeight } }) {
this.notificationsAtTop = scrollTop > 0 this.notificationsAtTop = scrollTop > 0

View file

@ -1,7 +1,9 @@
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { mapState } from 'pinia'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { usePostStatusStore } from 'src/stores/post_status.js' import { usePostStatusStore } from 'src/stores/post_status.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faPen } from '@fortawesome/free-solid-svg-icons' import { faPen } from '@fortawesome/free-solid-svg-icons'
@ -33,9 +35,6 @@ const MobilePostStatusButton = {
window.removeEventListener('resize', this.handleOSK) window.removeEventListener('resize', this.handleOSK)
}, },
computed: { computed: {
isLoggedIn() {
return !!this.$store.state.users.currentUser
},
isHidden() { isHidden() {
if (HIDDEN_FOR_PAGES.has(this.$route.name)) { if (HIDDEN_FOR_PAGES.has(this.$route.name)) {
return true return true
@ -51,6 +50,7 @@ const MobilePostStatusButton = {
autohideFloatingPostButton() { autohideFloatingPostButton() {
return !!useMergedConfigStore().mergedConfig.autohideFloatingPostButton return !!useMergedConfigStore().mergedConfig.autohideFloatingPostButton
}, },
...mapState(useUsersStore, ['loggedIn']),
}, },
watch: { watch: {
autohideFloatingPostButton: function (isEnabled) { autohideFloatingPostButton: function (isEnabled) {

View file

@ -1,6 +1,6 @@
<template> <template>
<button <button
v-if="isLoggedIn" v-if="loggedIn"
class="MobilePostButton button-default new-status-button" class="MobilePostButton button-default new-status-button"
:class="{ 'hidden': isHidden, 'always-show': isPersistent }" :class="{ 'hidden': isHidden, 'always-show': isPersistent }"
:title="$t('post_status.new_status')" :title="$t('post_status.new_status')"

View file

@ -5,6 +5,7 @@ import Popover from 'src/components/popover/popover.vue'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js' import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronDown } from '@fortawesome/free-solid-svg-icons' import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
@ -405,7 +406,7 @@ const ModerationTools = {
) )
}, },
isAdmin() { isAdmin() {
return this.$store.state.users.currentUser.role === 'admin' return useUsersStore().currentUser.role === 'admin'
}, },
}, },
methods: { methods: {
@ -452,7 +453,7 @@ const ModerationTools = {
}, },
privileged(privilege) { privileged(privilege) {
if (this.isAdmin) return true if (this.isAdmin) return true
return this.$store.state.users.currentUser.privileges.has(privilege) return useUsersStore().currentUser.privileges.has(privilege)
}, },
setTag(tag, value) { setTag(tag, value) {
useAdminSettingsStore().setUsersTags({ useAdminSettingsStore().setUsersTags({

View file

@ -1,14 +1,16 @@
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue' import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
import { useUsersStore } from 'src/stores/users.js'
const MuteCard = { const MuteCard = {
props: ['userId'], props: ['userId'],
computed: { computed: {
user() { user() {
return this.$store.getters.findUser(this.userId) return useUsersStore().findUser(this.userId)
}, },
relationship() { relationship() {
return this.$store.getters.relationship(this.userId) return useUsersStore().relationship(this.userId)
}, },
muted() { muted() {
return this.relationship.muting return this.relationship.muting
@ -30,7 +32,7 @@ const MuteCard = {
}, },
methods: { methods: {
unmuteUser() { unmuteUser() {
this.$store.dispatch('unmuteUser', this.userId) useUsersStore().unmuteUser(this.user.id)
}, },
muteUser() { muteUser() {
this.$refs.timedMuteDialog.optionallyPrompt() this.$refs.timedMuteDialog.optionallyPrompt()

View file

@ -1,5 +1,5 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { mapState } from 'vuex' import { mapState as mapVuexState } from 'vuex'
import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue' import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
@ -14,6 +14,7 @@ import { useChatsStore } from 'src/stores/chats.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -110,29 +111,29 @@ const NavPanel = {
}, },
}, },
computed: { computed: {
...mapPiniaState(useAnnouncementsStore, { ...mapState(useAnnouncementsStore, {
unreadAnnouncementCount: 'unreadAnnouncementCount', unreadAnnouncementCount: 'unreadAnnouncementCount',
supportsAnnouncements: (store) => store.supportsAnnouncements, supportsAnnouncements: (store) => store.supportsAnnouncements,
}), }),
...mapPiniaState(useInstanceCapabilitiesStore, [ ...mapState(useInstanceCapabilitiesStore, [
'pleromaChatMessagesAvailable', 'pleromaChatMessagesAvailable',
'pleromaBookmarkFoldersAvailable', 'pleromaBookmarkFoldersAvailable',
'localBubble', 'localBubble',
]), ]),
...mapPiniaState(useInstanceStore, ['federating']), ...mapState(useInstanceStore, ['federating']),
...mapPiniaState(useInstanceStore, { ...mapState(useInstanceStore, {
privateMode: (store) => store.private, privateMode: (store) => store.private,
}), }),
...mapPiniaState(useSyncConfigStore, { ...mapState(useSyncConfigStore, {
collapsed: (store) => store.prefsStorage.simple.collapseNav, collapsed: (store) => store.prefsStorage.simple.collapseNav,
pinnedItems: (store) => pinnedItems: (store) =>
new Set(store.prefsStorage.collections.pinnedNavItems), new Set(store.prefsStorage.collections.pinnedNavItems),
}), }),
...mapState({ ...mapState(useUsersStore, ['currentUser']),
currentUser: (state) => state.users.currentUser, ...mapVuexState({
followRequestCount: (state) => state.api.followRequests.length, followRequestCount: (state) => state.api.followRequests.length,
}), }),
...mapPiniaState(useChatsStore, ['unreadChatsCount']), ...mapState(useChatsStore, ['unreadChatsCount']),
timelinesItems() { timelinesItems() {
return filterNavigation( return filterNavigation(
Object.entries({ ...TIMELINES }) Object.entries({ ...TIMELINES })

View file

@ -1,11 +1,11 @@
import { mapState as mapPiniaState, mapStores } from 'pinia' import { mapState, mapStores } from 'pinia'
import { mapState } from 'vuex'
import { routeTo } from 'src/components/navigation/navigation.js' import { routeTo } from 'src/components/navigation/navigation.js'
import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useAnnouncementsStore } from 'src/stores/announcements.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faThumbtack } from '@fortawesome/free-solid-svg-icons' import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
@ -44,10 +44,8 @@ const NavigationEntry = {
return this.$store.getters return this.$store.getters
}, },
...mapStores(useAnnouncementsStore), ...mapStores(useAnnouncementsStore),
...mapState({ ...mapState(useUsersStore, ['currentUser']),
currentUser: (state) => state.users.currentUser, ...mapState(useSyncConfigStore, {
}),
...mapPiniaState(useSyncConfigStore, {
pinnedItems: (store) => pinnedItems: (store) =>
new Set(store.prefsStorage.collections.pinnedNavItems), new Set(store.prefsStorage.collections.pinnedNavItems),
}), }),

View file

@ -1,5 +1,5 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { mapState } from 'vuex' import { mapState as mapVuexState } from 'vuex'
import { import {
filterNavigation, filterNavigation,
@ -18,6 +18,7 @@ import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useListsStore } from 'src/stores/lists' import { useListsStore } from 'src/stores/lists'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -58,26 +59,26 @@ const NavPanel = {
getters() { getters() {
return this.$store.getters return this.$store.getters
}, },
...mapPiniaState(useListsStore, { ...mapState(useListsStore, {
lists: getListEntries, lists: getListEntries,
}), }),
...mapPiniaState(useAnnouncementsStore, { ...mapState(useAnnouncementsStore, {
supportsAnnouncements: (store) => store.supportsAnnouncements, supportsAnnouncements: (store) => store.supportsAnnouncements,
}), }),
...mapPiniaState(useBookmarkFoldersStore, { ...mapState(useBookmarkFoldersStore, {
bookmarks: getBookmarkFolderEntries, bookmarks: getBookmarkFolderEntries,
}), }),
...mapPiniaState(useSyncConfigStore, { ...mapState(useSyncConfigStore, {
pinnedItems: (store) => pinnedItems: (store) =>
new Set(store.prefsStorage.collections.pinnedNavItems), new Set(store.prefsStorage.collections.pinnedNavItems),
}), }),
...mapPiniaState(useInstanceStore, ['privateMode', 'federating']), ...mapState(useInstanceStore, ['privateMode', 'federating']),
...mapPiniaState(useInstanceCapabilitiesStore, [ ...mapState(useInstanceCapabilitiesStore, [
'pleromaChatMessagesAvailable', 'pleromaChatMessagesAvailable',
'localBubble', 'localBubble',
]), ]),
...mapState({ ...mapState(useUsersStore, ['currentUser']),
currentUser: (state) => state.users.currentUser, ...mapVuexState({
followRequestCount: (state) => state.api.followRequests.length, followRequestCount: (state) => state.api.followRequests.length,
}), }),
pinnedList() { pinnedList() {

View file

@ -1,5 +1,5 @@
import { mapState } from 'pinia'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import { mapState } from 'vuex'
import Report from 'src/components/report/report.vue' import Report from 'src/components/report/report.vue'
import StatusContent from 'src/components/status_content/status_content.vue' import StatusContent from 'src/components/status_content/status_content.vue'
@ -7,7 +7,7 @@ import Timeago from 'src/components/timeago/timeago.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
import UserLink from 'src/components/user_link/user_link.vue' import UserLink from 'src/components/user_link/user_link.vue'
import UserPopover from 'src/components/user_popover/user_popover.vue' import UserPopover from 'src/components/user_popover/user_popover.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { isStatusNotification } from '../../services/notification_utils/notification_utils_sw.js'
import { import {
highlightClass, highlightClass,
highlightStyle, highlightStyle,
@ -15,8 +15,11 @@ import {
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js'
import { useUsersStore } from 'src/stores/users.js'
import { approveUser, denyUser } from 'src/api/user.js' import { approveUser, denyUser } from 'src/api/user.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'
@ -115,9 +118,6 @@ const Notification = {
useInstanceStore().restrictedNicknames, useInstanceStore().restrictedNicknames,
) )
}, },
getUser(notification) {
return this.$store.state.users.usersObject[notification.from_profile.id]
},
interacted() { interacted() {
this.$emit('interacted') this.$emit('interacted')
}, },
@ -148,16 +148,9 @@ const Notification = {
id: this.user.id, id: this.user.id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}) })
// TODO Fix this
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('markSingleNotificationAsSeen', { useNotificationsStore().markSingleNotificationAsSeen(this.notification.id)
id: this.notification.id,
})
this.$store.dispatch('updateNotification', {
id: this.notification.id,
updater: (notification) => {
notification.type = 'follow'
},
})
this.hideApproveConfirmDialog() this.hideApproveConfirmDialog()
}, },
denyUser() { denyUser() {
@ -172,15 +165,20 @@ const Notification = {
id: this.user.id, id: this.user.id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}).then(() => { }).then(() => {
this.$store.dispatch('dismissNotificationLocal', { useNotificationsStore().dismissNotificationLocal(this.notification.id)
id: this.notification.id, // TODO Fix this
})
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.hideDenyConfirmDialog() this.hideDenyConfirmDialog()
}, },
}, },
computed: { computed: {
status() {
// Used for StatusContent
if (this.notification.status) {
return useStatusesStore().allStatuses.get(this.notification.status.id)
}
},
userClass() { userClass() {
return highlightClass(this.notification.from_profile) return highlightClass(this.notification.from_profile)
}, },
@ -194,19 +192,19 @@ const Notification = {
) )
}, },
user() { user() {
return this.$store.getters.findUser(this.notification.from_profile.id) return useUsersStore().findUser(this.notification.from_profile.id)
}, },
userProfileLink() { userProfileLink() {
return this.generateUserProfileLink(this.user) return this.generateUserProfileLink(this.user)
}, },
targetUser() { targetUser() {
return this.$store.getters.findUser(this.notification.target.id) return useUsersStore().findUser(this.notification.target.id)
}, },
targetUserProfileLink() { targetUserProfileLink() {
return this.generateUserProfileLink(this.targetUser) return this.generateUserProfileLink(this.targetUser)
}, },
needMute() { needMute() {
return this.$store.getters.relationship(this.user.id).muting return useUsersStore().relationship(this.user.id).muting
}, },
isStatusNotification() { isStatusNotification() {
return isStatusNotification(this.notification.type) return isStatusNotification(this.notification.type)
@ -229,9 +227,7 @@ const Notification = {
shouldConfirmDeny() { shouldConfirmDeny() {
return this.mergedConfig.modalOnDenyFollow return this.mergedConfig.modalOnDenyFollow
}, },
...mapState({ ...mapState(useUsersStore, ['currentUser']),
currentUser: (state) => state.users.currentUser,
}),
}, },
} }

View file

@ -6,7 +6,7 @@
<Status <Status
class="Notification panel-body" class="Notification panel-body"
:compact="true" :compact="true"
:statusoid="notification.status" :status-id="notification.status?.id"
@click="interacted" @click="interacted"
/> />
</article> </article>
@ -24,7 +24,7 @@
class="Notification container -muted" class="Notification container -muted"
> >
<small> <small>
<user-link <UserLink
:user="notification.from_profile" :user="notification.from_profile"
:at="false" :at="false"
/> />
@ -57,7 +57,7 @@
<UserAvatar <UserAvatar
class="post-avatar" class="post-avatar"
:compact="true" :compact="true"
:user="notification.from_profile" :user-id="notification.from_profile.id"
/> />
</UserPopover> </UserPopover>
</a> </a>
@ -215,7 +215,7 @@
v-if="notification.type === 'follow' || notification.type === 'follow_request'" v-if="notification.type === 'follow' || notification.type === 'follow_request'"
class="follow-text" class="follow-text"
> >
<user-link <UserLink
class="follow-name" class="follow-name"
:user="notification.from_profile" :user="notification.from_profile"
/> />
@ -249,7 +249,7 @@
v-else-if="notification.type === 'move'" v-else-if="notification.type === 'move'"
class="move-text" class="move-text"
> >
<user-link <UserLink
:user="notification.target" :user="notification.target"
/> />
</div> </div>
@ -261,7 +261,7 @@
<StatusContent <StatusContent
class="status-content" class="status-content"
:compact="!statusExpanded" :compact="!statusExpanded"
:status="notification.status" :status="status"
:collapse="!statusExpanded" :collapse="!statusExpanded"
@click="onContentClick" @click="onContentClick"
/> />

View file

@ -7,17 +7,16 @@ import FaviconService from '../../services/favicon_service/favicon_service.js'
import { import {
ACTIONABLE_NOTIFICATION_TYPES, ACTIONABLE_NOTIFICATION_TYPES,
countExtraNotifications, countExtraNotifications,
filteredNotificationsFromStore, filteredNotifications,
notificationsFromStore, unseenNotifications,
unseenNotificationsFromStore,
} from '../../services/notification_utils/notification_utils.js' } from '../../services/notification_utils/notification_utils.js'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import NotificationFilters from './notification_filters.vue' import NotificationFilters from './notification_filters.vue'
import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useAnnouncementsStore } from 'src/stores/announcements.js'
import { useChatsStore } from 'src/stores/chats.js' import { useChatsStore } from 'src/stores/chats.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -53,7 +52,6 @@ const Notifications = {
data() { data() {
return { return {
showScrollTop: false, showScrollTop: false,
bottomedOut: false,
// How many seen notifications to display in the list. The more there are, // How many seen notifications to display in the list. The more there are,
// the heavier the page becomes. This count is increased when loading // the heavier the page becomes. This count is increased when loading
// older notifications, and cut back to default whenever hitting "Read!". // older notifications, and cut back to default whenever hitting "Read!".
@ -70,14 +68,13 @@ const Notifications = {
return this.minimalMode ? '' : 'panel panel-default' return this.minimalMode ? '' : 'panel panel-default'
}, },
notifications() { notifications() {
return notificationsFromStore(this.$store) return useNotificationsStore().data
}, },
error() { error() {
return this.$store.state.notifications.error return useNotificationsStore().error
}, },
unseenNotifications() { unseenNotifications() {
return unseenNotificationsFromStore( return unseenNotifications(
this.$store,
useMergedConfigStore().mergedConfig.notificationVisibility, useMergedConfigStore().mergedConfig.notificationVisibility,
useMergedConfigStore().mergedConfig.ignoreInactionableSeen, useMergedConfigStore().mergedConfig.ignoreInactionableSeen,
) )
@ -85,18 +82,15 @@ const Notifications = {
filteredNotifications() { filteredNotifications() {
if (this.unseenAtTop) { if (this.unseenAtTop) {
return [ return [
...filteredNotificationsFromStore( ...filteredNotifications(
this.$store,
useMergedConfigStore().mergedConfig.notificationVisibility, useMergedConfigStore().mergedConfig.notificationVisibility,
).filter((n) => this.shouldShowUnseen(n)), ).filter((n) => this.shouldShowUnseen(n)),
...filteredNotificationsFromStore( ...filteredNotifications(
this.$store,
useMergedConfigStore().mergedConfig.notificationVisibility, useMergedConfigStore().mergedConfig.notificationVisibility,
).filter((n) => !this.shouldShowUnseen(n)), ).filter((n) => !this.shouldShowUnseen(n)),
] ]
} else { } else {
return filteredNotificationsFromStore( return filteredNotifications(
this.$store,
useMergedConfigStore().mergedConfig.notificationVisibility, useMergedConfigStore().mergedConfig.notificationVisibility,
this.filterMode, this.filterMode,
) )
@ -127,7 +121,10 @@ const Notifications = {
) )
}, },
loading() { loading() {
return this.$store.state.notifications.loading return useNotificationsStore().fetcher.loading
},
bottomedOut() {
return useNotificationsStore().fetcher.bottomedOut
}, },
noHeading() { noHeading() {
const { layoutType } = useInterfaceStore() const { layoutType } = useInterfaceStore()
@ -224,14 +221,14 @@ const Notifications = {
*/ */
notificationClicked(notification) { notificationClicked(notification) {
const { id } = notification const { id } = notification
this.$store.dispatch('notificationClicked', { id }) useNotificationsStore().notificationClicked(id)
}, },
notificationInteracted(notification) { notificationInteracted(notification) {
const { id } = notification const { id } = notification
this.$store.dispatch('markSingleNotificationAsSeen', { id }) useNotificationsStore().markSingleNotificationAsSeen(id)
}, },
markAsSeen() { markAsSeen() {
this.$store.dispatch('markNotificationsAsSeen') useNotificationsStore().markNotificationsAsSeen()
this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT
}, },
fetchOlderNotifications() { fetchOlderNotifications() {
@ -250,22 +247,7 @@ const Notifications = {
this.seenToDisplayCount = seenCount this.seenToDisplayCount = seenCount
} }
const store = this.$store useNotificationsStore().fetcher.fetchOlder()
const credentials = store.state.users.currentUser.credentials
store.commit('setNotificationsLoading', { value: true })
notificationsFetcher
.fetchAndUpdate({
store,
credentials,
older: true,
})
.then((notifs) => {
store.commit('setNotificationsLoading', { value: false })
if (notifs.length === 0) {
this.bottomedOut = true
}
this.seenToDisplayCount += notifs.length
})
}, },
}, },
} }

View file

@ -1,5 +1,6 @@
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useUsersStore } from 'src/stores/users.js'
import { getToken } from 'src/api/oauth.js' import { getToken } from 'src/api/oauth.js'
@ -15,9 +16,10 @@ const oac = {
clientSecret, clientSecret,
instance: useInstanceStore().server, instance: useInstanceStore().server,
code: this.code, code: this.code,
}).then(({ data: result }) => { }).then(async ({ data: result }) => {
oauthStore.setToken(result.access_token) oauthStore.setToken(result.access_token)
this.$store.dispatch('loginUser', result.access_token)
await useUsersStore().loginUser(result.access_token)
this.$router.push({ name: 'friends' }) this.$router.push({ name: 'friends' })
}) })
} }

View file

@ -1,7 +1,7 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { mapState } from 'vuex'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useUsersStore } from 'src/stores/users.js'
import { resetPassword } from 'src/api/public.js' import { resetPassword } from 'src/api/public.js'
@ -21,13 +21,11 @@ const passwordReset = {
error: null, error: null,
}), }),
computed: { computed: {
...mapState({ ...mapState(useUsersStore, ['loggedIn']),
signedIn: (state) => !!state.users.currentUser, ...mapState(useInstanceStore, ['mailerEnabled']),
}),
...mapPiniaState(useInstanceStore, ['mailerEnabled']),
}, },
created() { created() {
if (this.signedIn) { if (this.loggedIn) {
this.$router.push({ name: 'root' }) this.$router.push({ name: 'root' })
} }
}, },

View file

@ -5,6 +5,7 @@ import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { usePollsStore } from 'src/stores/polls.js' import { usePollsStore } from 'src/stores/polls.js'
import { useUsersStore } from 'src/stores/users.js'
export default { export default {
name: 'Poll', name: 'Poll',
@ -64,7 +65,7 @@ export default {
return useMergedConfigStore().mergedConfig.scaleMfm return useMergedConfigStore().mergedConfig.scaleMfm
}, },
loggedIn() { loggedIn() {
return this.$store.state.users.currentUser return useUsersStore().currentUser
}, },
showResults() { showResults() {
return this.poll.voted || this.expired || !this.loggedIn return this.poll.voted || this.expired || !this.loggedIn

View file

@ -31,6 +31,7 @@ import { useInterfaceStore } from 'src/stores/interface.js'
import { useMediaViewerStore } from 'src/stores/media_viewer.js' import { useMediaViewerStore } from 'src/stores/media_viewer.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js'
import { pollFormToMasto } from 'src/services/poll/poll.service.js' import { pollFormToMasto } from 'src/services/poll/poll.service.js'
@ -570,9 +571,7 @@ const PostStatusForm = {
}, },
// Global stuff // Global stuff
currentUser() { ...mapState(useUsersStore, ['currentUser']),
return this.$store.state.users.currentUser
},
...mapState(useMergedConfigStore, ['mergedConfig']), ...mapState(useMergedConfigStore, ['mergedConfig']),
...mapState(useInterfaceStore, { ...mapState(useInterfaceStore, {
mobileLayout: (store) => store.mobileLayout, mobileLayout: (store) => store.mobileLayout,

View file

@ -1,9 +1,11 @@
import { get } from 'lodash' import { get } from 'lodash'
import { mapState } from 'pinia'
import Modal from 'src/components/modal/modal.vue' import Modal from 'src/components/modal/modal.vue'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import { usePostStatusStore } from 'src/stores/post_status.js' import { usePostStatusStore } from 'src/stores/post_status.js'
import { useUsersStore } from 'src/stores/users.js'
const PostStatusModal = { const PostStatusModal = {
components: { components: {
@ -16,18 +18,16 @@ const PostStatusModal = {
} }
}, },
computed: { computed: {
isLoggedIn() {
return !!this.$store.state.users.currentUser
},
modalActivated() { modalActivated() {
return usePostStatusStore().modalActivated return usePostStatusStore().modalActivated
}, },
isFormVisible() { isFormVisible() {
return this.isLoggedIn && !this.resettingForm && this.modalActivated return this.loggedIn && !this.resettingForm && this.modalActivated
}, },
params() { params() {
return usePostStatusStore().params || {} return usePostStatusStore().params || {}
}, },
...mapState(useUsersStore, ['loggedIn']),
}, },
watch: { watch: {
params(newVal, oldVal) { params(newVal, oldVal) {

View file

@ -1,6 +1,6 @@
<template> <template>
<Modal <Modal
v-if="isLoggedIn && !resettingForm" v-if="loggedIn && !resettingForm"
:is-open="modalActivated" :is-open="modalActivated"
class="post-form-modal-view" class="post-form-modal-view"
@backdrop-clicked="closeModal" @backdrop-clicked="closeModal"

View file

@ -1,22 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const PublicAndExternalTimeline = {
components: {
Timeline,
},
computed: {
timeline() {
return this.$store.state.statuses.timelines.publicAndExternal
},
},
created() {
this.$store.dispatch('startFetchingTimeline', {
timeline: 'publicAndExternal',
})
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'publicAndExternal')
},
}
export default PublicAndExternalTimeline

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.twkn')"
:timeline="timeline"
:timeline-name="'publicAndExternal'"
/>
</template>
<script src="./public_and_external_timeline.js"></script>

View file

@ -1,20 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const PublicTimeline = {
components: {
Timeline,
},
computed: {
timeline() {
return this.$store.state.statuses.timelines.public
},
},
created() {
this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'public')
},
}
export default PublicTimeline

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.public_tl')"
:timeline="timeline"
:timeline-name="'public'"
/>
</template>
<script src="./public_timeline.js"></script>

View file

@ -6,6 +6,8 @@ import { useInterfaceStore } from 'src/stores/interface.js'
import { useLocalConfigStore } from 'src/stores/local_config.js' import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useTimelinesStore } from 'src/stores/timelines.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons' import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons'
@ -26,13 +28,14 @@ const QuickFilterSettings = {
path: 'replyVisibility', path: 'replyVisibility',
value: visibility, value: visibility,
}) })
this.$store.dispatch('queueFlushAll') useTimelinesStore().requireReloadAll()
}, },
openTab(tab) { openTab(tab) {
useInterfaceStore().openSettingsModalTab(tab) useInterfaceStore().openSettingsModalTab(tab)
}, },
}, },
computed: { computed: {
...mapState(useUsersStore, ['loggedIn']),
...mapState(useMergedConfigStore, ['mergedConfig']), ...mapState(useMergedConfigStore, ['mergedConfig']),
...mapState(useInterfaceStore, { ...mapState(useInterfaceStore, {
mobileLayout: (state) => state.layoutType === 'mobile', mobileLayout: (state) => state.layoutType === 'mobile',
@ -53,9 +56,6 @@ const QuickFilterSettings = {
return 'dropdown-item' return 'dropdown-item'
} }
}, },
loggedIn() {
return !!this.$store.state.users.currentUser
},
replyVisibilitySelf: { replyVisibilitySelf: {
get() { get() {
return this.mergedConfig.replyVisibility === 'self' return this.mergedConfig.replyVisibility === 'self'

View file

@ -6,6 +6,7 @@ import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filt
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -35,9 +36,7 @@ const QuickViewSettings = {
...mapState(useInterfaceStore, { ...mapState(useInterfaceStore, {
mobileLayout: (state) => state.layoutType === 'mobile', mobileLayout: (state) => state.layoutType === 'mobile',
}), }),
loggedIn() { ...mapState(useUsersStore, ['loggedIn']),
return !!this.$store.state.users.currentUser
},
conversationDisplay: { conversationDisplay: {
get() { get() {
return this.mergedConfig.conversationDisplay return this.mergedConfig.conversationDisplay

View file

@ -1,3 +1,5 @@
import { useStatusesStore } from 'src/stores/statuses.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
@ -45,7 +47,7 @@ export default {
computed: { computed: {
quotedStatus() { quotedStatus() {
return this.statusId return this.statusId
? this.$store.state.statuses.allStatusesObject[this.statusId] ? useStatusesStore().allStatuses.get(this.statusId)
: undefined : undefined
}, },
shouldDisplayQuote() { shouldDisplayQuote() {
@ -79,8 +81,8 @@ export default {
this.fetchAttempted = true this.fetchAttempted = true
this.fetching = true this.fetching = true
this.$emit('loading', true) this.$emit('loading', true)
this.$store useStatusesStore()
.dispatch('fetchStatus', this.statusId) .fetchStatus(this.statusId)
.then(() => { .then(() => {
this.displayQuote = true this.displayQuote = true
}) })

View file

@ -4,6 +4,7 @@ import Checkbox from 'src/components/checkbox/checkbox.vue'
import Quote from './quote.vue' import Quote from './quote.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useSearchStore } from 'src/stores/search.js'
export default { export default {
components: { components: {
@ -93,8 +94,8 @@ export default {
this.$emit('update:id', notice[3]) this.$emit('update:id', notice[3])
} else if (value) { } else if (value) {
this.loading = true this.loading = true
this.$store useSearchStore()
.dispatch('search', { .search({
q: value, q: value,
resolve: true, resolve: true,
offset: 0, offset: 0,

View file

@ -1,36 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const QuotesTimeline = {
created() {
this.$store.commit('clearTimeline', { timeline: 'quotes' })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'quotes',
statusId: this.statusId,
})
},
components: {
Timeline,
},
computed: {
statusId() {
return this.$route.params.id
},
timeline() {
return this.$store.state.statuses.timelines.quotes
},
},
watch: {
statusId() {
this.$store.commit('clearTimeline', { timeline: 'quotes' })
this.$store.dispatch('startFetchingTimeline', {
timeline: 'quotes',
statusId: this.statusId,
})
},
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'quotes')
},
}
export default QuotesTimeline

View file

@ -1,10 +0,0 @@
<template>
<Timeline
:title="$t('nav.quotes')"
:timeline="timeline"
:timeline-name="'quotes'"
:status-id="statusId"
/>
</template>
<script src='./quotes_timeline.js'></script>

View file

@ -1,14 +1,16 @@
import useVuelidate from '@vuelidate/core' import useVuelidate from '@vuelidate/core'
import { required, requiredIf, sameAs } from '@vuelidate/validators' import { required, requiredIf, sameAs } from '@vuelidate/validators'
import { mapState as mapPiniaState } from 'pinia' import { mapActions, mapState } from 'pinia'
import { mapActions, mapState } from 'vuex'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import TermsOfServicePanel from 'src/components/terms_of_service_panel/terms_of_service_panel.vue' import TermsOfServicePanel from 'src/components/terms_of_service_panel/terms_of_service_panel.vue'
import localeService from '../../services/locale/locale.service.js' import localeService from '../../services/locale/locale.service.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useUsersStore } from 'src/stores/users.js'
import { getCaptcha, register } from 'src/api/public.js'
import { DAY } from 'src/services/date_utils/date_utils.js' import { DAY } from 'src/services/date_utils/date_utils.js'
const registration = { const registration = {
@ -26,6 +28,9 @@ const registration = {
reason: '', reason: '',
language: [''], language: [''],
}, },
signUpPending: false,
signUpErrors: [],
signUpNotice: {},
captcha: {}, captcha: {},
}), }),
components: { components: {
@ -58,7 +63,7 @@ const registration = {
} }
}, },
created() { created() {
if ((!this.registrationOpen && !this.token) || this.signedIn) { if ((!this.registrationOpen && !this.token) || this.loggedIn) {
this.$router.push({ name: 'root' }) this.$router.push({ name: 'root' })
} }
@ -100,7 +105,10 @@ const registration = {
) )
) )
}, },
...mapPiniaState(useInstanceStore, { hasSignUpNotice(state) {
return this.signUpNotice.message
},
...mapState(useInstanceStore, {
registrationOpen: (store) => store.registrationOpen, registrationOpen: (store) => store.registrationOpen,
embeddedToS: (store) => store.embeddedToS, embeddedToS: (store) => store.embeddedToS,
termsOfService: (store) => store.tos, termsOfService: (store) => store.tos,
@ -109,16 +117,49 @@ const registration = {
birthdayRequired: (store) => store.birthdayRequired, birthdayRequired: (store) => store.birthdayRequired,
birthdayMinAge: (store) => store.birthdayMinAge, birthdayMinAge: (store) => store.birthdayMinAge,
}), }),
...mapState({ ...mapState(useUsersStore, ['loggedIn']),
signedIn: (state) => !!state.users.currentUser,
isPending: (state) => state.users.signUpPending,
serverValidationErrors: (state) => state.users.signUpErrors,
signUpNotice: (state) => state.users.signUpNotice,
hasSignUpNotice: (state) => !!state.users.signUpNotice.message,
}),
}, },
methods: { methods: {
...mapActions(['signUp', 'getCaptcha']), ...mapActions(useUsersStore, ['loginUser']),
getCaptcha(store) {
return getCaptcha({
credentials: useOAuthStore().token,
}).then(({ data }) => data)
},
async signUp(userInfo) {
const oauthStore = useOAuthStore()
this.signUpPending = true
this.signUpErrors = []
this.signUpNotice = {}
try {
const token = await oauthStore.ensureAppToken()
const { data } = await register({
credentials: token,
params: { ...userInfo },
})
if (data.access_token) {
this.signUpPending = false
oauthStore.setToken(data.access_token)
await this.loginUser(data.access_token)
return 'ok'
} else {
// Request succeeded, but user cannot login yet.
this.signUpErrors = []
this.signUpNotice = data
return 'request_sent'
}
} catch (e) {
const errors = e.message
this.signUpErrors = errors
this.signUpNotice = {}
throw e
} finally {
this.signUpPending = false
}
},
async submit() { async submit() {
this.user.nickname = this.user.username this.user.nickname = this.user.username
this.user.token = this.token this.user.token = this.token

View file

@ -28,7 +28,7 @@
<input <input
id="sign-up-username" id="sign-up-username"
v-model.trim="v$.user.username.$model" v-model.trim="v$.user.username.$model"
:disabled="isPending" :disabled="signUpPending"
class="input form-control" class="input form-control"
:aria-required="true" :aria-required="true"
:placeholder="$t('registration.username_placeholder')" :placeholder="$t('registration.username_placeholder')"
@ -56,7 +56,7 @@
<input <input
id="sign-up-fullname" id="sign-up-fullname"
v-model.trim="v$.user.fullname.$model" v-model.trim="v$.user.fullname.$model"
:disabled="isPending" :disabled="signUpPending"
class="input form-control" class="input form-control"
:aria-required="true" :aria-required="true"
:placeholder="$t('registration.fullname_placeholder')" :placeholder="$t('registration.fullname_placeholder')"
@ -84,7 +84,7 @@
<input <input
id="email" id="email"
v-model="v$.user.email.$model" v-model="v$.user.email.$model"
:disabled="isPending" :disabled="signUpPending"
class="input form-control" class="input form-control"
type="email" type="email"
:aria-required="accountActivationRequired" :aria-required="accountActivationRequired"
@ -109,7 +109,7 @@
<textarea <textarea
id="bio" id="bio"
v-model="user.bio" v-model="user.bio"
:disabled="isPending" :disabled="signUpPending"
class="input form-control" class="input form-control"
:placeholder="bioPlaceholder" :placeholder="bioPlaceholder"
/> />
@ -126,7 +126,7 @@
<input <input
id="sign-up-password" id="sign-up-password"
v-model="user.password" v-model="user.password"
:disabled="isPending" :disabled="signUpPending"
class="input form-control" class="input form-control"
type="password" type="password"
:aria-required="true" :aria-required="true"
@ -154,7 +154,7 @@
<input <input
id="sign-up-password-confirmation" id="sign-up-password-confirmation"
v-model="user.confirm" v-model="user.confirm"
:disabled="isPending" :disabled="signUpPending"
class="input form-control" class="input form-control"
type="password" type="password"
:aria-required="true" :aria-required="true"
@ -187,7 +187,7 @@
<input <input
id="sign-up-birthday" id="sign-up-birthday"
v-model="user.birthday" v-model="user.birthday"
:disabled="isPending" :disabled="signUpPending"
class="input form-control" class="input form-control"
type="date" type="date"
:max="birthdayRequired ? birthdayMinAttr : undefined" :max="birthdayRequired ? birthdayMinAttr : undefined"
@ -232,7 +232,7 @@
<textarea <textarea
id="reason" id="reason"
v-model="user.reason" v-model="user.reason"
:disabled="isPending" :disabled="signUpPending"
class="input form-control" class="input form-control"
:placeholder="reasonPlaceholder" :placeholder="reasonPlaceholder"
/> />
@ -259,7 +259,7 @@
<input <input
id="captcha-answer" id="captcha-answer"
v-model="captcha.solution" v-model="captcha.solution"
:disabled="isPending" :disabled="signUpPending"
class="input form-control" class="input form-control"
type="text" type="text"
autocomplete="off" autocomplete="off"
@ -285,7 +285,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<button <button
:disabled="isPending" :disabled="signUpPending"
type="submit" type="submit"
class="btn button-default" class="btn button-default"
> >
@ -303,12 +303,12 @@
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
</div> </div>
<div <div
v-if="serverValidationErrors.length" v-if="signUpErrors.length"
class="form-group" class="form-group"
> >
<div class="alert error"> <div class="alert error">
<span <span
v-for="error in serverValidationErrors" v-for="error in signUpErrors"
:key="error" :key="error"
>{{ error }}</span> >{{ error }}</span>
</div> </div>

View file

@ -1,4 +1,5 @@
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useUsersStore } from 'src/stores/users.js'
import { fetchUser } from 'src/api/public.js' import { fetchUser } from 'src/api/public.js'
@ -16,17 +17,14 @@ const RemoteUserResolver = {
id, id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}) })
.then(({ data: externalUser }) => { .then((result) => {
if (externalUser.error) { const { data: externalUser } = result
this.error = true useUsersStore().addNewUsers(result)
} else { const id = externalUser.id
this.$store.commit('addNewUsers', [externalUser]) this.$router.replace({
const id = externalUser.id name: 'external-user-profile',
this.$router.replace({ params: { id },
name: 'external-user-profile', })
params: { id },
})
}
}) })
.catch(() => { .catch(() => {
this.error = true this.error = true

View file

@ -1,6 +1,7 @@
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useUsersStore } from 'src/stores/users.js'
export default { export default {
props: ['user', 'relationship'], props: ['user', 'relationship'],
@ -43,9 +44,9 @@ export default {
}, },
doRemoveUserFromFollowers() { doRemoveUserFromFollowers() {
this.inProgress = true this.inProgress = true
this.$store useUsersStore()
.dispatch('removeUserFromFollowers', this.relationship.id) .removeUserFromFollowers(this.relationship.id)
.then(() => { .finally(() => {
this.inProgress = false this.inProgress = false
}) })
this.hideConfirmRemoveUserFromFollowers() this.hideConfirmRemoveUserFromFollowers()

View file

@ -4,6 +4,10 @@ import Conversation from 'src/components/conversation/conversation.vue'
import FollowCard from 'src/components/follow_card/follow_card.vue' import FollowCard from 'src/components/follow_card/follow_card.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import { useSearchStore } from 'src/stores/search.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch, faSearch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch, faSearch } from '@fortawesome/free-solid-svg-icons'
@ -34,14 +38,14 @@ const Search = {
}, },
computed: { computed: {
users() { users() {
return this.userIds.map((userId) => this.$store.getters.findUser(userId)) return this.userIds.map((userId) => useUsersStore().findUser(userId))
}, },
visibleStatuses() { visibleStatuses() {
const allStatusesObject = this.$store.state.statuses.allStatusesObject const allStatuses = useStatusesStore().allStatuses
return this.statuses.filter( return this.statuses.filter(
(status) => (status) =>
allStatusesObject[status.id] && !allStatusesObject[status.id].deleted, allStatuses.has(status.id) && !allStatuses.get(status.id).deleted,
) )
}, },
}, },
@ -76,8 +80,8 @@ const Search = {
this.lastStatusFetchCount = 0 this.lastStatusFetchCount = 0
} }
this.$store useSearchStore()
.dispatch('search', { .search({
q: query, q: query,
resolve: true, resolve: true,
offset: this.statusesOffset, offset: this.statusesOffset,

View file

@ -119,7 +119,7 @@
> >
<router-link <router-link
class="list-item hashtag" class="list-item hashtag"
:to="{ name: 'tag-timeline', params: { tag: hashtag.name } }" :to="{ name: 'tag-timeline', params: { id: hashtag.name } }"
> >
<span class="name"> <span class="name">
#{{ hashtag.name }} #{{ hashtag.name }}

View file

@ -1,6 +1,8 @@
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue' import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
import { useUsersStore } from 'src/stores/users.js'
const AdminUserCard = { const AdminUserCard = {
props: { props: {
userId: { userId: {
@ -13,7 +15,7 @@ const AdminUserCard = {
}, },
computed: { computed: {
user() { user() {
return this.$store.getters.findUser(this.userId) return useUsersStore().findUser(this.userId)
}, },
isAdmin() { isAdmin() {
return this.user.rights.admin return this.user.rights.admin

View file

@ -1,20 +1,20 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { mapState } from 'vuex'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js' import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useUsersStore } from 'src/stores/users.js'
const SharedComputedObject = () => ({ const SharedComputedObject = () => ({
...mapPiniaState(useMergedConfigStore, ['mergedConfig']), ...mapState(useMergedConfigStore, ['mergedConfig']),
...mapPiniaState(useMergedConfigStore, { ...mapState(useMergedConfigStore, {
expertLevel: (store) => store.mergedConfig.expertLevel, expertLevel: (store) => store.mergedConfig.expertLevel,
}), }),
...mapPiniaState(useAdminSettingsStore, { ...mapState(useAdminSettingsStore, {
adminConfig: (store) => store.config, adminConfig: (store) => store.config,
adminDraft: (store) => store.draft, adminDraft: (store) => store.draft,
}), }),
...mapState({ ...mapState(useUsersStore, {
user: (state) => state.users.currentUser, user: (store) => store.currentUser,
}), }),
}) })

View file

@ -1,6 +1,6 @@
// eslint-disable-next-line no-unused // eslint-disable-next-line no-unused
import { mapState as mapPiniaState } from 'pinia' import { mapState } from 'pinia'
import { Fragment } from 'vue' import { Fragment } from 'vue'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
@ -60,7 +60,7 @@ export default {
return this.$slots.default().findIndex(isWanted) === this.activeIndex return this.$slots.default().findIndex(isWanted) === this.activeIndex
} }
}, },
...mapPiniaState(useInterfaceStore, { ...mapState(useInterfaceStore, {
mobileLayout: (store) => store.layoutType === 'mobile', mobileLayout: (store) => store.layoutType === 'mobile',
}), }),
}, },

View file

@ -20,6 +20,7 @@ import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js' import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -97,10 +98,7 @@ const SettingsModalAdminContent = {
}, },
computed: { computed: {
user() { user() {
return this.$store.state.users.currentUser return useUsersStore().currentUser
},
isLoggedIn() {
return !!this.$store.state.users.currentUser
}, },
open() { open() {
return useInterfaceStore().settingsModalState !== 'hidden' return useInterfaceStore().settingsModalState !== 'hidden'

Some files were not shown because too many files have changed in this diff Show more