Merge remote-tracking branch 'origin/develop' into weight-removal

This commit is contained in:
Henry Jameson 2026-09-04 01:06:10 +03:00
commit d5b1763fe0
90 changed files with 5611 additions and 1868 deletions

1
changelog.d/drafts.fix Normal file
View file

@ -0,0 +1 @@
Fixed drafts creating duplicates on edit instead of updating

View file

@ -1,4 +1,4 @@
import { throttle } from 'lodash'
import { throttle } from 'lodash-es'
import { mapState } from 'pinia'
import { defineAsyncComponent } from 'vue'
@ -49,6 +49,12 @@ export default {
MobilePostStatusButton,
MobileNav,
DesktopNav,
FollowRequestConfirm: defineAsyncComponent(
() =>
import(
'src/components/follow_request_confirm/follow_request_confirm.vue'
),
),
SettingsModal: defineAsyncComponent(
() => import('src/components/settings_modal/settings_modal.vue'),
),

View file

@ -75,6 +75,7 @@
<UpdateNotification />
<GlobalError />
<GlobalNoticeList />
<FollowRequestConfirm v-if="currentUser" />
</div>
</template>

View file

@ -1,4 +1,4 @@
import { snakeCase } from 'lodash'
import { snakeCase } from 'lodash-es'
import { StatusCodeError } from 'src/services/errors/errors'

View file

@ -1,4 +1,4 @@
import { last } from 'lodash'
import { last } from 'lodash-es'
import { paramsString, promisedRequest } from './helpers.js'
import { fetchFriends, MASTODON_STATUS_URL } from './public.js'
@ -43,9 +43,10 @@ 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_APPROVE_USER_URL = (id) =>
export const MASTODON_APPROVE_USER_URL = (id) =>
`/api/v1/follow_requests/${id}/authorize`
const MASTODON_DENY_USER_URL = (id) => `/api/v1/follow_requests/${id}/reject`
export const MASTODON_DENY_USER_URL = (id) =>
`/api/v1/follow_requests/${id}/reject`
const MASTODON_USER_RELATIONSHIPS_URL = ({ id, withSuspended }) =>
`/api/v1/accounts/relationships/${paramsString({ id, withSuspended })}`
export const MASTODON_USER_IN_LISTS = (id) => `/api/v1/accounts/${id}/lists`

View file

@ -1,4 +1,4 @@
import { orderBy, uniqueId } from 'lodash'
import { orderBy, uniqueId } from 'lodash-es'
import ChatMessage from 'src/components/chat_message/chat_message.vue'

View file

@ -1,4 +1,4 @@
import { get, maxBy, minBy, sortBy, throttle } from 'lodash'
import { get, maxBy, minBy, sortBy, throttle } from 'lodash-es'
import { mapState } from 'pinia'
import { nextTick } from 'vue'

View file

@ -64,7 +64,7 @@
</div>
</template>
<script>
import { throttle } from 'lodash'
import { throttle } from 'lodash-es'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js'

View file

@ -1,4 +1,4 @@
import { get, reduce } from 'lodash'
import { get, reduce } from 'lodash-es'
import { mapState } from 'pinia'
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'

View file

@ -1,4 +1,4 @@
import { get } from 'lodash'
import { get } from 'lodash-es'
import { mapState } from 'pinia'
import { defineAsyncComponent } from 'vue'

View file

@ -1,4 +1,4 @@
import { take } from 'lodash'
import { take } from 'lodash-es'
import Popover from 'src/components/popover/popover.vue'
import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'

View file

@ -15,7 +15,7 @@ import { useUsersStore } from 'src/stores/users.js'
export default (data) => {
const emojiCurry = suggestEmoji(data.emoji)
const usersCurry = data.store && suggestUsers(data.store)
const usersCurry = suggestUsers()
return (input, nameKeywordLocalizer) => {
const firstChar = input[0]
if (firstChar === ':' && data.emoji) {

View file

@ -1,4 +1,4 @@
import { chunk, debounce, trim } from 'lodash'
import { chunk, debounce, trim } from 'lodash-es'
import { defineAsyncComponent } from 'vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'

View file

@ -1,96 +1,16 @@
import { defineAsyncComponent } from 'vue'
import { mapActions } from 'pinia'
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import { useFollowRequestsStore } from 'src/stores/follow_requests.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { approveUser, denyUser } from 'src/api/user.js'
const FollowRequestCard = {
props: ['user'],
components: {
BasicUserCard,
ConfirmModal: defineAsyncComponent(
() => import('src/components/confirm_modal/confirm_modal.vue'),
),
},
data() {
return {
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false,
}
},
methods: {
findFollowRequestNotificationId() {
const notif = useNotificationsStore().data.find(
(notif) =>
notif.from_profile.id === this.user.id &&
notif.type === 'follow_request',
)
return notif?.id
},
showApproveConfirmDialog() {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog() {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog() {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog() {
this.showingDenyConfirmDialog = false
},
approveUser() {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove() {
approveUser({
id: this.user.id,
credentials: useOAuthStore().token,
}).then(() => {
const notifId = this.findFollowRequestNotificationId()
useFollowRequestsStore().remove(this.user.id)
notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId)
})
this.hideApproveConfirmDialog()
},
denyUser() {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny() {
denyUser({
id: this.user.id,
credentials: useOAuthStore().token,
}).then(() => {
const notifId = this.findFollowRequestNotificationId()
useFollowRequestsStore().remove(this.user.id)
notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId)
})
this.hideDenyConfirmDialog()
},
},
computed: {
mergedConfig() {
return useMergedConfigStore().mergedConfig
},
shouldConfirmApprove() {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny() {
return this.mergedConfig.modalOnDenyFollow
},
...mapActions(useFollowRequestsStore, ['approve', 'deny']),
},
}

View file

@ -3,39 +3,17 @@
<div class="follow-request-card-content-container">
<button
class="btn button-default"
@click="approveUser"
@click="() => approve(user.id)"
>
{{ $t('user_card.approve') }}
</button>
<button
class="btn button-default"
@click="denyUser"
@click="() => deny(user.id)"
>
{{ $t('user_card.deny') }}
</button>
</div>
<teleport to="#modal">
<ConfirmModal
v-if="showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="doApprove"
@cancelled="hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</ConfirmModal>
<ConfirmModal
v-if="showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="doDeny"
@cancelled="hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</ConfirmModal>
</teleport>
</basic-user-card>
</template>

View file

@ -0,0 +1,38 @@
<template>
<teleport to="#modal">
<div>
<ConfirmModal
v-if="store.showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="store.doApprove"
@cancelled="store.hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</ConfirmModal>
<ConfirmModal
v-if="store.showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="store.doDeny"
@cancelled="store.hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</ConfirmModal>
</div>
</teleport>
</template>
<script setup>
import { computed } from 'vue'
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import { useFollowRequestsStore } from 'src/stores/follow_requests.js'
import { useUsersStore } from 'src/stores/users.js'
const store = useFollowRequestsStore()
const user = computed(() => useUsersStore().findUser(store.tempId))
</script>

View file

@ -1,4 +1,4 @@
import { set, sumBy } from 'lodash'
import { set, sumBy } from 'lodash-es'
import Attachment from 'src/components/attachment/attachment.vue'

View file

@ -1,4 +1,4 @@
import { isEmpty } from 'lodash'
import { isEmpty } from 'lodash-es'
import Checkbox from 'src/components/checkbox/checkbox.vue'

View file

@ -1,4 +1,4 @@
import { debounce } from 'lodash'
import { debounce } from 'lodash-es'
import Checkbox from 'src/components/checkbox/checkbox.vue'

View file

@ -1,4 +1,4 @@
import { debounce } from 'lodash'
import { debounce } from 'lodash-es'
import { mapState } from 'pinia'
import { useMergedConfigStore } from 'src/stores/merged_config.js'

View file

@ -1,4 +1,4 @@
import { last } from 'lodash'
import { last } from 'lodash-es'
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import Popover from 'src/components/popover/popover.vue'

View file

@ -1,4 +1,4 @@
import { get } from 'lodash'
import { get } from 'lodash-es'
import { mapState } from 'pinia'
import { useInstanceStore } from 'src/stores/instance.js'

View file

@ -1,5 +1,4 @@
import { mapState } from 'pinia'
import { defineAsyncComponent } from 'vue'
import { mapActions, mapState } from 'pinia'
import Report from 'src/components/report/report.vue'
import StatusContent from 'src/components/status_content/status_content.vue'
@ -16,13 +15,10 @@ import {
import { useFollowRequestsStore } from 'src/stores/follow_requests.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -58,8 +54,6 @@ const Notification = {
selecting: false,
statusExpanded: false,
unmuted: false,
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false,
}
},
props: ['notification'],
@ -73,9 +67,6 @@ const Notification = {
UserPopover,
UserLink,
ConfirmModal: defineAsyncComponent(
() => import('src/components/confirm_modal/confirm_modal.vue'),
),
},
mounted() {
document.addEventListener('selectionchange', this.onContentSelect)
@ -125,51 +116,7 @@ const Notification = {
toggleMute() {
this.unmuted = !this.unmuted
},
showApproveConfirmDialog() {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog() {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog() {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog() {
this.showingDenyConfirmDialog = false
},
approveUser() {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove() {
approveUser({
id: this.user.id,
credentials: useOAuthStore().token,
})
useFollowRequestsStore().remove(this.user.id)
useNotificationsStore().markSingleNotificationAsSeen(this.notification.id)
this.hideApproveConfirmDialog()
},
denyUser() {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny() {
denyUser({
id: this.user.id,
credentials: useOAuthStore().token,
}).then(() => {
useNotificationsStore().dismissNotificationLocal(this.notification.id)
useFollowRequestsStore().remove(this.user.id)
})
this.hideDenyConfirmDialog()
},
...mapActions(useFollowRequestsStore, ['approve', 'deny']),
},
computed: {
status() {
@ -220,12 +167,6 @@ const Notification = {
scaleMfm() {
return this.mergedConfig.scaleMfm
},
shouldConfirmApprove() {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny() {
return this.mergedConfig.modalOnDenyFollow
},
...mapState(useUsersStore, ['currentUser']),
},
}

View file

@ -226,7 +226,7 @@
<button
class="button-unstyled"
:title="$t('tool_tip.accept_follow_request')"
@click="approveUser()"
@click="() => approve(user.id)"
>
<FAIcon
icon="check"
@ -236,7 +236,7 @@
<button
class="button-unstyled"
:title="$t('tool_tip.reject_follow_request')"
@click="denyUser()"
@click="() => deny(user.id)"
>
<FAIcon
icon="times"
@ -268,28 +268,6 @@
</template>
</div>
</div>
<teleport to="#modal">
<ConfirmModal
v-if="showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="doApprove"
@cancelled="hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</ConfirmModal>
<ConfirmModal
v-if="showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="doDeny"
@cancelled="hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</ConfirmModal>
</teleport>
</article>
</template>

View file

@ -4,7 +4,7 @@ import {
unescape as ldUnescape,
reject,
uniqBy,
} from 'lodash'
} from 'lodash-es'
import { mapActions, mapState } from 'pinia'
import { defineAsyncComponent } from 'vue'

View file

@ -1,4 +1,4 @@
import { get } from 'lodash'
import { get } from 'lodash-es'
import { mapState } from 'pinia'
import Modal from 'src/components/modal/modal.vue'

View file

@ -1,4 +1,4 @@
import { debounce } from 'lodash'
import { debounce } from 'lodash-es'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Quote from './quote.vue'

View file

@ -1,4 +1,4 @@
import { flattenDeep, unescape as ldUnescape } from 'lodash'
import { flattenDeep, unescape as ldUnescape } from 'lodash-es'
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
import { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.js'

View file

@ -1,4 +1,4 @@
import { map, uniqBy } from 'lodash'
import { map, uniqBy } from 'lodash-es'
import Conversation from 'src/components/conversation/conversation.vue'
import FollowCard from 'src/components/follow_card/follow_card.vue'

View file

@ -2,7 +2,7 @@ import Checkbox from 'components/checkbox/checkbox.vue'
import Popover from 'components/popover/popover.vue'
import Select from 'components/select/select.vue'
import StillImage from 'components/still-image/still-image.vue'
import { clone } from 'lodash'
import { clone } from 'lodash-es'
import { defineAsyncComponent } from 'vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'

View file

@ -1,4 +1,4 @@
import { get } from 'lodash'
import { get } from 'lodash-es'
import AttachmentSetting from '../helpers/attachment_setting.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'

View file

@ -1,4 +1,4 @@
import { get } from 'lodash'
import { get } from 'lodash-es'
import AttachmentSetting from '../helpers/attachment_setting.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'

View file

@ -1,4 +1,4 @@
import { get } from 'lodash'
import { get } from 'lodash-es'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import AttachmentSetting from '../helpers/attachment_setting.vue'

View file

@ -1,4 +1,4 @@
import { clone } from 'lodash'
import { clone } from 'lodash-es'
import Attachment from 'src/components/attachment/attachment.vue'
import MediaUpload from 'src/components/media_upload/media_upload.vue'

View file

@ -1,4 +1,4 @@
import { cloneDeep, get, isEqual, set } from 'lodash'
import { cloneDeep, get, isEqual, set } from 'lodash-es'
import DraftButtons from './draft_buttons.vue'
import LocalSettingIndicator from './local_setting_indicator.vue'

View file

@ -1,4 +1,4 @@
import { cloneDeep, isEqual } from 'lodash'
import { cloneDeep, isEqual } from 'lodash-es'
import { mapActions, mapState } from 'pinia'
import { defineAsyncComponent } from 'vue'

View file

@ -1,4 +1,4 @@
import { cloneDeep } from 'lodash'
import { cloneDeep } from 'lodash-es'
import { mapActions, mapState } from 'pinia'
import { v4 as uuidv4 } from 'uuid'

View file

@ -1,4 +1,4 @@
import { get, map, reject } from 'lodash'
import { get, map, reject } from 'lodash-es'
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
import BlockCard from 'src/components/block_card/block_card.vue'

View file

@ -1,4 +1,4 @@
import { get, set, throttle, unset } from 'lodash'
import { get, set, throttle, unset } from 'lodash-es'
import {
computed,
getCurrentInstance,

View file

@ -1,4 +1,4 @@
import { flattenDeep, throttle } from 'lodash'
import { flattenDeep, throttle } from 'lodash-es'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import ColorInput from 'src/components/color_input/color_input.vue'

View file

@ -110,7 +110,7 @@ const SideDrawer = {
hideSitename: (store) => store.instanceIdentity.hideSitename,
}),
...mapState(useChatsStore, ['unreadChatsCount']),
...mapState(useDraftsStore, ['draftCount']),
...mapState(useDraftsStore, ['draftsCount']),
},
methods: {
toggleDrawer() {

View file

@ -269,10 +269,10 @@
icon="file-pen"
/> {{ $t('nav.drafts') }}
<span
v-if="draftCount"
v-if="draftsCount"
class="badge -neutral"
>
{{ draftCount }}
{{ draftsCount }}
</span>
</router-link>
</li>

View file

@ -1,4 +1,4 @@
import { groupBy, map } from 'lodash'
import { groupBy, map } from 'lodash-es'
import { mapState } from 'pinia'
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'

View file

@ -1,4 +1,4 @@
import { debounce, throttle } from 'lodash'
import { debounce, throttle } from 'lodash-es'
import { mapState } from 'pinia'
import Conversation from 'src/components/conversation/conversation.vue'

View file

@ -3,7 +3,7 @@ import {
escape as ldEscape,
unescape as ldUnescape,
merge,
} from 'lodash'
} from 'lodash-es'
import { mapState } from 'pinia'
import { defineAsyncComponent } from 'vue'

View file

@ -1,4 +1,4 @@
import { get } from 'lodash'
import { get } from 'lodash-es'
import FollowCard from 'src/components/follow_card/follow_card.vue'
import List from 'src/components/list/list.vue'

View file

@ -1,4 +1,4 @@
import { shuffle } from 'lodash'
import { shuffle } from 'lodash-es'
import { useInstanceStore } from 'src/stores/instance.js'
import { useOAuthStore } from 'src/stores/oauth.js'

View file

@ -1742,6 +1742,7 @@
"approve_confirm_accept_button": "Approve",
"approve_confirm_cancel_button": "Do not approve",
"approve_confirm": "Do you want to approve {user}'s follow request?",
"approve_error": "Failure approving follow request: {error}",
"block": "Block",
"blocked": "Blocked!",
"block_confirm_title": "Block confirmation",
@ -1754,6 +1755,7 @@
"deny_confirm_accept_button": "Deny",
"deny_confirm_cancel_button": "Do not deny",
"deny_confirm": "Do you want to deny {user}'s follow request?",
"deny_error": "Failure denying follow request: {error}",
"edit_profile": "Edit profile",
"favorites": "Favorites",
"follow": "Follow",

View file

@ -7,7 +7,7 @@
// sed -i -e "s/'//gm" -e 's/"/\\"/gm' -re 's/^( +)(.+?): ((.+?))?(,?)(\{?)$/\1"\2": "\4"/gm' -e 's/\"\{\"/{/g' -e 's/,"$/",/g' file.json
// There's only problem that apostrophe character ' gets replaced by \\ so you have to fix it manually, sorry.
import { isEqual } from 'lodash'
import { isEqual } from 'lodash-es'
import enMessages from './en.json'
import { langCodeToJsonName, languages } from './languages.js'

View file

@ -1,4 +1,4 @@
import { cloneDeep, get, set } from 'lodash'
import { cloneDeep, get, set } from 'lodash-es'
import { storage } from './storage.js'

View file

@ -1,4 +1,4 @@
import { get } from 'lodash'
import { get } from 'lodash-es'
const browserLocale = (navigator.language || 'en').split('-')[0]

View file

@ -1 +0,0 @@
export default {}

View file

@ -1,4 +1,4 @@
import { kebabCase } from 'lodash'
import { kebabCase } from 'lodash-es'
const propsToNative = (props) =>
Object.keys(props).reduce((acc, cur) => {

View file

@ -1,4 +1,4 @@
import { find, reduce } from 'lodash'
import { find, reduce } from 'lodash-es'
export const replaceWord = (str, toReplace, replacement) => {
return str.slice(0, toReplace.start) + replacement + str.slice(toReplace.end)

View file

@ -1,4 +1,4 @@
import { isFunction } from 'lodash'
import { isFunction } from 'lodash-es'
const getComponentOptions = (Component) =>
isFunction(Component) ? Component.options : Component

View file

@ -1,6 +1,6 @@
import { parseLinkHeader } from '@web3-storage/parse-link-header'
import escapeHtml from 'escape-html'
import { unescape as lodashUnescape } from 'lodash'
import { unescape as lodashUnescape } from 'lodash-es'
import punycode from 'punycode.js'
import { fileType } from '../file_type/file_type.service.js'

View file

@ -1,4 +1,4 @@
import { capitalize } from 'lodash'
import { capitalize } from 'lodash-es'
function humanizeErrors(errors) {
return Object.entries(errors).reduce((errs, [k, val]) => {

View file

@ -1,4 +1,4 @@
import { unescape as ldUnescape } from 'lodash'
import { unescape as ldUnescape } from 'lodash-es'
import { getTagName } from './utility.service.js'

View file

@ -1,5 +1,5 @@
import ISO6391 from 'iso-639-1'
import { map } from 'lodash'
import { map } from 'lodash-es'
import languagesObject from '../../i18n/messages'

View file

@ -1,4 +1,4 @@
import { map } from 'lodash'
import { map } from 'lodash-es'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'

View file

@ -1,6 +1,6 @@
import sum from 'hash-sum'
import localforage from 'localforage'
import { chunk, throttle } from 'lodash'
import { chunk, throttle } from 'lodash-es'
import { getCssRules } from '../theme_data/css_utils.js'
import { getEngineChecksum, init } from '../theme_data/theme_data_3.service.js'

View file

@ -1,4 +1,4 @@
import { flattenDeep } from 'lodash'
import { flattenDeep } from 'lodash-es'
export const deserializeShadow = (string) => {
const modes = [

View file

@ -1,4 +1,4 @@
import { sortBy } from 'lodash'
import { sortBy } from 'lodash-es'
// "Unrolls" a tree structure of item: { parent: { ...item2, parent: { ...item3, parent: {...} } }}
// into an array [item2, item3] for iterating

View file

@ -1,6 +1,6 @@
import { brightness, convert } from 'chromatism'
import sum from 'hash-sum'
import { flattenDeep, sortBy } from 'lodash'
import { flattenDeep, sortBy } from 'lodash-es'
import {
alphaBlend,

View file

@ -1,4 +1,4 @@
import { includes } from 'lodash'
import { includes } from 'lodash-es'
const generateProfileLink = (id, screenName, restrictedNicknames) => {
const complicated =

View file

@ -1,4 +1,4 @@
import { cloneDeep, differenceWith, get, isEqual, set } from 'lodash'
import { cloneDeep, differenceWith, get, isEqual, set } from 'lodash-es'
import { defineStore } from 'pinia'
import { useOAuthStore } from 'src/stores/oauth.js'

View file

@ -1,4 +1,4 @@
import { find, remove } from 'lodash'
import { find, remove } from 'lodash-es'
import { defineStore } from 'pinia'
import { useOAuthStore } from 'src/stores/oauth.js'

View file

@ -1,4 +1,4 @@
import { orderBy, sumBy } from 'lodash'
import { orderBy, sumBy } from 'lodash-es'
import { defineStore } from 'pinia'
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'

View file

@ -52,12 +52,8 @@ export const useDraftsStore = defineStore('drafts', {
},
},
actions: {
async abandonDraft(id) {
this.drafts.delete(id)
await deleteDraftFromStorage([id])
},
async loadDrafts() {
const currentData = await getStorageData()
const currentData = (await getStorageData()) ?? {}
this.drafts = new Map(Object.entries(currentData))
},
async addOrSaveDraft(draft) {
@ -67,9 +63,13 @@ export const useDraftsStore = defineStore('drafts', {
await saveDraftToStorage(draftWithId)
return id
},
async abandonAllDrafts(store) {
async abandonDraft(id) {
this.drafts.delete(id)
await deleteDraftFromStorage([id])
},
async abandonAllDrafts() {
const ids = [...this.drafts.keys()]
ids.forEach((id) => this.abandonDraft(id))
ids.forEach((id) => this.drafts.delete(id))
await deleteDraftFromStorage(ids)
},
},

View file

@ -1,4 +1,4 @@
import { merge } from 'lodash'
import { merge } from 'lodash-es'
import { defineStore } from 'pinia'
import { useInstanceStore } from 'src/stores/instance.js'

View file

@ -24,8 +24,6 @@ const followRequestFetcher = ({ credentials }) => {
const startFetching = () => {
if (interval.value) throw new Error('Interval already exists!')
fetchAndUpdate()
interval.value = promiseInterval(fetchAndUpdate, 10000)
}

View file

@ -118,8 +118,6 @@ const notificationsFetcher = (credentials) => {
const startFetching = () => {
if (interval.value) throw new Error('Interval already exists!')
fetchAndUpdate()
interval.value = promiseInterval(fetchAndUpdate, 10000)
}

View file

@ -1,12 +1,20 @@
import { defineStore } from 'pinia'
import followRequestFetcher from 'src/stores/fetchers/follow_requests.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { approveUser, denyUser } from 'src/api/user.js'
export const useFollowRequestsStore = defineStore('followRequests', {
state: () => ({
fetcher: null,
requests: new Map(),
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false,
tempId: null,
}),
getters: {
followRequestsCount(state) {
@ -14,8 +22,9 @@ export const useFollowRequestsStore = defineStore('followRequests', {
},
},
actions: {
// Fetcher stuff
startFetching() {
if (this.fetcher) throw 'Fetcher already exists!'
if (this.fetcher) throw new Error('Fetcher already exists!')
this.fetcher = followRequestFetcher({
credentials: useOAuthStore().token,
@ -24,14 +33,99 @@ export const useFollowRequestsStore = defineStore('followRequests', {
this.fetcher.startFetching()
},
stopFetching() {
if (!this.fetcher) throw "Fetcher doesn't exists!"
this.fetcher.stopFetching(), (this.fetcher = null)
if (!this.fetcher) throw new Error("Fetcher doesn't exists!")
this.fetcher.stopFetching()
this.fetcher = null
},
setFollowRequests(requests) {
this.requests = new Map(requests.map((user) => [user.id, user]))
},
remove(id) {
this.requests.delete(id)
// Confirm dialogs
showApproveConfirmDialog(id) {
this.showingApproveConfirmDialog = true
this.tempId = id
},
showDenyConfirmDialog(id) {
this.showingDenyConfirmDialog = true
this.tempId = id
},
hideApproveConfirmDialog() {
this.showingApproveConfirmDialog = false
this.tempId = null
},
hideDenyConfirmDialog() {
this.showingDenyConfirmDialog = false
this.tempId = null
},
// Dialog/Instant fork
approve(id) {
if (useMergedConfigStore().mergedConfig.modalOnApproveFollow) {
this.showApproveConfirmDialog(id)
} else {
this.doApprove(id)
}
},
deny(id) {
if (useMergedConfigStore().mergedConfig.modalOnDenyFollow) {
this.showDenyConfirmDialog(id)
} else {
this.doDeny(id)
}
},
// Actual calls
async doApprove(userId) {
const id = userId ?? this.tempId
this.hideApproveConfirmDialog()
try {
await approveUser({
id,
credentials: useOAuthStore().token,
})
const notifId = this.findFollowRequestNotificationId(id)
notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId)
this.requests.delete(id)
} catch (error) {
useInterfaceStore().pushGlobalNotice({
messageKey: 'user_card.approve_error',
messageArgs: { error },
level: 'error',
})
}
},
async doDeny(userId) {
const id = userId ?? this.tempId
this.hideDenyConfirmDialog()
try {
await denyUser({
id,
credentials: useOAuthStore().token,
})
const notifId = this.findFollowRequestNotificationId(id)
notifId && useNotificationsStore().markSingleNotificationAsSeen(notifId)
this.requests.delete(id)
} catch (error) {
useInterfaceStore().pushGlobalNotice({
messageKey: 'user_card.deny_error',
messageArgs: { error },
level: 'error',
})
}
},
// Utility
findFollowRequestNotificationId(userId) {
const notif = useNotificationsStore().data.find(
(notif) =>
notif.from_profile.id === userId && notif.type === 'follow_request',
)
return notif?.id
},
},
})

View file

@ -1,4 +1,4 @@
import { set } from 'lodash'
import { set } from 'lodash-es'
import { defineStore } from 'pinia'
import {

View file

@ -1,5 +1,8 @@
import { defineStore } from 'pinia'
import { useShoutStore } from 'src/stores/shout.js'
import { useUsersStore } from 'src/stores/users.js'
const defaultState = {
postFormats: [],
mailerEnabled: false,
@ -38,6 +41,13 @@ export const useInstanceCapabilitiesStore = defineStore(
}
this[capability] = value
if (
capability === 'shoutAvailable' &&
useUsersStore().currentUser?.token
) {
useShoutStore().initializeSocket()
}
},
},
},

View file

@ -1,4 +1,4 @@
import { find, remove } from 'lodash'
import { find, remove } from 'lodash-es'
import { defineStore } from 'pinia'
import { useOAuthStore } from 'src/stores/oauth.js'

View file

@ -1,4 +1,4 @@
import { cloneDeep, set } from 'lodash'
import { cloneDeep, set } from 'lodash-es'
import { defineStore } from 'pinia'
import {

View file

@ -1,4 +1,4 @@
import { merge } from 'lodash'
import { merge } from 'lodash-es'
import { defineStore } from 'pinia'
import { useOAuthStore } from 'src/stores/oauth.js'

View file

@ -1,4 +1,4 @@
import { get, set } from 'lodash'
import { get, set } from 'lodash-es'
import { defineStore } from 'pinia'
import { useOAuthStore } from 'src/stores/oauth.js'

View file

@ -10,7 +10,6 @@ export const useShoutStore = defineStore('shout', {
messages: [],
channel: { state: '' },
joined: false,
token: null,
socket: null,
}),
getters: {
@ -20,7 +19,7 @@ export const useShoutStore = defineStore('shout', {
initializeSocket() {
if (this.token === null) return
if (!useInstanceCapabilitiesStore().shoutAvailable) return
if (this.socket !== null) throw new Error('Shout socket already exist!')
if (this.socket !== null) return
this.socket = new Socket('/socket', { params: { token: this.token } })
this.socket.connect()

View file

@ -12,7 +12,7 @@ import {
take,
uniqWith,
unset,
} from 'lodash'
} from 'lodash-es'
import { defineStore } from 'pinia'
import { v4 as uuidv4 } from 'uuid'
import { toRaw } from 'vue'

View file

@ -1,4 +1,4 @@
import { first, last } from 'lodash'
import { first, last } from 'lodash-es'
import { defineStore } from 'pinia'
import timelineFetcher from 'src/stores/fetchers/timeline_fetcher.js'

View file

@ -5,7 +5,7 @@ import {
groupBy,
isEqual,
last,
} from 'lodash'
} from 'lodash-es'
import { defineStore } from 'pinia'
import { toRaw } from 'vue'

View file

@ -1,5 +1,5 @@
import Cookies from 'js-cookie'
import { last } from 'lodash'
import { last } from 'lodash-es'
import { defineStore } from 'pinia'
import { useAnnouncementsStore } from 'src/stores/announcements.js'
@ -706,7 +706,7 @@ export const useUsersStore = defineStore('users', {
useOAuthStore().clearToken()
}
if (error.tatusCode === 401) {
if (error.statusCode === 401) {
throw new Error('Wrong username or password', error)
} else {
throw new Error('An error occurred, please try again', error)
@ -717,6 +717,7 @@ export const useUsersStore = defineStore('users', {
},
logout() {
const oauth = useOAuthStore()
const locked = this.currentUser.locked
// Pause fetching
useNotificationsStore().pause()
@ -727,7 +728,7 @@ export const useUsersStore = defineStore('users', {
useListsStore().stopFetching()
useBookmarkFoldersStore().stopFetching()
useChatsStore().stopFetching()
if (this.currentUser.locked) {
if (locked) {
useFollowRequestsStore().stopFetching()
}
@ -788,7 +789,9 @@ export const useUsersStore = defineStore('users', {
useListsStore().startFetching()
useBookmarkFoldersStore().startFetching()
useChatsStore().startFetching()
useFollowRequestsStore().startFetching()
if (locked) {
useFollowRequestsStore().startFetching()
}
})
.finally(() => {
useNotificationsStore().resume()

View file

@ -0,0 +1,220 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { useDraftsStore } from 'src/stores/drafts.js'
import { storage } from 'src/lib/storage.js'
describe('Drafts store', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
// Localforage does something weird that resets get/setItem and breaks
// mocking if we just spy one them without overriding implementation
vi.spyOn(storage, 'setItem').mockImplementation(() => ({}))
vi.spyOn(storage, 'getItem').mockImplementation(() => ({}))
})
afterEach(() => {
vi.resetAllMocks()
})
describe('Getters', () => {
it('draftsCount returns total number of drafts', async () => {
const store = useDraftsStore()
await store.addOrSaveDraft({ id: 1, status: 'draft' })
await store.addOrSaveDraft({ id: 2, status: 'draft' })
await store.addOrSaveDraft({ id: 3, status: 'draft' })
expect(store).to.have.property('draftsCount', 3)
})
it('draftsArray returns array of drafts', async () => {
const store = useDraftsStore()
await store.addOrSaveDraft({ id: 1, status: 'draft' })
await store.addOrSaveDraft({ id: 2, status: 'draft' })
await store.addOrSaveDraft({ id: 3, status: 'draft' })
expect(store.draftsArray).to.have.length(3)
expect(store.draftsArray).to.be.an('Array')
expect(store.draftsArray).to.have.deep.members([
{ id: 1, status: 'draft' },
{ id: 2, status: 'draft' },
{ id: 3, status: 'draft' },
])
})
it('draftsByTypeAndRefId', async () => {
const store = useDraftsStore()
await store.addOrSaveDraft({
id: 1,
type: 'edit',
refId: 'e1',
status: 'draft',
})
await store.addOrSaveDraft({
id: 2,
type: 'reply',
refId: 'r1',
status: 'draft',
})
await store.addOrSaveDraft({ id: 3, status: 'draft' })
await store.addOrSaveDraft({
id: 4,
type: 'edit',
refId: 'e2',
status: 'draft',
})
await store.addOrSaveDraft({
id: 5,
type: 'reply',
refId: 'r2',
status: 'draft',
})
expect(store.draftsByTypeAndRefId).to.be.a('function')
expect(store.draftsByTypeAndRefId('edit', 'e1')).to.eql([
{ id: 1, type: 'edit', refId: 'e1', status: 'draft' },
])
expect(store.draftsByTypeAndRefId('reply', 'r1')).to.eql([
{ id: 2, type: 'reply', refId: 'r1', status: 'draft' },
])
})
})
describe('Actions', () => {
describe('loadDrafts', () => {
it('should load drafts from storage and populate cache', async () => {
const store = useDraftsStore()
storage.getItem.mockResolvedValueOnce({
a: { id: 'a', status: 'draft' },
b: { id: 'b', status: 'draft' },
c: { id: 'c', status: 'draft' },
})
await store.loadDrafts()
expect(store.drafts.get('a')).to.have.property('status', 'draft')
expect(store.drafts.get('b')).to.have.property('status', 'draft')
expect(store.drafts.get('c')).to.have.property('status', 'draft')
expect(storage.getItem).to.have.been.calledOnce
expect(storage.getItem).to.have.been.calledWith('pleroma-fe-drafts')
expect(storage.setItem).to.have.not.been.called
})
it('should handle case where there is no local draft storage yet', async () => {
const store = useDraftsStore()
storage.getItem.mockResolvedValueOnce(null)
await store.loadDrafts()
expect(store.drafts).to.have.property('size', 0)
expect(storage.getItem).to.have.been.calledOnce
expect(storage.getItem).to.have.been.calledWith('pleroma-fe-drafts')
expect(storage.setItem).to.have.not.been.called
})
})
describe('addOrSaveDraft', () => {
it('create draft', async () => {
const store = useDraftsStore()
vi.setSystemTime(new Date(859586400000))
const id = await store.addOrSaveDraft({ status: 'draft' })
expect(store.drafts).to.have.property('size', 1)
expect(id).to.eql('859586400000')
expect(store.drafts.get(id)).to.have.property('status', 'draft')
expect(storage.getItem).to.have.been.calledOnce
expect(storage.setItem).to.have.been.calledOnce
expect(storage.setItem).to.have.been.calledWith('pleroma-fe-drafts', {
[id]: {
id,
status: 'draft',
},
})
})
it('update draft', async () => {
const store = useDraftsStore()
await store.addOrSaveDraft({ id: '1', status: 'draft' })
expect(store.drafts.get('1')).to.have.property('status', 'draft')
await store.addOrSaveDraft({ id: '1', status: 'updated' })
expect(store.drafts.get('1')).to.have.property('status', 'updated')
expect(storage.getItem).to.have.been.calledTwice
expect(storage.setItem).to.have.been.calledTwice
expect(storage.setItem).to.have.been.calledWith('pleroma-fe-drafts', {
1: {
id: '1',
status: 'draft',
},
})
})
})
describe('abandonDraft', () => {
it('should remove draft from storage and cache', async () => {
const store = useDraftsStore()
store.drafts.set('a', { id: 'a', status: 'draft' })
store.drafts.set('b', { id: 'b', status: 'draft' })
store.drafts.set('c', { id: 'c', status: 'draft' })
storage.getItem.mockResolvedValueOnce({
a: { id: 'a', status: 'draft' },
b: { id: 'b', status: 'draft' },
c: { id: 'c', status: 'draft' },
})
await store.abandonDraft('b')
expect(store.drafts.get('a')).to.have.property('status', 'draft')
expect(store.drafts.get('b')).to.be.undefined
expect(store.drafts.get('c')).to.have.property('status', 'draft')
expect(storage.getItem).to.have.been.calledOnce
expect(storage.setItem).to.have.been.calledOnce
expect(storage.setItem).to.have.been.calledWith('pleroma-fe-drafts', {
a: {
id: 'a',
status: 'draft',
},
c: {
id: 'c',
status: 'draft',
},
})
})
})
describe('abandonAllDrafts', () => {
it('should remove draft from storage and cache', async () => {
const store = useDraftsStore()
store.drafts.set('a', { id: 'a', status: 'draft' })
store.drafts.set('b', { id: 'b', status: 'draft' })
store.drafts.set('c', { id: 'c', status: 'draft' })
storage.getItem.mockResolvedValueOnce({
a: { id: 'a', status: 'draft' },
b: { id: 'b', status: 'draft' },
c: { id: 'c', status: 'draft' },
})
await store.abandonAllDrafts()
expect(store.drafts.get('a')).to.be.undefined
expect(store.drafts.get('b')).to.be.undefined
expect(store.drafts.get('c')).to.be.undefined
expect(storage.getItem).to.have.been.calledOnce
expect(storage.setItem).to.have.been.calledOnce
expect(storage.setItem).to.have.been.calledWith('pleroma-fe-drafts', {})
})
})
})
})

View file

@ -0,0 +1,350 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { useFollowRequestsStore } from 'src/stores/follow_requests.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js'
import { useUsersStore } from 'src/stores/users.js'
import * as USER_API from 'src/api/user.js'
const mockMastoAPIUser = ({
screen_name = 'u1',
name = 'user1',
url = 'http://localhost/u1',
id = 'u1',
} = {}) => ({
id,
acct: screen_name,
display_name: name,
fields: [],
avatar: '',
url,
pleroma: {
emoji_reactions: [],
},
})
describe('Follow Requests store', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
vi.resetAllMocks()
})
describe('Getters', () => {
it('followRequestsCount returns total number of follow requests', async () => {
const store = useFollowRequestsStore()
store.requests = new Map([
['1', {}],
['2', {}],
])
expect(store).to.have.property('followRequestsCount', 2)
})
})
describe('Actions', () => {
describe('Fetcher stuff', () => {
it('startFetching should initialize fetcher and fetch some data', async () => {
const store = useFollowRequestsStore()
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify([mockMastoAPIUser()]), {
headers: { 'Content-Type': 'application/json' },
}),
)
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify([mockMastoAPIUser()]), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
store.startFetching()
expect(store.fetcher).to.not.be.null
await vi.advanceTimersToNextTimerAsync()
expect(mockFetch).to.have.been.calledOnce
await vi.advanceTimersToNextTimerAsync()
expect(mockFetch).to.have.been.calledTwice
expect(useUsersStore().findUser('u1')).to.not.be.undefined
expect(store.requests.get('u1')).to.not.be.undefined
})
it('stopFetching should stop and remove the fetcher', async () => {
const store = useFollowRequestsStore()
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify([mockMastoAPIUser()]), {
headers: { 'Content-Type': 'application/json' },
}),
)
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify([mockMastoAPIUser()]), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
store.startFetching()
expect(store.fetcher).to.not.be.null
store.stopFetching()
expect(store.fetcher).to.be.null
})
})
describe.each(['Approve', 'Deny'])('%s', (intent) => {
const doCall = `do${intent}`
const apiCall = USER_API[`MASTODON_${intent.toUpperCase()}_USER_URL`]
const forkCall = intent.toLowerCase()
const forkProperty = `modalOn${intent}Follow`
const modalProperty = `showing${intent}ConfirmDialog`
const modalCalls = ['show', 'hide'].map(
(vis) => `${vis}${intent}ConfirmDialog`,
)
describe('Dialog calls', () => {
it(`${modalCalls[0]} should show dialog and set tempId`, async () => {
const store = useFollowRequestsStore()
await store[modalCalls[0]]('u13')
expect(store).to.have.property(modalProperty, true)
expect(store).to.have.property('tempId', 'u13')
})
it(`${modalCalls[1]} should hide dialog and clear tempId`, async () => {
const store = useFollowRequestsStore()
await store[modalCalls[1]]()
expect(store).to.have.property(modalProperty, false)
expect(store).to.have.property('tempId', null)
})
})
describe('Fork calls', () => {
it(`Should call ${doCall} if confirmations are disabled (${forkProperty} = false)`, async () => {
const store = useFollowRequestsStore()
const modalSpy = vi
.spyOn(store, modalCalls[0])
.mockImplementation(() => ({}))
const apiSpy = vi.spyOn(store, doCall).mockImplementation(() => ({}))
useMergedConfigStore().mergedConfig = { [forkProperty]: false }
await store[forkCall]('u23')
expect(modalSpy).to.not.have.been.called
expect(apiSpy).to.have.been.calledOnce
expect(apiSpy).to.have.been.calledWith('u23')
})
it(`Should call ${modalCalls[0]} if confirmations are enabled (${forkProperty} = true)`, async () => {
const store = useFollowRequestsStore()
const modalSpy = vi
.spyOn(store, modalCalls[0])
.mockImplementation(() => ({}))
const apiSpy = vi.spyOn(store, doCall).mockImplementation(() => ({}))
useMergedConfigStore().mergedConfig = { [forkProperty]: true }
await store[forkCall]('u23')
expect(modalSpy).to.have.been.called
expect(apiSpy).to.not.have.been.calledOnce
})
})
describe('Actual call', () => {
it('Should hide popover', async () => {
const store = useFollowRequestsStore()
const spy = vi.spyOn(store, modalCalls[1])
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify('ok'), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
await store[doCall]()
expect(spy).to.have.been.calledOnce
})
it('Should call API', async () => {
const store = useFollowRequestsStore()
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify('ok'), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
await store[doCall]('u99')
expect(mockFetch).to.have.been.calledOnce
expect(mockFetch.mock.calls[0][0]).to.eql(apiCall('u99'))
})
it('Should mark notification as seen', async () => {
const store = useFollowRequestsStore()
const mockFetch = vi.fn()
store.findFollowRequestNotificationId = vi.fn()
store.findFollowRequestNotificationId.mockReturnValue('n91')
const spy = vi.spyOn(
useNotificationsStore(),
'markSingleNotificationAsSeen',
)
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify('ok'), {
headers: { 'Content-Type': 'application/json' },
}),
)
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify('ok'), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
await store[doCall]('u99')
expect(spy).to.have.been.calledOnce
expect(spy).to.have.been.calledWith('n91')
})
it('Should fallback to tempId if no id is provided', async () => {
const store = useFollowRequestsStore()
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify('ok'), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
store.tempId = 'u80'
await store[doCall]()
expect(mockFetch).to.have.been.calledOnce
expect(mockFetch.mock.calls[0][0]).to.eql(apiCall('u80'))
})
it('Should remove request from cache', async () => {
const store = useFollowRequestsStore()
const mockFetch = vi.fn()
store.requests.set('u95', { id: 'u95' })
store.requests.set('u96', { id: 'u96' })
store.requests.set('u97', { id: 'u97' })
store.requests.set('u98', { id: 'u98' })
store.requests.set('u99', { id: 'u99' })
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify('ok'), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
await store[doCall]('u99')
expect(store.requests).to.have.length(4)
expect(store.requests.get('u99')).to.be.undefined
})
})
})
describe('Utility', () => {
describe('findFollowRequestNotificationId', () => {
it('should search notifications store for relevant notification', () => {
const store = useFollowRequestsStore()
useNotificationsStore().data = [
{
id: 'n4',
from_profile: { id: 'u3' },
type: 'follow_request',
},
{
id: 'n3',
from_profile: { id: 'u2' },
type: 'repeat',
},
{
id: 'n2',
from_profile: { id: 'u1' },
type: 'follow_request',
},
{
id: 'n1',
from_profile: { id: 'u1' },
type: 'favorite',
},
]
const result = store.findFollowRequestNotificationId('u1')
expect(result).to.have.eql('n2')
})
it("shouldn't crash if there is no notification available", () => {
const store = useFollowRequestsStore()
useNotificationsStore().data = [
{
id: 'n4',
from_profile: { id: 'u3' },
type: 'follow_request',
},
{
id: 'n3',
from_profile: { id: 'u2' },
type: 'repeat',
},
{
id: 'n2',
from_profile: { id: 'u1' },
type: 'follow_request',
},
{
id: 'n1',
from_profile: { id: 'u1' },
type: 'favorite',
},
]
const result = store.findFollowRequestNotificationId('u5')
expect(result).to.have.eql(undefined)
})
})
})
})
})

View file

@ -1,5 +1,5 @@
import { createTestingPinia } from '@pinia/testing'
import { snakeCase } from 'lodash'
import { snakeCase } from 'lodash-es'
import { setActivePinia } from 'pinia'
import { useStatusesStore } from 'src/stores/statuses.js'

View file

@ -1,4 +1,4 @@
import { cloneDeep } from 'lodash'
import { cloneDeep } from 'lodash-es'
import { createPinia, setActivePinia } from 'pinia'
import { useLocalConfigStore } from 'src/stores/local_config.js'

View file

@ -1,11 +1,13 @@
import { createTestingPinia } from '@pinia/testing'
import { snakeCase } from 'lodash'
import { snakeCase } from 'lodash-es'
import { setActivePinia } from 'pinia'
import { useAnnouncementsStore } from 'src/stores/announcements.js'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
import { useChatsStore } from 'src/stores/chats.js'
import { useDraftsStore } from 'src/stores/drafts.js'
import { useEmojiStore } from 'src/stores/emoji.js'
import { useFollowRequestsStore } from 'src/stores/follow_requests.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useListsStore } from 'src/stores/lists.js'
@ -53,12 +55,14 @@ const mockMastoAPIUser = ({
name = userName,
url = userUrl,
id = userId,
locked = true,
} = {}) => ({
id,
acct: screen_name,
display_name: name,
fields: [],
avatar: '',
locked,
url,
})
@ -67,17 +71,20 @@ const mockUser = ({
id = userId,
name = userName,
url = userUrl,
locked = true,
} = {}) => ({
_original: mockMastoAPIUser({
screen_name,
id,
name,
url,
locked,
}),
id,
name,
screen_name,
url,
locked,
relationship: undefined,
})
@ -631,22 +638,24 @@ describe('Users store', () => {
const spies = [
// Misc initialization
vi.spyOn(useSyncConfigStore(), 'initSyncConfig'),
vi.spyOn(useUserHighlightStore(), 'initUserHighlight'),
vi.spyOn(useInterfaceStore(), 'applyTheme'),
vi.spyOn(useInterfaceStore(), 'onLogin'),
vi.spyOn(useEmojiStore(), 'fetchEmoji'),
/* 0 */ vi.spyOn(useSyncConfigStore(), 'initSyncConfig'),
/* 1 */ vi.spyOn(useUserHighlightStore(), 'initUserHighlight'),
/* 2 */ vi.spyOn(useInterfaceStore(), 'applyTheme'),
/* 3 */ vi.spyOn(useInterfaceStore(), 'onLogin'),
/* 4 */ vi.spyOn(useEmojiStore(), 'fetchEmoji'),
/* 5 */ vi.spyOn(useDraftsStore(), 'loadDrafts'),
// Timeline / Notifications
vi.spyOn(useNotificationsStore(), 'activate'),
vi.spyOn(useTimelinesStore(), 'activatePersistents'),
/* 6 */ vi.spyOn(useNotificationsStore(), 'activate'),
/* 7 */ vi.spyOn(useTimelinesStore(), 'activatePersistents'),
// Fetchers
vi.spyOn(useChatsStore(), 'startFetching'),
vi.spyOn(useListsStore(), 'startFetching'),
vi.spyOn(useAnnouncementsStore(), 'startFetching'),
vi.spyOn(useBookmarkFoldersStore(), 'startFetching'),
vi.spyOn(useStreamingStore(), 'initSocket'),
/* 8 */ vi.spyOn(useChatsStore(), 'startFetching'),
/* 9 */ vi.spyOn(useListsStore(), 'startFetching'),
/* 10 */ vi.spyOn(useAnnouncementsStore(), 'startFetching'),
/* 11 */ vi.spyOn(useBookmarkFoldersStore(), 'startFetching'),
/* 12 */ vi.spyOn(useFollowRequestsStore(), 'startFetching'),
/* 13 */ vi.spyOn(useStreamingStore(), 'initSocket'),
]
spies.forEach((spy) => {
@ -758,6 +767,7 @@ describe('Users store', () => {
/* 11 */ vi.spyOn(useAnnouncementsStore(), 'stopFetching'),
/* 12 */ vi.spyOn(useBookmarkFoldersStore(), 'stopFetching'),
/* 13 */ vi.spyOn(useStreamingStore(), 'stopSocket'),
/* 14 */ vi.spyOn(useFollowRequestsStore(), 'stopFetching'),
]
spies.forEach((spy) => {
@ -771,6 +781,7 @@ describe('Users store', () => {
const store = useUsersStore()
store.currentUser = mockUser()
store.currentUser.locked = true
// Adding some users to verify they are getting cleaned afterwards
store.addNewUsers({
@ -832,17 +843,19 @@ describe('Users store', () => {
/* 3 */ vi.spyOn(useChatsStore(), 'stopFetching'),
/* 4 */ vi.spyOn(useAnnouncementsStore(), 'stopFetching'),
/* 5 */ vi.spyOn(useBookmarkFoldersStore(), 'stopFetching'),
/* 6 */ vi.spyOn(useFollowRequestsStore(), 'stopFetching'),
// ## RESUME ##
// Timeline / Notifications
/* 6 */ vi.spyOn(useNotificationsStore(), 'resume'),
/* 7 */ vi.spyOn(useTimelinesStore(), 'resumeAll'),
/* 7 */ vi.spyOn(useNotificationsStore(), 'resume'),
/* 8 */ vi.spyOn(useTimelinesStore(), 'resumeAll'),
// Fetchers (Pauseless)
/* 8 */ vi.spyOn(useListsStore(), 'startFetching'),
/* 9 */ vi.spyOn(useChatsStore(), 'startFetching'),
/* 10 */ vi.spyOn(useAnnouncementsStore(), 'startFetching'),
/* 11 */ vi.spyOn(useBookmarkFoldersStore(), 'startFetching'),
/* 9 */ vi.spyOn(useListsStore(), 'startFetching'),
/* 10 */ vi.spyOn(useChatsStore(), 'startFetching'),
/* 11 */ vi.spyOn(useAnnouncementsStore(), 'startFetching'),
/* 12 */ vi.spyOn(useBookmarkFoldersStore(), 'startFetching'),
/* 13 */ vi.spyOn(useFollowRequestsStore(), 'startFetching'),
]
spies.forEach((spy) => {

6317
yarn.lock

File diff suppressed because it is too large Load diff