diff --git a/changelog.d/customizable-actions.add b/changelog.d/customizable-actions.add
new file mode 100644
index 000000000..5ce6a5180
--- /dev/null
+++ b/changelog.d/customizable-actions.add
@@ -0,0 +1 @@
+Post actions can be customized
diff --git a/src/App.scss b/src/App.scss
index 1b781304e..c0f2e3fec 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -408,32 +408,11 @@ nav {
}
}
-.menu-item,
.list-item {
- display: block;
- box-sizing: border-box;
- border: none;
- outline: none;
- text-align: initial;
- color: inherit;
- clear: both;
- position: relative;
- white-space: nowrap;
border-color: var(--border);
border-style: solid;
border-width: 0;
border-top-width: 1px;
- width: 100%;
- padding: var(--__vertical-gap) var(--__horizontal-gap);
- background: transparent;
-
- --__line-height: 1.5em;
- --__horizontal-gap: 0.75em;
- --__vertical-gap: 0.5em;
-
- &.-non-interactive {
- cursor: auto;
- }
&.-active,
&:hover {
@@ -455,18 +434,6 @@ nav {
border-bottom-width: 1px;
}
- a,
- button:not(.button-default) {
- text-align: initial;
- padding: 0;
- background: none;
- border: none;
- outline: none;
- display: inline;
- font-family: inherit;
- line-height: unset;
- }
-
&:first-child {
border-top-right-radius: var(--roundness);
border-top-left-radius: var(--roundness);
@@ -480,6 +447,42 @@ nav {
}
}
+.menu-item,
+.list-item {
+ display: block;
+ box-sizing: border-box;
+ border: none;
+ outline: none;
+ text-align: initial;
+ color: inherit;
+ clear: both;
+ position: relative;
+ white-space: nowrap;
+ width: 100%;
+ padding: var(--__vertical-gap) var(--__horizontal-gap);
+ background: transparent;
+
+ --__line-height: 1.5em;
+ --__horizontal-gap: 0.75em;
+ --__vertical-gap: 0.5em;
+
+ &.-non-interactive {
+ cursor: auto;
+ }
+
+ a,
+ button:not(.button-default) {
+ text-align: initial;
+ padding: 0;
+ background: none;
+ border: none;
+ outline: none;
+ display: inline;
+ font-family: inherit;
+ line-height: unset;
+ }
+}
+
.button-unstyled {
border: none;
outline: none;
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
index 62862a8fc..fd4837ee4 100644
--- a/src/components/account_actions/account_actions.vue
+++ b/src/components/account_actions/account_actions.vue
@@ -9,60 +9,80 @@
+
-
-
-
-
-
+
+
+
+
diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue
index 7e3498197..0efdf7977 100644
--- a/src/components/chat_message/chat_message.vue
+++ b/src/components/chat_message/chat_message.vue
@@ -51,12 +51,14 @@
>
diff --git a/src/components/confirm_modal/mute_confirm.js b/src/components/confirm_modal/mute_confirm.js
new file mode 100644
index 000000000..62f8c3de3
--- /dev/null
+++ b/src/components/confirm_modal/mute_confirm.js
@@ -0,0 +1,111 @@
+import { unitToSeconds } from 'src/services/date_utils/date_utils.js'
+import { mapGetters } from 'vuex'
+
+import ConfirmModal from './confirm_modal.vue'
+import Select from 'src/components/select/select.vue'
+
+export default {
+ props: ['type', 'user'],
+ emits: ['hide', 'show', 'muted'],
+ data: () => ({
+ showing: false,
+ muteExpiryAmount: 2,
+ muteExpiryUnit: 'hours'
+ }),
+ components: {
+ ConfirmModal,
+ Select
+ },
+ computed: {
+ muteExpiryValue () {
+ unitToSeconds(this.muteExpiryUnit, this.muteExpiryAmount)
+ },
+ muteExpiryUnits () {
+ return ['minutes', 'hours', 'days']
+ },
+ domain () {
+ return this.user.fqn.split('@')[1]
+ },
+ keypath () {
+ if (this.type === 'domain') {
+ return 'status.mute_domain_confirm'
+ } else if (this.type === 'conversation') {
+ return 'status.mute_conversation_confirm'
+ } else {
+ return 'user_card.mute_confirm'
+ }
+ },
+ userIsMuted () {
+ return this.$store.getters.relationship(this.user.id).muting
+ },
+ conversationIsMuted () {
+ return this.status.conversation_muted
+ },
+ domainIsMuted () {
+ return new Set(this.$store.state.users.currentUser.domainMutes).has(this.domain)
+ },
+ shouldConfirm () {
+ switch (this.type) {
+ case 'domain': {
+ return this.mergedConfig.modalOnMuteDomain
+ }
+ case 'conversation': {
+ return this.mergedConfig.modalOnMuteConversation
+ }
+ default: {
+ return this.mergedConfig.modalOnMute
+ }
+ }
+ },
+ ...mapGetters(['mergedConfig'])
+ },
+ methods: {
+ optionallyPrompt () {
+ console.log('Triggered')
+ if (this.shouldConfirm) {
+ console.log('SHAWN!!')
+ this.show()
+ } else {
+ this.doMute()
+ }
+ },
+ show () {
+ this.showing = true
+ this.$emit('show')
+ },
+ hide () {
+ this.showing = false
+ this.$emit('hide')
+ },
+ doMute () {
+ switch (this.type) {
+ case 'domain': {
+ if (!this.domainIsMuted) {
+ this.$store.dispatch('muteDomain', { id: this.domain, expiresIn: this.muteExpiryValue })
+ } else {
+ this.$store.dispatch('unmuteDomain', { id: this.domain })
+ }
+ break
+ }
+ case 'conversation': {
+ if (!this.conversationIsMuted) {
+ this.$store.dispatch('muteConversation', { id: this.status.id, expiresIn: this.muteExpiryValue })
+ } else {
+ this.$store.dispatch('unmuteConversation', { id: this.status.id })
+ }
+ break
+ }
+ default: {
+ if (!this.userIsMuted) {
+ this.$store.dispatch('muteUser', { id: this.user.id, expiresIn: this.muteExpiryValue })
+ } else {
+ this.$store.dispatch('unmuteUser', { id: this.user.id })
+ }
+ break
+ }
+ }
+ this.$emit('muted')
+ this.hide()
+ }
+ }
+}
diff --git a/src/components/confirm_modal/mute_confirm.vue b/src/components/confirm_modal/mute_confirm.vue
new file mode 100644
index 000000000..bf7ab338f
--- /dev/null
+++ b/src/components/confirm_modal/mute_confirm.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ ' ' }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index a94d2130d..6f6f2cac2 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -350,6 +350,7 @@ const conversation = {
},
...mapGetters(['mergedConfig']),
...mapState({
+ mobileLayout: state => state.interface.layoutType === 'mobile',
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
})
},
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 52d578b1e..51c016323 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -20,7 +20,7 @@
{{ $t('timeline.collapse') }}
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
deleted file mode 100644
index b3e1cb2bb..000000000
--- a/src/components/extra_buttons/extra_buttons.js
+++ /dev/null
@@ -1,175 +0,0 @@
-import Popover from '../popover/popover.vue'
-import genRandomSeed from '../../services/random_seed/random_seed.service.js'
-import ConfirmModal from '../confirm_modal/confirm_modal.vue'
-import StatusBookmarkFolderMenu from '../status_bookmark_folder_menu/status_bookmark_folder_menu.vue'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faEllipsisH,
- faBookmark,
- faEyeSlash,
- faThumbtack,
- faShareAlt,
- faExternalLinkAlt,
- faHistory,
- faPlus,
- faTimes
-} from '@fortawesome/free-solid-svg-icons'
-import {
- faBookmark as faBookmarkReg,
- faFlag
-} from '@fortawesome/free-regular-svg-icons'
-
-library.add(
- faEllipsisH,
- faBookmark,
- faBookmarkReg,
- faEyeSlash,
- faThumbtack,
- faShareAlt,
- faExternalLinkAlt,
- faFlag,
- faHistory,
- faPlus,
- faTimes
-)
-
-const ExtraButtons = {
- props: ['status'],
- components: {
- Popover,
- ConfirmModal,
- StatusBookmarkFolderMenu
- },
- data () {
- return {
- expanded: false,
- showingDeleteDialog: false,
- randomSeed: genRandomSeed()
- }
- },
- methods: {
- onShow () {
- this.expanded = true
- },
- onClose () {
- this.expanded = false
- },
- deleteStatus () {
- if (this.shouldConfirmDelete) {
- this.showDeleteStatusConfirmDialog()
- } else {
- this.doDeleteStatus()
- }
- },
- doDeleteStatus () {
- this.$store.dispatch('deleteStatus', { id: this.status.id })
- this.hideDeleteStatusConfirmDialog()
- },
- showDeleteStatusConfirmDialog () {
- this.showingDeleteDialog = true
- },
- hideDeleteStatusConfirmDialog () {
- this.showingDeleteDialog = false
- },
- pinStatus () {
- this.$store.dispatch('pinStatus', this.status.id)
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- unpinStatus () {
- this.$store.dispatch('unpinStatus', this.status.id)
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- muteConversation () {
- this.$store.dispatch('muteConversation', this.status.id)
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- unmuteConversation () {
- this.$store.dispatch('unmuteConversation', this.status.id)
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- copyLink () {
- navigator.clipboard.writeText(this.statusLink)
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- bookmarkStatus () {
- this.$store.dispatch('bookmark', { id: this.status.id })
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- unbookmarkStatus () {
- this.$store.dispatch('unbookmark', { id: this.status.id })
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- reportStatus () {
- this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
- },
- editStatus () {
- this.$store.dispatch('fetchStatusSource', { id: this.status.id })
- .then(data => this.$store.dispatch('openEditStatusModal', {
- statusId: this.status.id,
- subject: data.spoiler_text,
- statusText: data.text,
- statusIsSensitive: this.status.nsfw,
- statusPoll: this.status.poll,
- statusFiles: [...this.status.attachments],
- visibility: this.status.visibility,
- statusContentType: data.content_type
- }))
- },
- showStatusHistory () {
- const originalStatus = { ...this.status }
- const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html']
- stripFieldsList.forEach(p => delete originalStatus[p])
- this.$store.dispatch('openStatusHistoryModal', originalStatus)
- }
- },
- computed: {
- currentUser () { return this.$store.state.users.currentUser },
- canDelete () {
- if (!this.currentUser) { return }
- return this.currentUser.privileges.includes('messages_delete') || this.status.user.id === this.currentUser.id
- },
- ownStatus () {
- return this.status.user.id === this.currentUser.id
- },
- canPin () {
- return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
- },
- canMute () {
- return !!this.currentUser
- },
- canBookmark () {
- return !!this.currentUser
- },
- bookmarkFolders () {
- return this.$store.state.instance.pleromaBookmarkFoldersAvailable
- },
- statusLink () {
- return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
- },
- isEdited () {
- return this.status.edited_at !== null
- },
- editingAvailable () { return this.$store.state.instance.editingAvailable },
- shouldConfirmDelete () {
- return this.$store.getters.mergedConfig.modalOnDelete
- },
- triggerAttrs () {
- return {
- title: this.$t('status.more_actions'),
- id: `popup-trigger-${this.randomSeed}`,
- 'aria-controls': `popup-menu-${this.randomSeed}`,
- 'aria-expanded': this.expanded,
- 'aria-haspopup': 'menu'
- }
- }
- }
-}
-
-export default ExtraButtons
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
deleted file mode 100644
index c030de0c8..000000000
--- a/src/components/extra_buttons/extra_buttons.vue
+++ /dev/null
@@ -1,238 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js
deleted file mode 100644
index cf3378c90..000000000
--- a/src/components/favorite_button/favorite_button.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { mapGetters } from 'vuex'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faStar,
- faPlus,
- faMinus,
- faCheck
-} from '@fortawesome/free-solid-svg-icons'
-import {
- faStar as faStarRegular
-} from '@fortawesome/free-regular-svg-icons'
-
-library.add(
- faStar,
- faStarRegular,
- faPlus,
- faMinus,
- faCheck
-)
-
-const FavoriteButton = {
- props: ['status', 'loggedIn'],
- data () {
- return {
- animated: false
- }
- },
- methods: {
- favorite () {
- if (!this.status.favorited) {
- this.$store.dispatch('favorite', { id: this.status.id })
- } else {
- this.$store.dispatch('unfavorite', { id: this.status.id })
- }
- this.animated = true
- setTimeout(() => {
- this.animated = false
- }, 500)
- }
- },
- computed: {
- ...mapGetters(['mergedConfig']),
- remoteInteractionLink () {
- return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
- }
- }
-}
-
-export default FavoriteButton
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
deleted file mode 100644
index 2e0dd0476..000000000
--- a/src/components/favorite_button/favorite_button.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/input.style.js b/src/components/input.style.js
index 30715ec1d..160b2f33d 100644
--- a/src/components/input.style.js
+++ b/src/components/input.style.js
@@ -18,7 +18,7 @@ export default {
{
component: 'Root',
directives: {
- '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2), inset 0 0 2 #000000 / 0.15',
+ '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2), inset 0 0 2 #000000 / 0.15, 1 0 1 1 --text / 0.15, -1 0 1 1 --text / 0.15',
'--defaultInputHoverGlow': 'shadow | 0 0 4 --text / 0.5',
'--defaultInputFocusGlow': 'shadow | 0 0 4 4 --link / 0.5'
}
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
index df3bb655f..9c8894bff 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -10,119 +10,150 @@
>
+
diff --git a/src/components/notifications/notification_filters.vue b/src/components/notifications/notification_filters.vue
index 497a5156f..0b740ea0a 100644
--- a/src/components/notifications/notification_filters.vue
+++ b/src/components/notifications/notification_filters.vue
@@ -7,78 +7,94 @@
>
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index 80585d698..242068109 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -197,8 +197,8 @@ const Popover = {
// Default to whatever user wished with placement prop
let usingTop = this.placement !== 'bottom'
- // Handle special cases, first force to displaying on top if there's not space on bottom,
- // regardless of what placement value was. Then check if there's not space on top, and
+ // Handle special cases, first force to displaying on top if there's no space on bottom,
+ // regardless of what placement value was. Then check if there's no space on top, and
// force to bottom, again regardless of what placement value was.
const topBoundary = origin.y - anchorHeight * 0.5 + (this.removePadding ? topPadding : 0)
const bottomBoundary = origin.y + anchorHeight * 0.5 - (this.removePadding ? bottomPadding : 0)
@@ -214,20 +214,20 @@ const Popover = {
translateX = origin.x + horizOffset + xOffset
} else {
// Default to whatever user wished with placement prop
- let usingRight = this.placement !== 'left'
+ let usingLeft = this.placement !== 'right'
- // Handle special cases, first force to displaying on top if there's not space on bottom,
- // regardless of what placement value was. Then check if there's not space on top, and
- // force to bottom, again regardless of what placement value was.
- const rightBoundary = origin.x - anchorWidth * 0.5 + (this.removePadding ? rightPadding : 0)
- const leftBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? leftPadding : 0)
- if (leftBoundary + content.offsetWidth > xBounds.max) usingRight = true
- if (rightBoundary - content.offsetWidth < xBounds.min) usingRight = false
+ // Handle special cases, first force to displaying on left if there's no space on right,
+ // regardless of what placement value was. Then check if there's no space on right, and
+ // force to left, again regardless of what placement value was.
+ const leftBoundary = origin.x - anchorWidth * 0.5 + (this.removePadding ? leftPadding : 0)
+ const rightBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? rightPadding : 0)
+ if (rightBoundary + content.offsetWidth > xBounds.max) usingLeft = true
+ if (leftBoundary - content.offsetWidth < xBounds.min) usingLeft = false
const xOffset = (this.offset && this.offset.x) || 0
- translateX = usingRight
- ? rightBoundary - xOffset - content.offsetWidth
- : leftBoundary + xOffset
+ translateX = usingLeft
+ ? leftBoundary - xOffset - content.offsetWidth
+ : rightBoundary + xOffset
const yOffset = (this.offset && this.offset.y) || 0
translateY = origin.y + vertOffset + yOffset
@@ -275,6 +275,11 @@ const Popover = {
this.scrollable.removeEventListener('scroll', this.onScroll)
this.scrollable.removeEventListener('resize', this.onResize)
},
+ resizePopover () {
+ setTimeout(() => {
+ this.updateStyles()
+ }, 1)
+ },
onMouseenter (e) {
if (this.trigger === 'hover') {
this.lockReEntry = false
@@ -323,7 +328,12 @@ const Popover = {
this.updateStyles()
},
onResize (e) {
- this.updateStyles()
+ const content = this.$refs.content
+ if (!content) return
+ if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
+ this.updateStyles()
+ this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
+ }
},
onChildPopoverState (childRef, state) {
if (state) {
@@ -337,12 +347,7 @@ const Popover = {
// Monitor changes to content size, update styles only when content sizes have changed,
// that should be the only time we need to move the popover box if we don't care about scroll
// or resize
- const content = this.$refs.content
- if (!content) return
- if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
- this.updateStyles()
- this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
- }
+ this.onResize()
},
mounted () {
this.teleport = true
diff --git a/src/components/popover/popover.scss b/src/components/popover/popover.scss
new file mode 100644
index 000000000..57a5250a7
--- /dev/null
+++ b/src/components/popover/popover.scss
@@ -0,0 +1,142 @@
+.popover-trigger-button {
+ display: inline-block;
+}
+
+.popover {
+ z-index: var(--ZI_popover_override, var(--ZI_popovers));
+ position: fixed;
+ min-width: 0;
+ max-width: calc(100vw - 20px);
+ box-shadow: var(--shadow);
+}
+
+.popover-default {
+ &::after {
+ content: "";
+ position: absolute;
+ top: -1px;
+ bottom: -1px;
+ left: -1px;
+ right: -1px;
+ z-index: -1px;
+ box-shadow: var(--shadow);
+ pointer-events: none;
+ }
+
+ border-radius: var(--roundness);
+ border-color: var(--border);
+ border-style: solid;
+ border-width: 1px;
+ background-color: var(--background);
+}
+
+.dropdown-menu {
+ display: block;
+ padding: 0;
+ font-size: 1em;
+ text-align: left;
+ list-style: none;
+ max-width: 100vw;
+ z-index: var(--ZI_popover_override, var(--ZI_popovers));
+ white-space: nowrap;
+ background-color: var(--background);
+
+ .dropdown-divider {
+ height: 0;
+ margin: 0.5rem 0;
+ overflow: hidden;
+ border-top: 1px solid var(--border);
+ }
+
+ .dropdown-item {
+ padding: 0;
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-auto-flow: column;
+ grid-auto-columns: auto;
+
+ .popover-wrapper {
+ box-sizing: border-box;
+ display: grid;
+ grid-template-columns: 1fr;
+ }
+
+ .extra-button {
+ border-left: 1px solid var(--icon);
+ padding-left: calc(var(--__horizontal-gap) - 1px);
+ border-right: var(--__horizontal-gap) solid transparent;
+ border-top: var(--__horizontal-gap) solid transparent;
+ border-bottom: var(--__horizontal-gap) solid transparent;
+ }
+
+ .main-button {
+ width: 100%;
+ padding: var(--__horizontal-gap) var(--__horizontal-gap);
+ grid-gap: var(--__horizontal-gap);
+ grid-template-columns: 1fr var(--__line-height);
+ grid-auto-flow: column;
+ grid-auto-columns: auto;
+
+ .menu-checkbox {
+ display: inline-block;
+ vertical-align: middle;
+ min-width: calc(var(--__line-height) + 1px);
+ max-width: calc(var(--__line-height) + 1px);
+ min-height: calc(var(--__line-height) + 1px);
+ max-height: calc(var(--__line-height) + 1px);
+ line-height: var(--__line-height);
+ text-align: center;
+ border-radius: 0;
+ box-shadow: var(--shadow);
+ margin-right: var(--__horizontal-gap);
+
+ &.menu-checkbox-checked::after {
+ font-size: 1.25em;
+ content: "✓";
+ }
+
+ &.-radio {
+ border-radius: 9999px;
+
+ &.menu-checkbox-checked::after {
+ font-size: 2em;
+ content: "•";
+ }
+ }
+ }
+ }
+
+ .main-button,
+ .extra-button {
+ display: grid;
+ box-sizing: border-box;
+ align-items: center;
+
+ &.disabled {
+ cursor: not-allowed;
+ }
+
+ &:not(.disabled) {
+ cursor: pointer;
+ }
+ }
+
+ &.-icon {
+ .main-button {
+ grid-template-columns: var(--__line-height) 1fr;
+ }
+ }
+
+ &.-icon-space {
+ .main-button {
+ padding-left: calc(var(--__line-height) + var(--__horizontal-gap) * 2);
+ }
+ }
+
+ &.-icon-double {
+ .main-button {
+ grid-template-columns: var(--__line-height) var(--__line-height) 1fr;
+ }
+ }
+ }
+}
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index 0c5b372ee..f91abc3f0 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -1,5 +1,6 @@
@@ -32,6 +33,7 @@
name="content"
class="popover-inner"
:close="hidePopover"
+ :resize="resizePopover"
/>
@@ -41,101 +43,4 @@
-
+
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 220d3e975..53f236536 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -336,7 +336,7 @@
>
-
+
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 81a064b3c..13872d0e6 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -491,6 +491,8 @@
"confirm_dialogs_unfollow": "unfollowing a user",
"confirm_dialogs_block": "blocking a user",
"confirm_dialogs_mute": "muting a user",
+ "confirm_dialogs_mute_domain": "muting domains",
+ "confirm_dialogs_mute_conversation": "muting conversations",
"confirm_dialogs_delete": "deleting a status",
"confirm_dialogs_logout": "logging out",
"confirm_dialogs_approve_follow": "approving a follower",
@@ -1213,7 +1215,8 @@
"socket_reconnected": "Realtime connection established",
"socket_broke": "Realtime connection lost: CloseEvent code {0}",
"quick_view_settings": "Quick view settings",
- "quick_filter_settings": "Quick filter settings"
+ "quick_filter_settings": "Quick filter settings",
+ "filter_settings": "Filter"
},
"status": {
"favorites": "Favorites",
@@ -1242,6 +1245,11 @@
"mentions": "Mentions",
"replies_list": "Replies:",
"replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):",
+ "mute_ellipsis": "Mute…",
+ "mute_user": "Mute user",
+ "unmute_user": "Unmute user",
+ "mute_domain": "Mute domain",
+ "unmute_domain": "Unmute domain",
"mute_conversation": "Mute conversation",
"unmute_conversation": "Unmute conversation",
"status_unavailable": "Status unavailable",
@@ -1414,8 +1422,11 @@
"media_upload": "Upload media",
"mentions": "Mentions",
"repeat": "Repeat",
+ "unrepeat": "Unrepeat",
"reply": "Reply",
+ "add_reaction": "Add reaction",
"favorite": "Favorite",
+ "unfavorite": "Unfavorite",
"add_reaction": "Add Reaction",
"user_settings": "User Settings",
"accept_follow_request": "Accept follow request",
diff --git a/src/modules/config.js b/src/modules/config.js
index 7a10eb492..c42fc8c12 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -137,6 +137,8 @@ export const defaultState = {
modalOnUnfollow: undefined, // instance default
modalOnBlock: undefined, // instance default
modalOnMute: undefined, // instance default
+ modalOnMuteConversation: undefined, // instance default
+ modalOnMuteDomain: undefined, // instance default
modalOnDelete: undefined, // instance default
modalOnLogout: undefined, // instance default
modalOnApproveFollow: undefined, // instance default
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 7f920eaac..408aaef16 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -77,6 +77,8 @@ const defaultState = {
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
+ modalOnMuteConversation: false,
+ modalOnMuteDomain: true,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,
diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js
index c55f54fdd..ad581fd4e 100644
--- a/src/modules/serverSideStorage.js
+++ b/src/modules/serverSideStorage.js
@@ -1,5 +1,16 @@
import { toRaw } from 'vue'
-import { isEqual, cloneDeep, set, get, clamp, flatten, groupBy, findLastIndex, takeRight, uniqWith } from 'lodash'
+import {
+ isEqual,
+ cloneDeep,
+ set,
+ get,
+ clamp,
+ flatten,
+ groupBy,
+ findLastIndex,
+ takeRight,
+ uniqWith
+} from 'lodash'
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
export const VERSION = 1
@@ -26,6 +37,7 @@ export const defaultState = {
collapseNav: false
},
collections: {
+ pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'],
pinnedNavItems: ['home', 'dms', 'chats']
}
},
@@ -77,7 +89,7 @@ const _verifyPrefs = (state) => {
})
}
-export const _getRecentData = (cache, live) => {
+export const _getRecentData = (cache, live, isTest) => {
const result = { recent: null, stale: null, needUpload: false }
const cacheValid = _checkValidity(cache || {})
const liveValid = _checkValidity(live || {})
@@ -110,6 +122,23 @@ export const _getRecentData = (cache, live) => {
console.debug('Both sources are invalid, start from scratch')
result.needUpload = true
}
+
+ const merge = (a, b) => ({
+ _version: a._version ?? b._version,
+ _timestamp: a._timestamp ?? b._timestamp,
+ needUpload: b.needUpload ?? a.needUpload,
+ prefsStorage: {
+ ...a.prefsStorage,
+ ...b.prefsStorage
+ },
+ flagStorage: {
+ ...a.flagStorage,
+ ...b.flagStorage
+ }
+ })
+ result.recent = isTest ? result.recent : (result.recent && merge(defaultState, result.recent))
+ result.stale = isTest ? result.stale : (result.stale && merge(defaultState, result.stale))
+
return result
}
@@ -281,7 +310,7 @@ export const mutations = {
clearServerSideStorage (state, userData) {
state = { ...cloneDeep(defaultState) }
},
- setServerSideStorage (state, userData) {
+ setServerSideStorage (state, userData, test) {
const live = userData.storage
state.raw = live
let cache = state.cache
@@ -292,7 +321,7 @@ export const mutations = {
cache = _doMigrations(cache)
- let { recent, stale, needsUpload } = _getRecentData(cache, live)
+ let { recent, stale, needUpload } = _getRecentData(cache, live)
const userNew = userData.created_at > NEW_USER_DATE
const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage
@@ -306,7 +335,7 @@ export const mutations = {
})
}
- if (!needsUpload && recent && stale) {
+ if (!needUpload && recent && stale) {
console.debug('Checking if data needs merging...')
// discarding timestamps and versions
const { _timestamp: _0, _version: _1, ...recentData } = recent
@@ -335,7 +364,7 @@ export const mutations = {
recent.flagStorage = { ...flagsTemplate, ...totalFlags }
recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
- state.dirty = dirty || needsUpload
+ state.dirty = dirty || needUpload
state.cache = recent
// set local timestamp to smaller one if we don't have any changes
if (stale && recent && !state.dirty) {
diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js
index 2e43263ae..50df2a853 100644
--- a/test/unit/specs/modules/serverSideStorage.spec.js
+++ b/test/unit/specs/modules/serverSideStorage.spec.js
@@ -74,7 +74,7 @@ describe('The serverSideStorage module', () => {
})
})
- it('should reset local timestamp to remote if contents are the same', () => {
+ it.only('should reset local timestamp to remote if contents are the same', () => {
const state = {
...cloneDeep(defaultState),
cache: null
@@ -176,33 +176,33 @@ describe('The serverSideStorage module', () => {
})
describe('_getRecentData', () => {
it('should handle nulls correctly', () => {
- expect(_getRecentData(null, null)).to.eql({ recent: null, stale: null, needUpload: true })
+ expect(_getRecentData(null, null, true)).to.eql({ recent: null, stale: null, needUpload: true })
})
it('doesn\'t choke on invalid data', () => {
- expect(_getRecentData({ a: 1 }, { b: 2 })).to.eql({ recent: null, stale: null, needUpload: true })
+ expect(_getRecentData({ a: 1 }, { b: 2 }, true)).to.eql({ recent: null, stale: null, needUpload: true })
})
it('should prefer the valid non-null correctly, needUpload works properly', () => {
const nonNull = { _version: VERSION, _timestamp: 1 }
- expect(_getRecentData(nonNull, null)).to.eql({ recent: nonNull, stale: null, needUpload: true })
- expect(_getRecentData(null, nonNull)).to.eql({ recent: nonNull, stale: null, needUpload: false })
+ expect(_getRecentData(nonNull, null, true)).to.eql({ recent: nonNull, stale: null, needUpload: true })
+ expect(_getRecentData(null, nonNull, true)).to.eql({ recent: nonNull, stale: null, needUpload: false })
})
it('should prefer the one with higher timestamp', () => {
const a = { _version: VERSION, _timestamp: 1 }
const b = { _version: VERSION, _timestamp: 2 }
- expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false })
- expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false })
+ expect(_getRecentData(a, b, true)).to.eql({ recent: b, stale: a, needUpload: false })
+ expect(_getRecentData(b, a, true)).to.eql({ recent: b, stale: a, needUpload: false })
})
it('case where both are same', () => {
const a = { _version: VERSION, _timestamp: 3 }
const b = { _version: VERSION, _timestamp: 3 }
- expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false })
- expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false })
+ expect(_getRecentData(a, b, true)).to.eql({ recent: b, stale: a, needUpload: false })
+ expect(_getRecentData(b, a, true)).to.eql({ recent: b, stale: a, needUpload: false })
})
})