Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
24 KB
Referenced Files
None
Subscribers
None
diff --git a/changelog.d/release-fixes.skip b/changelog.d/release-fixes.skip
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 48f6d1d8ed..f1861748b4 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -1,169 +1,169 @@
import { mapState as mapPiniaState } from 'pinia'
import { mapState } from 'vuex'
import UnicodeDomainIndicator from 'src/components/unicode_domain_indicator/unicode_domain_indicator.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
import UserPopover from 'src/components/user_popover/user_popover.vue'
import {
highlightClass,
highlightStyle,
} from '../../services/user_highlighter/user_highlighter.js'
+import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faAt } from '@fortawesome/free-solid-svg-icons'
library.add(faAt)
const MentionLink = {
name: 'MentionLink',
components: {
UserAvatar,
UnicodeDomainIndicator,
UserPopover,
},
props: {
url: {
required: true,
type: String,
},
content: {
required: true,
type: String,
},
userId: {
required: false,
type: String,
},
userScreenName: {
required: false,
type: String,
},
},
data() {
return {
hasSelection: false,
}
},
methods: {
onClick() {
if (this.shouldShowTooltip) return
const link = generateProfileLink(
this.userId || this.user.id,
this.userScreenName || this.user.screen_name,
)
this.$router.push(link)
},
handleSelection() {
if (this.$refs.full) {
this.hasSelection = document
.getSelection()
.containsNode(this.$refs.full, true)
}
},
},
mounted() {
document.addEventListener('selectionchange', this.handleSelection)
},
unmounted() {
document.removeEventListener('selectionchange', this.handleSelection)
},
computed: {
user() {
return (
this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
)
},
isYou() {
// FIXME why user !== currentUser???
return this.user && this.user.id === this.currentUser.id
},
userName() {
return this.user && this.userNameFullUi.split('@')[0]
},
serverName() {
// XXX assumed that domain does not contain @
return (
this.user &&
- (this.userNameFullUi.split('@')[1] ||
- this.$store.getters.instanceDomain)
+ (this.userNameFullUi.split('@')[1] || useInstanceStore().instanceDomain)
)
},
userNameFull() {
return this.user && this.user.screen_name
},
userNameFullUi() {
return this.user && this.user.screen_name_ui
},
highlightData() {
return this.highlight[this.user?.screen_name]
},
highlightType() {
return this.highlightData && '-' + this.highlightData.type
},
highlightClass() {
return this.highlightData && highlightClass(this.user)
},
style() {
if (this.highlightData) {
const {
backgroundColor,
backgroundPosition,
backgroundImage,
...rest
} = highlightStyle(this.highlightData)
return rest
}
},
classnames() {
return [
{
'-you': this.isYou && this.shouldBoldenYou,
'-highlighted': !!this.highlightData,
'-has-selection': this.hasSelection,
},
this.highlightType,
]
},
isRemote() {
return this.userName !== this.userNameFull
},
shouldShowFullUserName() {
const conf = this.mergedConfig.mentionLinkDisplay
if (conf === 'short') {
return false
} else if (conf === 'full') {
return true
} else {
// full_for_remote
return this.isRemote
}
},
shouldShowTooltip() {
return this.mergedConfig.mentionLinkShowTooltip
},
shouldShowAvatar() {
return this.mergedConfig.mentionLinkShowAvatar
},
shouldShowYous() {
return this.mergedConfig.mentionLinkShowYous
},
shouldBoldenYou() {
return this.mergedConfig.mentionLinkBoldenYou
},
shouldFadeDomain() {
return this.mergedConfig.mentionLinkFadeDomain
},
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
...mapPiniaState(useUserHighlightStore, ['highlight']),
...mapState({
currentUser: (state) => state.users.currentUser,
}),
},
}
export default MentionLink
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 1c1191940b..c526eef792 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -1,297 +1,297 @@
<template>
<article
v-if="notification.type === 'mention' || notification.type === 'status'"
ref="root"
>
<Status
- class="Notification"
+ class="Notification panel-body"
:compact="true"
:statusoid="notification.status"
@click="interacted"
/>
</article>
<article
v-else
ref="root"
class="NotificationParent panel-body"
:class="{ '-expandable': expandable }"
>
<div
v-if="needMute && !unmuted"
:id="'notif-' +notification.id"
:aria-expanded="statusExpanded"
:aria-controls="'notif-' +notification.id"
class="Notification container -muted"
>
<small>
<user-link
:user="notification.from_profile"
:at="false"
/>
</small>
<button
class="button-unstyled unmute"
@click.prevent="toggleMute"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="eye-slash"
/>
</button>
</div>
<div
v-else
class="Notification non-mention"
:class="[userClass, { highlighted: userStyle }, '-type--' + notification.type]"
:style="[ userStyle ]"
>
<a
class="avatar-container"
:href="$router.resolve(userProfileLink).href"
@click.prevent
>
<UserPopover
:user-id="notification.from_profile.id"
:overlay-centers="true"
>
<UserAvatar
class="post-avatar"
:compact="true"
:user="notification.from_profile"
/>
</UserPopover>
</a>
<div class="notification-right">
<span class="notification-details">
<div class="name-and-action">
<!-- eslint-disable vue/no-v-html -->
<bdi v-if="!!notification.from_profile.name_html">
<RichContent
class="username"
:title="'@'+notification.from_profile.screen_name_ui"
:html="notification.from_profile.name_html"
:emoji="notification.from_profile.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
:is-local="notification.from_profile.is_local"
/>
</bdi>
<!-- eslint-enable vue/no-v-html -->
<span
v-else
class="username"
:title="'@'+notification.from_profile.screen_name_ui"
>
{{ notification.from_profile.name }}
</span>
{{ ' ' }}
<span v-if="notification.type === 'like'">
<FAIcon
class="type-icon"
icon="star"
/>
{{ ' ' }}
<small>{{ $t('notifications.favorited_you') }}</small>
</span>
<span v-if="notification.type === 'repeat'">
<FAIcon
class="type-icon"
icon="retweet"
:title="$t('tool_tip.repeat')"
/>
{{ ' ' }}
<small>{{ $t('notifications.repeated_you') }}</small>
</span>
<span v-if="notification.type === 'follow'">
<FAIcon
class="type-icon"
icon="user-plus"
/>
{{ ' ' }}
<small>{{ $t('notifications.followed_you') }}</small>
</span>
<span v-if="notification.type === 'follow_request'">
<FAIcon
class="type-icon"
icon="user"
/>
{{ ' ' }}
<small>{{ $t('notifications.follow_request') }}</small>
</span>
<span v-if="notification.type === 'move'">
<FAIcon
class="type-icon"
icon="suitcase-rolling"
/>
{{ ' ' }}
<small>{{ $t('notifications.migrated_to') }}</small>
</span>
<span v-if="notification.type === 'pleroma:emoji_reaction'">
<small>
<i18n-t
scope="global"
keypath="notifications.reacted_with"
>
<img
v-if="notification.emoji_url"
class="emoji-reaction-emoji emoji-reaction-emoji-image"
:src="notification.emoji_url"
:alt="notification.emoji"
:title="notification.emoji"
:class="{ ['-wide']: allowNonSquareEmoji }"
>
<span
v-else
class="emoji-reaction-emoji"
>{{ notification.emoji }}</span>
</i18n-t>
</small>
</span>
<span v-if="notification.type === 'pleroma:report'">
<small>{{ $t('notifications.submitted_report') }}</small>
</span>
<span v-if="notification.type === 'poll'">
<FAIcon
class="type-icon"
icon="poll-h"
/>
{{ ' ' }}
<small>{{ $t('notifications.poll_ended') }}</small>
</span>
</div>
<div
v-if="isStatusNotification"
class="timeago"
>
<router-link
v-if="notification.status"
:to="{ name: 'conversation', params: { id: notification.status.id } }"
class="timeago-link faint"
>
<Timeago
:time="notification.created_at"
:auto-update="240"
/>
</router-link>
<button
class="button-unstyled expand-icon"
:title="$t('tool_tip.toggle_expand')"
:aria-expanded="statusExpanded"
@click.prevent="toggleStatusExpanded"
>
<FAIcon
class="fa-scale-110"
fixed-width
:icon="statusExpanded ? 'compress-alt' : 'expand-alt'"
/>
</button>
</div>
<div
v-else
class="timeago"
>
<span class="faint">
<Timeago
:time="notification.created_at"
:auto-update="240"
/>
</span>
</div>
<button
v-if="needMute"
class="button-unstyled"
:title="$t('tool_tip.toggle_mute')"
:aria-expanded="!unmuted"
@click.prevent="toggleMute"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="eye-slash"
/>
</button>
</span>
<div
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
class="follow-text"
>
<user-link
class="follow-name"
:user="notification.from_profile"
/>
<div
v-if="notification.type === 'follow_request'"
style="white-space: nowrap;"
>
<button
class="button-unstyled"
:title="$t('tool_tip.accept_follow_request')"
@click="approveUser()"
>
<FAIcon
icon="check"
class="fa-scale-110 fa-old-padding follow-request-accept"
/>
</button>
<button
class="button-unstyled"
:title="$t('tool_tip.reject_follow_request')"
@click="denyUser()"
>
<FAIcon
icon="times"
class="fa-scale-110 fa-old-padding follow-request-reject"
/>
</button>
</div>
</div>
<div
v-else-if="notification.type === 'move'"
class="move-text"
>
<user-link
:user="notification.target"
/>
</div>
<Report
v-else-if="notification.type === 'pleroma:report'"
:report-id="notification.report.id"
/>
<template v-else>
<StatusContent
class="status-content"
:compact="!statusExpanded"
:status="notification.status"
:collapse="!statusExpanded"
@click="onContentClick"
/>
</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>
<script src="./notification.js"></script>
<style src="./notification.scss" lang="scss"></style>
diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js
index e02b71b0cd..2341d21c56 100644
--- a/src/components/status_action_buttons/status_action_buttons.js
+++ b/src/components/status_action_buttons/status_action_buttons.js
@@ -1,158 +1,158 @@
import { mapState } from 'pinia'
import { defineAsyncComponent } from 'vue'
import Popover from 'src/components/popover/popover.vue'
import ActionButtonContainer from './action_button_container.vue'
import { BUTTONS } from './buttons_definitions.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
library.add(faEllipsisH)
const StatusActionButtons = {
props: ['status', 'replying'],
emits: ['toggleReplying', 'onSuccess', 'onError'],
data() {
return {
showPin: false,
showingConfirmDialog: false,
currentConfirmTitle: '',
currentConfirmOkText: '',
currentConfirmCancelText: '',
currentConfirmAction: () => {
/* no-op */
},
randomSeed: genRandomSeed(),
emojiPickerShown: false,
}
},
components: {
Popover,
ConfirmModal: defineAsyncComponent(
() => import('src/components/confirm_modal/confirm_modal.vue'),
),
ActionButtonContainer,
},
computed: {
...mapState(useSyncConfigStore, {
pinnedItems: (store) =>
new Set(store.prefsStorage.collections.pinnedStatusActions),
}),
buttons() {
return BUTTONS.filter((x) => (x.if ? x.if(this.funcArg) : true))
},
quickButtons() {
return this.buttons.filter((x) => this.pinnedItems.has(x.name))
},
extraButtons() {
return this.buttons.filter((x) => !this.pinnedItems.has(x.name))
},
currentUser() {
return this.$store.state.users.currentUser
},
funcArg() {
return {
status: this.status,
replying: this.replying,
emojiPickerShown: this.emojiPickerShown,
emit: this.$emit,
dispatch: this.$store.dispatch,
state: this.$store.state,
getters: this.$store.getters,
router: this.$router,
currentUser: this.currentUser,
loggedIn: !!this.currentUser,
}
},
triggerAttrs() {
return {
title: this.$t('status.more_actions'),
'aria-controls': `popup-menu-${this.randomSeed}`,
'aria-haspopup': 'menu',
}
},
},
methods: {
doAction(button) {
if (button.confirm?.(this.funcArg)) {
// TODO move to action_button
this.currentConfirmTitle = this.$t(
button.confirmStrings(this.funcArg).title,
)
this.currentConfirmOkText = this.$t(
button.confirmStrings(this.funcArg).confirm,
)
this.currentConfirmCancelText = this.$t(
button.confirmStrings(this.funcArg).cancel,
)
this.currentConfirmBody = this.$t(
button.confirmStrings(this.funcArg).body,
)
this.currentConfirmAction = () => {
this.showingConfirmDialog = false
this.doActionReal(button)
}
this.showingConfirmDialog = true
} else {
this.doActionReal(button)
}
},
doActionReal(button) {
- button
- .action?.(this.funcArg)
+ const promise = button.action?.(this.funcArg) ?? Promise.resolve()
+ promise
.then(() => this.$emit('onSuccess'))
.catch((err) => this.$emit('onError', err))
},
onExtraClose() {
this.showPin = false
},
onEmojiPickerShown(state) {
this.emojiPickerShown = state
},
isPinned(button) {
return this.pinnedItems.has(button.name)
},
unpin(button) {
useSyncConfigStore().removeCollectionPreference({
path: 'collections.pinnedStatusActions',
value: button.name,
})
useSyncConfigStore().pushSyncConfig()
},
pin(button) {
useSyncConfigStore().addCollectionPreference({
path: 'collections.pinnedStatusActions',
value: button.name,
})
useSyncConfigStore().pushSyncConfig()
},
getComponent(button) {
if (!this.$store.state.users.currentUser && button.anonLink) {
return 'a'
} else if (button.action == null && button.link != null) {
return 'a'
} else {
return 'button'
}
},
getClass(button) {
return {
[button.name + '-button']: true,
disabled: button.interactive
? !button.interactive(this.funcArg)
: false,
'-pin-edit': this.showPin,
'-dropdown': button.dropdown?.(),
'-active': button.active?.(this.funcArg),
}
},
},
}
export default StatusActionButtons
diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx
index a3c5b61cd1..07327af27d 100644
--- a/src/components/tab_switcher/tab_switcher.jsx
+++ b/src/components/tab_switcher/tab_switcher.jsx
@@ -1,183 +1,183 @@
// eslint-disable-next-line no-unused
import { Fragment } from 'vue'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
import './tab_switcher.scss'
const findFirstUsable = (slots) => slots.findIndex((_) => _.props)
export default {
name: 'TabSwitcher',
props: {
renderOnlyFocused: {
required: false,
type: Boolean,
default: false,
},
onSwitch: {
required: false,
type: Function,
default: undefined,
},
activeTab: {
required: false,
type: String,
default: undefined,
},
scrollableTabs: {
required: false,
type: Boolean,
default: false,
},
bodyScrollLock: {
required: false,
type: Boolean,
default: false,
},
},
data() {
return {
active: findFirstUsable(this.slots()),
}
},
computed: {
activeIndex() {
// In case of controlled component
if (this.activeTab) {
return this.slots().findIndex(
(slot) => slot && slot.props && this.activeTab === slot.props.key,
)
} else {
return this.active
}
},
isActive() {
return (tabName) => {
const isWanted = (slot) =>
slot.props && slot.props['data-tab-name'] === tabName
return this.$slots.default().findIndex(isWanted) === this.activeIndex
}
},
},
beforeUpdate() {
const currentSlot = this.slots()[this.active]
if (!currentSlot.props) {
this.active = findFirstUsable(this.slots())
}
},
methods: {
clickTab(index) {
return (e) => {
e.preventDefault()
this.setTab(index)
}
},
// DO NOT put it to computed, it doesn't work (caching?)
slots() {
if (this.$slots.default()[0].type === Fragment) {
return this.$slots.default()[0].children
}
return this.$slots.default()
},
setTab(index) {
if (typeof this.onSwitch === 'function') {
this.onSwitch.call(null, this.slots()[index].key)
}
this.active = index
- if (this.scrollableTabs) {
+ if (this.scrollableTabs && this.$refs.contents) {
this.$refs.contents.scrollTop = 0
}
},
},
render() {
const tabs = this.slots().map((slot, index) => {
const props = slot.props
if (!props) return
const classesTab = ['tab']
const classesWrapper = ['tab-wrapper']
if (this.activeIndex === index) {
classesTab.push('active')
classesWrapper.push('active')
}
if (props.image) {
return (
<div class={classesWrapper.join(' ')}>
<button
disabled={props.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
>
<img src={props.image} title={props['image-tooltip']} />
{props.label ? '' : props.label}
</button>
</div>
)
}
return (
<div class={classesWrapper.join(' ')}>
<button
disabled={props.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
>
{!props.icon ? (
''
) : (
<FAIcon
class="tab-icon"
size="2x"
fixed-width
icon={props.icon}
/>
)}
<span class="text">{props.label}</span>
</button>
</div>
)
})
const contents = this.slots().map((slot, index) => {
const props = slot.props
if (!props) return
const active = this.activeIndex === index
const classes = [active ? 'active' : 'hidden']
if (props.fullHeight || props['full-height']) {
classes.push('-full-height')
}
if (props.fullWidth || props['full-width']) {
classes.push('-full-width')
}
let delayRender = slot.props['delay-render']
if (delayRender && active) {
slot.props['delay-render'] = false
delayRender = false
}
const renderSlot =
!delayRender && (!this.renderOnlyFocused || active) ? slot : ''
return <div class={classes}>{renderSlot}</div>
})
return (
<div class="tab-switcher top-tabs" ref="root">
<div class="tabs" role="tablist" ref="nav">
{tabs}
</div>
<div
role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock}
ref="content"
>
{contents}
</div>
</div>
)
},
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Oct 29, 8:15 PM (1 d, 21 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1798612
Default Alt Text
(24 KB)

Event Timeline