diff options
Diffstat (limited to 'src/components')
254 files changed, 7739 insertions, 2770 deletions
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index ce19291a..9a7c6824 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -11,14 +11,14 @@ <template v-if="relationship.following"> <button v-if="relationship.showing_reblogs" - class="btn button-default dropdown-item" + class="dropdown-item menu-item" @click="hideRepeats" > {{ $t('user_card.hide_repeats') }} </button> <button v-if="!relationship.showing_reblogs" - class="btn button-default dropdown-item" + class="dropdown-item menu-item" @click="showRepeats" > {{ $t('user_card.show_repeats') }} @@ -31,34 +31,34 @@ <UserListMenu :user="user" /> <button v-if="relationship.followed_by" - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="removeUserFromFollowers" > {{ $t('user_card.remove_follower') }} </button> <button v-if="relationship.blocking" - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="unblockUser" > {{ $t('user_card.unblock') }} </button> <button v-else - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="blockUser" > {{ $t('user_card.block') }} </button> <button - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="reportUser" > {{ $t('user_card.report') }} </button> <button v-if="pleromaChatMessagesAvailable" - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="openChat" > {{ $t('user_card.message') }} @@ -122,19 +122,12 @@ <script src="./account_actions.js"></script> <style lang="scss"> -@import "../../variables"; - .AccountActions { .ellipsis-button { width: 2.5em; margin: -0.5em 0; padding: 0.5em 0; text-align: center; - - &:not(:hover) .icon { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } } } </style> diff --git a/src/components/alert.style.js b/src/components/alert.style.js new file mode 100644 index 00000000..abbeb5ba --- /dev/null +++ b/src/components/alert.style.js @@ -0,0 +1,53 @@ +export default { + name: 'Alert', + selector: '.alert', + validInnerComponents: [ + 'Text', + 'Icon', + 'Link', + 'Border', + 'ButtonUnstyled' + ], + variants: { + normal: '.neutral', + error: '.error', + warning: '.warning', + success: '.success' + }, + defaultRules: [ + { + directives: { + background: '--text', + opacity: 0.5, + blur: '9px' + } + }, + { + parent: { + component: 'Alert' + }, + component: 'Border', + directives: { + textColor: '--parent' + } + }, + { + variant: 'error', + directives: { + background: '--cRed' + } + }, + { + variant: 'warning', + directives: { + background: '--cOrange' + } + }, + { + variant: 'success', + directives: { + background: '--cGreen' + } + } + ] +} diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue index a1c5791e..92880831 100644 --- a/src/components/announcement/announcement.vue +++ b/src/components/announcement/announcement.vue @@ -99,16 +99,14 @@ <script src="./announcement.js"></script> <style lang="scss"> -@import "../../variables"; - .announcement { - border-bottom: 1px solid var(--border, $fallback--border); + border-bottom: 1px solid var(--border); border-radius: 0; - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); .heading, .body { - margin-bottom: var(--status-margin, $status-margin); + margin-bottom: var(--status-margin); } .footer { diff --git a/src/components/announcement_editor/announcement_editor.vue b/src/components/announcement_editor/announcement_editor.vue index 0f29f9f7..c0a3c30a 100644 --- a/src/components/announcement_editor/announcement_editor.vue +++ b/src/components/announcement_editor/announcement_editor.vue @@ -3,7 +3,7 @@ <textarea ref="textarea" v-model="announcement.content" - class="post-textarea" + class="input post-textarea" rows="1" cols="1" :placeholder="$t('announcements.post_placeholder')" @@ -14,6 +14,7 @@ <input id="announcement-start-time" v-model="announcement.startsAt" + class="input" :type="announcement.allDay ? 'date' : 'datetime-local'" :disabled="disabled" > @@ -23,6 +24,7 @@ <input id="announcement-end-time" v-model="announcement.endsAt" + class="input" :type="announcement.allDay ? 'date' : 'datetime-local'" :disabled="disabled" > diff --git a/src/components/announcements_page/announcements_page.vue b/src/components/announcements_page/announcements_page.vue index 78d3ecee..25f830ff 100644 --- a/src/components/announcements_page/announcements_page.vue +++ b/src/components/announcements_page/announcements_page.vue @@ -61,15 +61,13 @@ <script src="./announcements_page.js"></script> <style lang="scss"> -@import "../../variables"; - .announcements-page { .post-form { - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); .heading, .body { - margin-bottom: var(--status-margin, $status-margin); + margin-bottom: var(--status-margin); } .post-button { diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss index 681bab29..13afbe64 100644 --- a/src/components/attachment/attachment.scss +++ b/src/components/attachment/attachment.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Attachment { display: inline-flex; flex-direction: column; @@ -9,10 +7,8 @@ height: 100%; border-style: solid; border-width: 1px; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-radius: var(--roundness); + border-color: var(--border); .attachment-wrapper { flex: 1 1 auto; @@ -84,6 +80,13 @@ } } + .video-container { + border: none; + outline: none; + color: inherit; + background: transparent; + } + .audio-container { display: flex; align-items: flex-end; @@ -126,23 +129,12 @@ .attachment-button { padding: 0; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); text-align: center; width: 2em; height: 2em; margin-left: 0.5em; font-size: 1.25em; - // TODO: theming? hard to theme with unknown background image color - background: rgb(230 230 230 / 70%); - - .svg-inline--fa { - color: rgb(0 0 0 / 60%); - } - - &:hover .svg-inline--fa { - color: rgb(0 0 0 / 90%); - } } } @@ -217,8 +209,7 @@ &.-placeholder { display: inline-block; - color: $fallback--link; - color: var(--postLink, $fallback--link); + color: var(--link); overflow: hidden; white-space: nowrap; height: auto; diff --git a/src/components/attachment/attachment.style.js b/src/components/attachment/attachment.style.js new file mode 100644 index 00000000..5fb4701c --- /dev/null +++ b/src/components/attachment/attachment.style.js @@ -0,0 +1,24 @@ +export default { + name: 'Attachment', + selector: '.Attachment', + validInnerComponents: [ + 'Border', + 'ButtonUnstyled', + 'Input' + ], + defaultRules: [ + { + directives: { + roundness: 3 + } + }, + { + component: 'ButtonUnstyled', + parent: { component: 'Attachment' }, + directives: { + background: '#FFFFFF', + opacity: 0.5 + } + } + ] +} diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 79f62806..9abc2627 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -38,7 +38,7 @@ v-if="edit" v-model="localDescription" type="text" - class="description-field" + class="input description-field" :placeholder="$t('post_status.media_description')" @keydown.enter.prevent="" > @@ -175,7 +175,6 @@ :is="videoTag" v-if="type === 'video' && !hidden" class="video-container" - :class="{ 'button-unstyled': 'isModal' }" :href="attachment.url" @click.stop.prevent="openModal" > @@ -253,7 +252,7 @@ v-if="edit" v-model="localDescription" type="text" - class="description-field" + class="input description-field" :placeholder="$t('post_status.media_description')" @keydown.enter.prevent="" > diff --git a/src/components/autosuggest/autosuggest.vue b/src/components/autosuggest/autosuggest.vue index 7b7102fd..04f41801 100644 --- a/src/components/autosuggest/autosuggest.vue +++ b/src/components/autosuggest/autosuggest.vue @@ -1,3 +1,4 @@ +<!-- FIXME THIS NEEDS TO BE REFACTORED TO USE POPOVER --> <template> <div v-click-outside="onClickOutside" @@ -6,12 +7,12 @@ <input v-model="term" :placeholder="placeholder" - class="autosuggest-input" + class="input autosuggest-input" @click="onInputClick" > <div v-if="resultsVisible && filtered.length > 0" - class="autosuggest-results" + class="panel autosuggest-results" > <slot v-for="item in filtered" @@ -24,8 +25,6 @@ <script src="./autosuggest.js"></script> <style lang="scss"> -@import "../../variables"; - .autosuggest { position: relative; @@ -40,18 +39,15 @@ top: 100%; right: 0; max-height: 400px; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); + background-color: var(--bg); border-style: solid; border-width: 1px; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border-color: var(--border); + border-radius: var(--roundness); border-top-left-radius: 0; border-top-right-radius: 0; box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + box-shadow: var(--shadow); overflow-y: auto; z-index: 1; } diff --git a/src/components/avatar_list/avatar_list.vue b/src/components/avatar_list/avatar_list.vue index 2d00cb7b..9252ba32 100644 --- a/src/components/avatar_list/avatar_list.vue +++ b/src/components/avatar_list/avatar_list.vue @@ -17,8 +17,6 @@ <script src="./avatar_list.js"></script> <style lang="scss"> -@import "../../variables"; - .avatars { display: flex; margin: 0; @@ -36,8 +34,7 @@ } .avatar-small { - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); height: 24px; width: 24px; } diff --git a/src/components/badge.style.js b/src/components/badge.style.js new file mode 100644 index 00000000..0697cac6 --- /dev/null +++ b/src/components/badge.style.js @@ -0,0 +1,30 @@ +export default { + name: 'Badge', + selector: '.badge', + validInnerComponents: [ + 'Text', + 'Icon' + ], + variants: { + notification: '.-notification' + }, + defaultRules: [ + { + component: 'Root', + directives: { + '--badgeNotification': 'color | --cRed' + } + }, + { + directives: { + background: '--cGreen' + } + }, + { + variant: 'notification', + directives: { + background: '--cRed' + } + } + ] +} diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index 705e20f5..9e2b0295 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -47,7 +47,6 @@ display: flex; flex: 1 0; margin: 0; - padding: 0.6em 1em; --emoji-size: 14px; diff --git a/src/components/border.style.js b/src/components/border.style.js new file mode 100644 index 00000000..a87ee9c8 --- /dev/null +++ b/src/components/border.style.js @@ -0,0 +1,13 @@ +export default { + name: 'Border', + selector: '/*border*/', + virtual: true, + defaultRules: [ + { + directives: { + textColor: '$mod(--parent, 10)', + textAuto: 'no-auto' + } + } + ] +} diff --git a/src/components/button.style.js b/src/components/button.style.js new file mode 100644 index 00000000..1bee8f8e --- /dev/null +++ b/src/components/button.style.js @@ -0,0 +1,101 @@ +export default { + name: 'Button', // Name of the component + selector: '.button-default', // CSS selector/prefix + // outOfTreeSelector: '' // out-of-tree selector is used when other components are laid over it but it's not part of the tree, see Underlay component + // States, system witll calculate ALL possible combinations of those and prepend "normal" to them + standalone "normal" state + states: { + // States are a bit expensive - the amount of combinations generated is about (1/6)n^3+n, so adding more state increased number of combination by an order of magnitude! + // All states inherit from "normal" state, there is no other inheirtance, i.e. hover+disabled only inherits from "normal", not from hover nor disabled. + // However, cascading still works, so resulting state will be result of merging of all relevant states/variants + // normal: '' // normal state is implicitly added, it is always included + toggled: '.toggled', + pressed: ':active', + hover: ':hover:not(:disabled)', + focused: ':focus-within', + disabled: ':disabled' + }, + // Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it. + variants: { + // Variants save on computation time since adding new variant just adds one more "set". + // normal: '', // you can override normal variant, it will be appenended to the main class + danger: '.danger' + // Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants. + // This (currently) is further multipled by number of places where component can exist. + }, + // This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever). + validInnerComponents: [ + 'Text', + 'Icon' + ], + // Default rules, used as "default theme", essentially. + defaultRules: [ + { + component: 'Root', + directives: { + '--defaultButtonHoverGlow': 'shadow | 0 0 4 --text', + '--defaultButtonShadow': 'shadow | 0 0 2 #000000', + '--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2), $borderSide(#000000, bottom, 0.2)', + '--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)' + } + }, + { + // component: 'Button', // no need to specify components every time unless you're specifying how other component should look + // like within it + directives: { + background: '--fg', + shadow: ['--defaultButtonShadow', '--defaultButtonBevel'], + roundness: 3 + } + }, + { + state: ['hover'], + directives: { + shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel'] + } + }, + { + state: ['pressed'], + directives: { + shadow: ['--defaultButtonShadow', '--pressedButtonBevel'] + } + }, + { + state: ['hover', 'pressed'], + directives: { + shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel'] + } + }, + { + state: ['toggled'], + directives: { + background: '--inheritedBackground,-14.2', + shadow: ['--defaultButtonShadow', '--pressedButtonBevel'] + } + }, + { + state: ['toggled', 'hover'], + directives: { + background: '--inheritedBackground,-14.2', + shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel'] + } + }, + { + state: ['disabled'], + directives: { + background: '$blend(--inheritedBackground, 0.25, --parent)', + shadow: ['--defaultButtonBevel'] + } + }, + { + component: 'Text', + parent: { + component: 'Button', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + } + ] +} diff --git a/src/components/button_unstyled.style.js b/src/components/button_unstyled.style.js new file mode 100644 index 00000000..435f9cc6 --- /dev/null +++ b/src/components/button_unstyled.style.js @@ -0,0 +1,95 @@ +export default { + name: 'ButtonUnstyled', + selector: '.button-unstyled', + states: { + toggled: '.toggled', + disabled: ':disabled', + hover: ':hover:not(:disabled)', + focused: ':focus-within' + }, + validInnerComponents: [ + 'Text', + 'Icon', + 'Badge' + ], + defaultRules: [ + { + directives: { + background: '#ffffff', + opacity: 0 + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['hover'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled', 'hover'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled', 'focused'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled', 'focused', 'hover'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Text', + parent: { + component: 'ButtonUnstyled', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + } + ] +} diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss index 43e7a5e4..8af710ae 100644 --- a/src/components/chat/chat.scss +++ b/src/components/chat/chat.scss @@ -11,15 +11,15 @@ .chat-view-body { box-sizing: border-box; - background-color: var(--chatBg, $fallback--bg); display: flex; flex-direction: column; width: 100%; overflow: visible; min-height: calc(100vh - var(--navbar-height)); margin: 0; - border-radius: 10px 10px 0 0; - border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0; + border-radius: var(--roundness); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; &::after { border-radius: 0; @@ -37,8 +37,6 @@ .footer { position: sticky; bottom: 0; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); z-index: 1; } @@ -61,8 +59,6 @@ position: absolute; right: 1.3em; top: -3.2em; - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); display: flex; justify-content: center; align-items: center; @@ -79,12 +75,6 @@ visibility: visible; } - i { - font-size: 1em; - color: $fallback--text; - color: var(--text, $fallback--text); - } - .unread-message-count { font-size: 0.8em; left: 50%; diff --git a/src/components/chat/chat.style.js b/src/components/chat/chat.style.js new file mode 100644 index 00000000..9ae2b7d7 --- /dev/null +++ b/src/components/chat/chat.style.js @@ -0,0 +1,19 @@ +export default { + name: 'Chat', + selector: '.chat-message-list', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Avatar', + 'ChatMessage' + ], + defaultRules: [ + { + directives: { + background: '--bg', + blur: '5px' + } + } + ] +} diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue index b1e5468c..6efe576b 100644 --- a/src/components/chat/chat.vue +++ b/src/components/chat/chat.vue @@ -26,7 +26,7 @@ </div> </div> <div - class="message-list" + class="chat-message-list message-list" :style="{ height: scrollableContainerHeight }" > <template v-if="!errorLoadingChat"> @@ -61,7 +61,7 @@ <FAIcon icon="chevron-down" /> <div v-if="newMessageCount" - class="badge badge-notification unread-chat-count unread-message-count" + class="badge -notification unread-chat-count unread-message-count" > {{ newMessageCount }} </div> @@ -95,6 +95,5 @@ <script src="./chat.js"></script> <style lang="scss"> -@import "../../variables"; @import "./chat"; </style> diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue index 27a475ed..b33ea67e 100644 --- a/src/components/chat_list/chat_list.vue +++ b/src/components/chat_list/chat_list.vue @@ -45,8 +45,6 @@ <script src="./chat_list.js"></script> <style lang="scss"> -@import "../../variables"; - .chat-list { min-height: 25em; margin-bottom: 0; @@ -57,8 +55,7 @@ font-size: 1.2em; display: flex; justify-content: center; - color: $fallback--text; - color: var(--faint, $fallback--text); + color: var(--textFaint); } </style> diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss index 3a84672b..9711b41d 100644 --- a/src/components/chat_list_item/chat_list_item.scss +++ b/src/components/chat_list_item/chat_list_item.scss @@ -1,8 +1,6 @@ .chat-list-item { display: flex; flex-direction: row; - padding: 0.75em; - height: 5em; overflow: hidden; box-sizing: border-box; cursor: pointer; @@ -11,11 +9,6 @@ outline: none; } - &:hover { - background-color: var(--selectedPost, $fallback--lightBg); - box-shadow: 0 0 3px 1px rgb(0 0 0 / 10%); - } - .chat-list-item-left { margin-right: 1em; } @@ -29,7 +22,7 @@ .heading { width: 100%; - display: inline-flex; + display: flex; justify-content: space-between; line-height: 1em; } @@ -47,18 +40,17 @@ } .chat-preview { - display: inline-flex; + display: flex; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin: 0.35em 0; - color: $fallback--text; - color: var(--faint, $fallback--text); + color: var(--textFaint); width: 100%; } a { - color: var(--faintLink, $fallback--link); + color: var(--linkFaint); text-decoration: none; pointer-events: none; } @@ -73,11 +65,6 @@ } } - .Avatar { - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); - } - .chat-preview-body { --emoji-size: 1.4em; diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue index 69ad609b..0f9d5c5f 100644 --- a/src/components/chat_list_item/chat_list_item.vue +++ b/src/components/chat_list_item/chat_list_item.vue @@ -36,7 +36,7 @@ /> <div v-if="chat.unread > 0" - class="badge badge-notification unread-chat-count" + class="badge -notification unread-chat-count" > {{ chat.unread }} </div> @@ -48,6 +48,5 @@ <script src="./chat_list_item.js"></script> <style lang="scss"> -@import "../../variables"; @import "./chat_list_item"; </style> diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss index fd5b7aa4..f7254ea3 100644 --- a/src/components/chat_message/chat_message.scss +++ b/src/components/chat_message/chat_message.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .chat-message-wrapper { &.hovered-message-chain { .animated.Avatar { @@ -27,12 +25,6 @@ .menu-icon { cursor: pointer; - - &:hover, - .extra-button-popover.open & { - color: $fallback--text; - color: var(--text, $fallback--text); - } } .popover { @@ -61,10 +53,12 @@ } .status { - border-radius: $fallback--chatMessageRadius; - border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius); + background-color: var(--background); + color: var(--text); + border-radius: var(--roundness); display: flex; padding: 0.75em; + border: 1px solid var(--border); } .created-at { @@ -97,8 +91,7 @@ .error { .status-content.media-body, .created-at { - color: $fallback--cRed; - color: var(--badgeNotification, $fallback--cRed); + color: var(--badgeNotification); } } @@ -117,16 +110,6 @@ align-content: end; justify-content: flex-end; - a { - color: var(--chatMessageOutgoingLink, $fallback--link); - } - - .status { - color: var(--chatMessageOutgoingText, $fallback--text); - background-color: var(--chatMessageOutgoingBg, $fallback--lightBg); - border: 1px solid var(--chatMessageOutgoingBorder, --lightBg); - } - .chat-message-inner { align-items: flex-end; } @@ -137,22 +120,6 @@ } .incoming { - a { - color: var(--chatMessageIncomingLink, $fallback--link); - } - - .status { - color: var(--chatMessageIncomingText, $fallback--text); - background-color: var(--chatMessageIncomingBg, $fallback--bg); - border: 1px solid var(--chatMessageIncomingBorder, --border); - } - - .created-at { - a { - color: var(--chatMessageIncomingText, $fallback--text); - } - } - .chat-message-menu { left: 0.4rem; } @@ -176,6 +143,5 @@ margin: 1.4em 0; font-size: 0.9em; user-select: none; - color: $fallback--text; - color: var(--faintedText, $fallback--text); + color: var(--textFaint); } diff --git a/src/components/chat_message/chat_message.style.js b/src/components/chat_message/chat_message.style.js new file mode 100644 index 00000000..9b57ad37 --- /dev/null +++ b/src/components/chat_message/chat_message.style.js @@ -0,0 +1,30 @@ +export default { + name: 'ChatMessage', + selector: '.chat-message', + variants: { + outgoing: '.outgoing' + }, + validInnerComponents: [ + 'Text', + 'Icon', + 'Border', + 'Button', + 'RichContent', + 'Attachment', + 'PollGraph' + ], + defaultRules: [ + { + directives: { + background: '--bg, 2', + backgroundNoCssColor: 'yes' + } + }, + { + variant: 'outgoing', + directives: { + background: '--bg, 5' + } + } + ] +} diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue index 381574c3..166889d7 100644 --- a/src/components/chat_message/chat_message.vue +++ b/src/components/chat_message/chat_message.vue @@ -53,7 +53,7 @@ <template #content> <div class="dropdown-menu"> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click="deleteMessage" > <FAIcon icon="times" /> {{ $t("chats.delete") }} diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss index b145ecf9..b84e0beb 100644 --- a/src/components/chat_new/chat_new.scss +++ b/src/components/chat_new/chat_new.scss @@ -16,11 +16,6 @@ padding-bottom: 0.7rem; } - .basic-user-card:hover { - cursor: pointer; - background-color: var(--selectedPost, $fallback--lightBg); - } - .go-back-button { text-align: center; line-height: 1; diff --git a/src/components/chat_new/chat_new.vue b/src/components/chat_new/chat_new.vue index 52306c1d..673ba56b 100644 --- a/src/components/chat_new/chat_new.vue +++ b/src/components/chat_new/chat_new.vue @@ -16,27 +16,29 @@ /> </button> </div> - <div class="input-wrap"> - <div class="input-search"> - <FAIcon - class="search-icon fa-scale-110 fa-old-padding" - icon="search" - /> + <div class="panel-body"> + <div class="input-wrap"> + <div class="input-search"> + <FAIcon + class="search-icon fa-scale-110 fa-old-padding" + icon="search" + /> + </div> + <input + ref="search" + v-model="query" + class="input" + placeholder="Search people" + @input="onInput" + > </div> - <input - ref="search" - v-model="query" - placeholder="Search people" - @input="onInput" - > - </div> - <div class="member-list"> - <div - v-for="user in availableUsers" - :key="user.id" - class="member" - > - <div @click.capture.prevent="goToChat(user)"> + <div class="member-list"> + <div + v-for="user in availableUsers" + :key="user.id" + class="list-item" + @click.capture.prevent="goToChat(user)" + > <BasicUserCard :user="user" /> </div> </div> @@ -46,6 +48,5 @@ <script src="./chat_new.js"></script> <style lang="scss"> -@import "../../variables"; @import "./chat_new"; </style> diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue index 93db4fa7..68ee7a5a 100644 --- a/src/components/chat_title/chat_title.vue +++ b/src/components/chat_title/chat_title.vue @@ -26,8 +26,6 @@ <script src="./chat_title.js"></script> <style lang="scss"> -@import "../../variables"; - .chat-title { display: flex; overflow: hidden; @@ -54,8 +52,7 @@ margin-right: 0.5em; height: 1.5em; width: 1.5em; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); &.animated::before { display: none; diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index 7139d4fc..6261bf3a 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -1,16 +1,21 @@ <template> <label class="checkbox" - :class="{ disabled, indeterminate }" + :class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }" > <input type="checkbox" + class="visible-for-screenreader-only" :disabled="disabled" :checked="modelValue" :indeterminate="indeterminate" @change="$emit('update:modelValue', $event.target.checked)" > - <i class="checkbox-indicator" /> + <i + class="input -checkbox checkbox-indicator" + :aria-hidden="true" + @transitionend.capture="onTransitionEnd" + /> <span v-if="!!$slots.default" class="label" @@ -27,21 +32,44 @@ export default { 'indeterminate', 'disabled' ], - emits: ['update:modelValue'] + emits: ['update:modelValue'], + data: (vm) => ({ + indeterminateTransitionFix: vm.indeterminate + }), + watch: { + indeterminate (e) { + if (e) { + this.indeterminateTransitionFix = true + } + } + }, + methods: { + onTransitionEnd (e) { + if (!this.indeterminate) { + this.indeterminateTransitionFix = false + } + } + } } </script> <style lang="scss"> -@import "../../variables"; +@import "../../mixins"; .checkbox { position: relative; display: inline-block; min-height: 1.2em; - &-indicator { + & > &-indicator { + /* Reset .input stuff */ + padding: 0; + margin: 0; position: relative; + line-height: inherit; + display: inline; padding-left: 1.2em; + box-shadow: none; } &-indicator::before { @@ -53,12 +81,9 @@ export default { transition: color 200ms; width: 1.1em; height: 1.1em; - border-radius: $fallback--checkboxRadius; - border-radius: var(--checkboxRadius, $fallback--checkboxRadius); - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); + border-radius: var(--roundness); + box-shadow: var(--shadow); + background-color: var(--background); vertical-align: top; text-align: center; line-height: 1.1em; @@ -75,23 +100,24 @@ export default { } .label { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--text); } } input[type="checkbox"] { - display: none; - &:checked + .checkbox-indicator::before { - color: $fallback--text; - color: var(--inputText, $fallback--text); + color: var(--text); } &:indeterminate + .checkbox-indicator::before { content: "–"; - color: $fallback--text; - color: var(--inputText, $fallback--text); + color: var(--text); + } + } + + &.indeterminate-fix { + input[type="checkbox"] + .checkbox-indicator::before { + content: "–"; } } diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss index ca46199a..b0fc879f 100644 --- a/src/components/color_input/color_input.scss +++ b/src/components/color_input/color_input.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .color-input { display: inline-flex; @@ -11,9 +9,8 @@ padding: 0.2em 8px; input { + color: var(--text); background: none; - color: $fallback--lightText; - color: var(--inputText, $fallback--lightText); border: none; padding: 0; margin: 0; @@ -23,21 +20,38 @@ min-width: 3em; padding: 0; } + } + + .nativeColor { + cursor: pointer; + flex: 0 0 auto; - &.nativeColor { - flex: 0 0 2em; - min-width: 2em; - align-self: stretch; - min-height: 100%; + input { + appearance: none; + max-width: 0; + min-width: 0; + max-height: 0; + /* stylelint-disable-next-line declaration-no-important */ + opacity: 0 !important; } } .computedIndicator, + .validIndicator, + .invalidIndicator, .transparentIndicator { flex: 0 0 2em; + margin: 0 0.5em; min-width: 2em; align-self: stretch; - min-height: 100%; + min-height: 1.5em; + border-radius: var(--roundness); + } + + .invalidIndicator { + background: transparent; + box-sizing: border-box; + border: 2px solid var(--cRed); } .transparentIndicator { @@ -58,11 +72,13 @@ &::after { top: 0; left: 0; + border-top-left-radius: var(--roundness); } &::before { bottom: 0; right: 0; + border-bottom-right-radius: var(--roundness); } } } diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue index dfc084f9..66ee9d53 100644 --- a/src/components/color_input/color_input.vue +++ b/src/components/color_input/color_input.vue @@ -25,30 +25,51 @@ :disabled="!present || disabled" @input="$emit('update:modelValue', $event.target.value)" > - <input + <div v-if="validColor" - :id="name" - class="nativeColor unstyled" - type="color" - :value="modelValue || fallback" - :disabled="!present || disabled" - @input="$emit('update:modelValue', $event.target.value)" - > + class="validIndicator" + :style="{backgroundColor: modelValue || fallback}" + /> <div - v-if="transparentColor" + v-else-if="transparentColor" class="transparentIndicator" /> <div - v-if="computedColor" + v-else-if="computedColor" class="computedIndicator" :style="{backgroundColor: fallback}" /> + <div + v-else + class="invalidIndicator" + /> + <label class="nativeColor"> + <FAIcon icon="eye-dropper" /> + <input + :id="name" + class="unstyled" + type="color" + :value="modelValue || fallback" + :disabled="!present || disabled" + @input="$emit('update:modelValue', $event.target.value)" + > + </label> </div> </div> </template> <script> import Checkbox from '../checkbox/checkbox.vue' import { hex2rgb } from '../../services/color_convert/color_convert.js' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEyeDropper +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEyeDropper +) + export default { components: { Checkbox @@ -108,12 +129,3 @@ export default { } </script> <style lang="scss" src="./color_input.scss"></style> - -<style lang="scss"> -.color-control { - input.text-input { - max-width: 7em; - flex: 1; - } -} -</style> diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 85e6d8ad..a94d2130 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -56,7 +56,8 @@ const conversation = { expanded: false, threadDisplayStatusObject: {}, // id => 'showing' | 'hidden' statusContentPropertiesObject: {}, - inlineDivePosition: null + inlineDivePosition: null, + loadStatusError: null } }, props: [ @@ -392,11 +393,15 @@ const conversation = { this.setHighlight(this.originalStatusId) }) } else { + this.loadStatusError = null this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId }) .then((status) => { this.$store.dispatch('addNewStatuses', { statuses: [status] }) this.fetchConversation() }) + .catch((error) => { + this.loadStatusError = error + }) } }, getReplies (id) { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 7577129e..526de5c3 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -28,7 +28,27 @@ class="rightside-button" /> </div> - <div class="conversation-body panel-body"> + <div + v-if="isPage && !status" + class="conversation-body" + :class="{ 'panel-body': isExpanded }" + > + <p v-if="!loadStatusError"> + <FAIcon + spin + icon="circle-notch" + /> + {{ $t('status.loading') }} + </p> + <p v-else> + {{ $t('status.load_error', { error: loadStatusError }) }} + </p> + </div> + <div + v-else + class="conversation-body" + :class="{ 'panel-body': isExpanded }" + > <div v-if="isTreeView" class="thread-body" @@ -203,6 +223,7 @@ </div> <div v-else + class="Conversation -hidden" :style="hiddenStyle" /> </template> @@ -210,14 +231,17 @@ <script src="./conversation.js"></script> <style lang="scss"> -@import "../../variables"; - .Conversation { z-index: 1; + &.-hidden { + background: var(--__panel-background); + backdrop-filter: var(--__panel-backdrop-filter); + } + .conversation-dive-to-top-level-box { - padding: var(--status-margin, $status-margin); - border-bottom: 1px solid var(--border, $fallback--border); + padding: var(--status-margin); + border-bottom: 1px solid var(--border); border-radius: 0; /* Make the button stretch along the whole row */ @@ -227,20 +251,22 @@ } .thread-ancestors { - margin-left: var(--status-margin, $status-margin); - border-left: 2px solid var(--border, $fallback--border); + margin-left: var(--status-margin); + border-left: 2px solid var(--border); } - .thread-ancestor.-faded .StatusContent { - --link: var(--faintLink); - --text: var(--faint); - - color: var(--text); + .thread-ancestor.-faded .RichContent { + /* stylelint-disable declaration-no-important */ + --text: var(--textFaint) !important; + --link: var(--linkFaint) !important; + --funtextGreentext: var(--funtextGreentextFaint) !important; + --funtextCyantext: var(--funtextCyantextFaint) !important; + /* stylelint-enable declaration-no-important */ } .thread-ancestor-dive-box { - padding-left: var(--status-margin, $status-margin); - border-bottom: 1px solid var(--border, $fallback--border); + padding-left: var(--status-margin); + border-bottom: 1px solid var(--border); border-radius: 0; /* Make the button stretch along the whole row */ @@ -253,16 +279,17 @@ } .thread-ancestor-dive-box-inner { - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); } .conversation-status { - border-bottom: 1px solid var(--border, $fallback--border); + border-bottom: 1px solid var(--border); border-radius: 0; } .thread-ancestor-has-other-replies .conversation-status, - &:last-child .conversation-status, + &:last-child:not(.-expanded) .conversation-status, + &.-expanded .conversation-status:last-child, .thread-ancestor:last-child .conversation-status, .thread-ancestor:last-child .thread-ancestor-dive-box, &.-expanded .thread-tree .conversation-status { @@ -270,20 +297,36 @@ } .thread-ancestors + .thread-tree > .conversation-status { - border-top: 1px solid var(--border, $fallback--border); + border-top: 1px solid var(--border); } /* expanded conversation in timeline */ &.status-fadein.-expanded .thread-body { - border-left: 4px solid $fallback--cRed; - border-left-color: var(--cRed, $fallback--cRed); - border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; - border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); - border-bottom: 1px solid var(--border, $fallback--border); + border-left: 4px solid var(--cRed); + border-radius: var(--roundness); + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom: 1px solid var(--border); } &.-expanded.status-fadein { - margin: calc(var(--status-margin, $status-margin) / 2); + --___margin: calc(var(--status-margin) / 2); + + background: var(--background); + margin: var(--___margin); + + &::before { + z-index: -1; + content: ""; + display: block; + position: absolute; + top: calc(var(--___margin) * -1); + bottom: calc(var(--___margin) * -1); + left: calc(var(--___margin) * -1); + right: calc(var(--___margin) * -1); + background: var(--background); + backdrop-filter: var(--__panel-backdrop-filter); + } } } </style> diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js index 745b1a81..f6a2e294 100644 --- a/src/components/desktop_nav/desktop_nav.js +++ b/src/components/desktop_nav/desktop_nav.js @@ -107,7 +107,10 @@ export default { this.searchBarHidden = hidden }, openSettingsModal () { - this.$store.dispatch('openSettingsModal') + this.$store.dispatch('openSettingsModal', 'user') + }, + openAdminModal () { + this.$store.dispatch('openSettingsModal', 'admin') } } } diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss index c7e02936..61d2541c 100644 --- a/src/components/desktop_nav/desktop_nav.scss +++ b/src/components/desktop_nav/desktop_nav.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .DesktopNav { width: 100%; z-index: var(--ZI_navbar); @@ -9,7 +7,7 @@ } a { - color: var(--topBarLink, $fallback--link); + color: var(--link); } .inner-nav { @@ -54,27 +52,7 @@ .button-default { &, svg { - color: $fallback--text; - color: var(--btnTopBarText, $fallback--text); - } - - &:active { - background-color: $fallback--fg; - background-color: var(--btnPressedTopBar, $fallback--fg); - color: $fallback--text; - color: var(--btnPressedTopBarText, $fallback--text); - } - - &:disabled { - color: $fallback--text; - color: var(--btnDisabledTopBarText, $fallback--text); - } - - &.toggled { - color: $fallback--text; - color: var(--btnToggledTopBarText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnToggledTopBar, $fallback--fg); + color: var(--text); } } @@ -94,8 +72,7 @@ mask-repeat: no-repeat; mask-position: center; mask-size: contain; - background-color: $fallback--fg; - background-color: var(--topBarText, $fallback--fg); + background-color: var(--text); position: absolute; top: 0; bottom: 0; @@ -116,8 +93,7 @@ text-align: center; .svg-inline--fa { - color: $fallback--link; - color: var(--topBarLink, $fallback--link); + color: var(--link); } } diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index dc8bbfd3..49382f8e 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -48,20 +48,19 @@ icon="cog" /> </button> - <a + <button v-if="currentUser && currentUser.role === 'admin'" - href="/pleroma/admin/#/login-pleroma" - class="nav-icon" + class="button-unstyled nav-icon" target="_blank" :title="$t('nav.administration')" - @click.stop + @click.stop="openAdminModal" > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" icon="tachometer-alt" /> - </a> + </button> <span class="spacer" /> <button v-if="currentUser" diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue index 341cf105..3bceff22 100644 --- a/src/components/dialog_modal/dialog_modal.vue +++ b/src/components/dialog_modal/dialog_modal.vue @@ -12,7 +12,7 @@ <slot name="header" /> </div> </div> - <div class="dialog-modal-content"> + <div class="panel-body dialog-modal-content"> <slot name="default" /> </div> <div class="dialog-modal-footer user-interactions panel-footer"> @@ -25,8 +25,6 @@ <script src="./dialog_modal.js"></script> <style lang="scss"> -@import "../../variables"; - // TODO: unify with other modals. .dark-overlay { &::before { @@ -54,8 +52,6 @@ z-index: 2001; cursor: default; display: block; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); .dialog-modal-heading { .title { @@ -66,18 +62,13 @@ .dialog-modal-content { margin: 0; padding: 1rem; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); white-space: normal; } .dialog-modal-footer { margin: 0; padding: 0.5em; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - border-top: 1px solid $fallback--border; - border-top: 1px solid var(--border, $fallback--border); + border-top: 1px solid var(--border); display: flex; justify-content: flex-end; diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index 8a8d098d..9baf63f2 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -1,4 +1,5 @@ import Completion from '../../services/completion/completion.js' +import genRandomSeed from '../../services/random_seed/random_seed.service.js' import EmojiPicker from '../emoji_picker/emoji_picker.vue' import Popover from 'src/components/popover/popover.vue' import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue' @@ -110,7 +111,7 @@ const EmojiInput = { }, data () { return { - randomSeed: `${Math.random()}`.replace('.', '-'), + randomSeed: genRandomSeed(), input: undefined, caretEl: undefined, highlighted: -1, @@ -134,6 +135,9 @@ const EmojiInput = { padEmoji () { return this.$store.getters.mergedConfig.padEmoji }, + defaultCandidateIndex () { + return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1 + }, preText () { return this.modelValue.slice(0, this.caret) }, @@ -287,7 +291,7 @@ const EmojiInput = { ...rest, img: imageUrl || '' })) - this.highlighted = -1 + this.highlighted = this.defaultCandidateIndex this.$refs.screenReaderNotice.announce( this.$tc('tool_tip.autocomplete_available', this.suggestions.length, diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue index 7f9ecc99..9bd5c8f4 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -1,7 +1,7 @@ <template> <div ref="root" - class="emoji-input" + class="input emoji-input" :class="{ 'with-picker': !hideEmojiButton }" > <slot @@ -68,9 +68,9 @@ v-for="(suggestion, index) in suggestions" :id="suggestionItemId(index)" :key="index" - class="autocomplete-item" + class="menu-item autocomplete-item" role="option" - :class="{ highlighted: index === highlighted }" + :class="{ '-active': index === highlighted }" :aria-label="autoCompleteItemLabel(suggestion)" :aria-selected="index === highlighted" @click.stop.prevent="onClick($event, suggestion)" @@ -110,9 +110,8 @@ <script src="./emoji_input.js"></script> <style lang="scss"> -@import "../../variables"; - -.emoji-input { +.input.emoji-input { + padding: 0; display: flex; flex-direction: column; position: relative; @@ -127,8 +126,7 @@ line-height: 24px; &:hover i { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } @@ -145,6 +143,12 @@ input, textarea { flex: 1 0 auto; + color: inherit; + /* stylelint-disable-next-line declaration-no-important */ + background: none !important; + box-shadow: none; + border: none; + outline: none; } &.with-picker input { @@ -179,26 +183,28 @@ position: absolute; } - &-item { + &-item.menu-item { display: flex; - cursor: pointer; - padding: 0.2em 0.4em; - border-bottom: 1px solid rgb(0 0 0 / 40%); - height: 32px; + padding-top: 0; + padding-bottom: 0; .image { - width: 32px; - height: 32px; - line-height: 32px; + width: calc(var(--__line-height) + var(--__vertical-gap) * 2); + height: calc(var(--__line-height) + var(--__vertical-gap) * 2); + line-height: var(--__line-height); text-align: center; - font-size: 32px; - margin-right: 4px; + margin-right: var(--__horizontal-gap); img { - width: 32px; - height: 32px; + width: calc(var(--__line-height) + var(--__vertical-gap) * 2); + height: calc(var(--__line-height) + var(--__vertical-gap) * 2); object-fit: contain; } + + span { + font-size: var(--__line-height); + line-height: var(--__line-height); + } } .label { @@ -216,17 +222,6 @@ line-height: 9px; } } - - &.highlighted { - background-color: $fallback--fg; - background-color: var(--selectedMenuPopover, $fallback--fg); - color: var(--selectedMenuPopoverText, $fallback--text); - - --faint: var(--selectedMenuPopoverFaintText, $fallback--faint); - --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); - --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - } } } </style> diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js index adaa879e..e746dcd7 100644 --- a/src/components/emoji_input/suggestor.js +++ b/src/components/emoji_input/suggestor.js @@ -94,8 +94,9 @@ export const suggestUsers = ({ dispatch, state }) => { const newSuggestions = state.users.users.filter( user => - user.screen_name.toLowerCase().startsWith(noPrefix) || - user.name.toLowerCase().startsWith(noPrefix) + user.screen_name && user.name && ( + user.screen_name.toLowerCase().startsWith(noPrefix) || + user.name.toLowerCase().startsWith(noPrefix)) ).slice(0, 20).sort((a, b) => { let aScore = 0 let bScore = 0 diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index 0d7ca812..9ea5c877 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -98,8 +98,14 @@ const EmojiPicker = { required: false, type: Boolean, default: false + }, + hideCustomEmoji: { + required: false, + type: Boolean, + default: false } }, + inject: ['popoversZLayer'], data () { return { keyword: '', @@ -108,11 +114,13 @@ const EmojiPicker = { groupsScrolledClass: 'scrolled-top', keepOpen: false, customEmojiTimeout: null, + hideCustomEmojiInPicker: false, // Lazy-load only after the first time `showing` becomes true. contentLoaded: false, groupRefs: {}, emojiRefs: {}, filteredEmojiGroups: [], + emojiSize: 0, width: 0 } }, @@ -123,9 +131,28 @@ const EmojiPicker = { Popover }, methods: { + updateEmojiSize () { + const css = window.getComputedStyle(this.$refs.popover.$el) + const emojiSize = css.getPropertyValue('--emojiSize') + const emojiSizeUnit = emojiSize.replace(/[0-9,.]+/, '') + const emojiSizeValue = Number(emojiSize.replace(/[^0-9,.]+/, '')) + const fontSize = css.getPropertyValue('font-size').replace(/[^0-9,.]+/, '') + + let emojiSizeReal + if (emojiSizeUnit.endsWith('em')) { + emojiSizeReal = emojiSizeValue * fontSize + } else { + emojiSizeReal = emojiSizeValue + } + + const fullEmojiSize = emojiSizeReal + (2 * 0.2 * fontSize) + this.emojiSize = fullEmojiSize + }, showPicker () { this.$refs.popover.showPopover() - this.onShowing() + this.$nextTick(() => { + this.onShowing() + }) }, hidePicker () { this.$refs.popover.hidePopover() @@ -217,6 +244,7 @@ const EmojiPicker = { }, onShowing () { const oldContentLoaded = this.contentLoaded + this.updateEmojiSize() this.recalculateItemPerRow() this.$nextTick(() => { this.$refs.search.focus() @@ -259,16 +287,20 @@ const EmojiPicker = { }, computed: { minItemSize () { - return this.emojiHeight + return this.emojiSize }, - emojiHeight () { - return 32 + 4 + // used to watch it + fontSize () { + this.$nextTick(() => { + this.updateEmojiSize() + }) + return this.$store.getters.mergedConfig.fontSize }, - emojiWidth () { - return 32 + 4 + emojiHeight () { + return this.emojiSize }, itemPerRow () { - return this.width ? Math.floor(this.width / this.emojiWidth - 1) : 6 + return this.width ? Math.floor(this.width / this.emojiSize) : 6 }, activeGroupView () { return this.showingStickers ? '' : this.activeGroup @@ -280,6 +312,9 @@ const EmojiPicker = { return 0 }, allCustomGroups () { + if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) { + return {} + } const emojis = this.$store.getters.groupedCustomEmojis if (emojis.unpacked) { emojis.unpacked.text = this.$t('emoji.unpacked') @@ -342,6 +377,9 @@ const EmojiPicker = { return emoji.displayText } + }, + isInModal () { + return this.popoversZLayer === 'modals' } } } diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss index 5bcff33b..12c09388 100644 --- a/src/components/emoji_picker/emoji_picker.scss +++ b/src/components/emoji_picker/emoji_picker.scss @@ -1,62 +1,55 @@ -@import "../../variables"; - -$emoji-picker-header-height: 36px; -$emoji-picker-header-picture-width: 32px; -$emoji-picker-header-picture-height: 32px; -$emoji-picker-emoji-size: 32px; - .emoji-picker { + --__emoji-picker-header: 2.2em; + width: 25em; max-width: calc(100vw - 20px); // popover gives 10px margin from window edge display: flex; flex-direction: column; - background-color: $fallback--bg; - background-color: var(--popover, $fallback--bg); - color: $fallback--link; - color: var(--popoverText, $fallback--link); - - --faint: var(--popoverFaintText, $fallback--faint); - --faintLink: var(--popoverFaintLink, $fallback--faint); - --lightText: var(--popoverLightText, $fallback--lightText); - --icon: var(--popoverIcon, $fallback--icon); &-header-image { display: inline-flex; justify-content: center; align-items: center; - width: $emoji-picker-header-picture-width; - max-width: $emoji-picker-header-picture-width; - height: $emoji-picker-header-picture-height; - max-height: $emoji-picker-header-picture-height; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); .still-image { - max-width: 100%; - max-height: 100%; - height: 100%; - width: 100%; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); object-fit: contain; + + --_still_image-label-scale: 0.5; } } .keep-open, - .too-many-emoji { - padding: 7px; + .too-many-emoji, + .hide-custom-emoji { + padding: 0.5em; line-height: normal; } + .hide-custom-emoji { + padding-top: 0; + } + .too-many-emoji { display: flex; flex-direction: column; } .keep-open-label { - padding: 0 7px; + padding: 0 0.5em; display: flex; } .heading { display: flex; - padding: 10px 7px 5px; + padding: 0.7em 0.5em 0; } .content { @@ -71,14 +64,14 @@ $emoji-picker-emoji-size: 32px; display: flex; flex-flow: row nowrap; overflow-x: auto; + overflow-y: hidden; } .additional-tabs { display: flex; border-left: 1px solid; - border-left-color: $fallback--icon; - border-left-color: var(--icon, $fallback--icon); - padding-left: 7px; + border-left-color: var(--border); + padding-left: 0.5em; flex: 0 0 auto; } @@ -87,30 +80,29 @@ $emoji-picker-emoji-size: 32px; flex-basis: auto; display: flex; align-content: center; + scrollbar-width: thin; &-item { - padding: 0 7px; + padding: 0 0.5em; cursor: pointer; - font-size: 1.85em; - width: $emoji-picker-header-picture-width; - max-width: $emoji-picker-header-picture-width; - height: $emoji-picker-header-picture-height; - max-height: $emoji-picker-header-picture-height; + width: var(--__emoji-picker-header); + max-width: var(--__emoji-picker-header); + height: var(--__emoji-picker-header); + max-height: var(--__emoji-picker-header); display: flex; align-items: center; + .svg-inline--fa { + font-size: 1.85em; + } + &.disabled { opacity: 0.5; pointer-events: none; } - &.active { - border-bottom: 4px solid; - - svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } + &.toggled { + border-bottom: 0.2em solid; } } } @@ -137,7 +129,7 @@ $emoji-picker-emoji-size: 32px; .emoji { &-search { - padding: 5px; + padding: 0.3em; flex: 0 0 auto; input { @@ -151,6 +143,7 @@ $emoji-picker-emoji-size: 32px; flex: 1 1 1px; position: relative; overflow: auto; + scrollbar-gutter: stable both-edges; user-select: none; mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, @@ -177,13 +170,13 @@ $emoji-picker-emoji-size: 32px; display: flex; align-items: center; flex-wrap: wrap; - padding-left: 5px; justify-content: left; &-title { font-size: 0.85em; width: 100%; margin: 0; + padding-left: 0.3em; &.disabled { display: none; @@ -192,24 +185,28 @@ $emoji-picker-emoji-size: 32px; } &-item { - width: $emoji-picker-emoji-size; - height: $emoji-picker-emoji-size; + width: var(--emoji-size); + height: var(--emoji-size); box-sizing: border-box; display: flex; - line-height: $emoji-picker-emoji-size; + line-height: var(--emoji-size); align-items: center; justify-content: center; - margin: 4px; + margin: 0.2em; cursor: pointer; .emoji-picker-emoji.-custom { object-fit: contain; - max-width: 100%; - max-height: 100%; + width: var(--emoji-size); + max-width: var(--emoji-size); + height: var(--emoji-size); + max-height: var(--emoji-size); + + --_still_image-label-scale: 0.5; } .emoji-picker-emoji.-unicode { - font-size: 24px; + font-size: 1.6em; overflow: hidden; } } diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue index 6972164b..a3dc8f9e 100644 --- a/src/components/emoji_picker/emoji_picker.vue +++ b/src/components/emoji_picker/emoji_picker.vue @@ -3,25 +3,32 @@ ref="popover" trigger="click" popover-class="emoji-picker popover-default" - :trigger-attrs="{ 'aria-hidden': true }" + :trigger-attrs="{ 'aria-hidden': true, tabindex: -1 }" @show="onPopoverShown" @close="onPopoverClosed" > <template #content> <div class="heading"> + <!-- + Body scroll lock needs to be on every scrollable element on safari iOS. + Here we tell it to enable scrolling for this element. + See https://github.com/willmcpo/body-scroll-lock#vanilla-js + --> <span ref="header" + v-body-scroll-lock="isInModal" class="emoji-tabs" > <span v-for="group in filteredEmojiGroups" :ref="setGroupRef('group-header-' + group.id)" :key="group.id" - class="emoji-tabs-item" + class="button-unstyled emoji-tabs-item" :class="{ - active: activeGroupView === group.id + toggled: activeGroupView === group.id }" :title="group.text" + role="button" @click.prevent="highlight(group.id)" > <span @@ -45,8 +52,8 @@ class="additional-tabs" > <span - class="stickers-tab-icon additional-tabs-item" - :class="{active: showingStickers}" + class="button-unstyled stickers-tab-icon additional-tabs-item" + :class="{toggled: showingStickers}" :title="$t('emoji.stickers')" @click.prevent="toggleStickers" > @@ -70,16 +77,19 @@ ref="search" v-model="keyword" type="text" - class="form-control" + class="input form-control" :placeholder="$t('emoji.search_emoji')" @input="$event.target.composing = false" > </div> + <!-- Enables scrolling for this element on safari iOS. See comments for header. --> <DynamicScroller ref="emoji-groups" + v-body-scroll-lock="isInModal" class="emoji-groups" :class="groupsScrolledClass" :min-item-size="minItemSize" + :buffer="minItemSize" :items="emojiItems" :emit-update="true" @update="onScroll" @@ -108,6 +118,7 @@ :key="group.id + emoji.displayText" :title="maybeLocalizedEmojiName(emoji)" class="emoji-item" + role="button" @click.stop.prevent="onEmoji(emoji)" > <span @@ -118,6 +129,7 @@ v-else class="emoji-picker-emoji -custom" loading="lazy" + :alt="maybeLocalizedEmojiName(emoji)" :src="emoji.imageUrl" :data-emoji-name="group.id + emoji.displayText" /> @@ -131,6 +143,17 @@ {{ $t('emoji.keep_open') }} </Checkbox> </div> + <div + v-if="!hideCustomEmoji" + class="hide-custom-emoji" + > + <Checkbox + v-model="hideCustomEmojiInPicker" + @change="onShowing" + > + {{ $t('emoji.hide_custom_emoji') }} + </Checkbox> + </div> </div> <div v-if="showingStickers" diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js index bb11b840..4d5c6c5a 100644 --- a/src/components/emoji_reactions/emoji_reactions.js +++ b/src/components/emoji_reactions/emoji_reactions.js @@ -1,5 +1,17 @@ import UserAvatar from '../user_avatar/user_avatar.vue' import UserListPopover from '../user_list_popover/user_list_popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faPlus, + faMinus, + faCheck +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faPlus, + faMinus, + faCheck +) const EMOJI_REACTION_COUNT_CUTOFF = 12 @@ -33,6 +45,9 @@ const EmojiReactions = { }, loggedIn () { return !!this.$store.state.users.currentUser + }, + remoteInteractionLink () { + return this.$store.getters.remoteInteractionLink({ statusId: this.status.id }) } }, methods: { @@ -42,10 +57,10 @@ const EmojiReactions = { reactedWith (emoji) { return this.status.emoji_reactions.find(r => r.name === emoji).me }, - fetchEmojiReactionsByIfMissing () { + async fetchEmojiReactionsByIfMissing () { const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts) if (hasNoAccounts) { - this.$store.dispatch('fetchEmojiReactionsBy', this.status.id) + return await this.$store.dispatch('fetchEmojiReactionsBy', this.status.id) } }, reactWith (emoji) { @@ -54,14 +69,26 @@ const EmojiReactions = { unreact (emoji) { this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) }, - emojiOnClick (emoji, event) { + async emojiOnClick (emoji, event) { if (!this.loggedIn) return + await this.fetchEmojiReactionsByIfMissing() if (this.reactedWith(emoji)) { this.unreact(emoji) } else { this.reactWith(emoji) } + }, + counterTriggerAttrs (reaction) { + return { + class: [ + 'btn', + 'button-default', + 'emoji-reaction-count-button', + { '-picked-reaction': this.reactedWith(reaction.name) } + ], + 'aria-label': this.$tc('status.reaction_count_label', reaction.count, { num: reaction.count }) + } } } } diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue index a63daa97..3ab4c125 100644 --- a/src/components/emoji_reactions/emoji_reactions.vue +++ b/src/components/emoji_reactions/emoji_reactions.vue @@ -1,20 +1,64 @@ <template> <div class="EmojiReactions"> - <UserListPopover + <span v-for="(reaction) in emojiReactions" - :key="reaction.name" - :users="accountsForEmoji[reaction.name]" + :key="reaction.url || reaction.name" + class="emoji-reaction-container btn-group" > - <button + <component + :is="loggedIn ? 'button' : 'a'" + v-bind="!loggedIn ? { href: remoteInteractionLink } : {}" + role="button" class="emoji-reaction btn button-default" - :class="{ '-picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" + :class="{ '-picked-reaction': reactedWith(reaction.name) }" + :title="reaction.url ? reaction.name : undefined" + :aria-pressed="reactedWith(reaction.name)" @click="emojiOnClick(reaction.name, $event)" - @mouseenter="fetchEmojiReactionsByIfMissing()" > - <span class="reaction-emoji">{{ reaction.name }}</span> - <span>{{ reaction.count }}</span> - </button> - </UserListPopover> + <span + class="reaction-emoji" + > + <img + v-if="reaction.url" + :src="reaction.url" + class="reaction-emoji-content" + width="1em" + > + <span + v-else + class="reaction-emoji reaction-emoji-content" + >{{ reaction.name }}</span> + </span> + <FALayers> + <FAIcon + v-if="reactedWith(reaction.name)" + class="active-marker" + transform="shrink-6 up-9" + icon="check" + /> + <FAIcon + v-if="!reactedWith(reaction.name)" + class="focus-marker" + transform="shrink-6 up-9" + icon="plus" + /> + <FAIcon + v-else + class="focus-marker" + transform="shrink-6 up-9" + icon="minus" + /> + </FALayers> + </component> + <UserListPopover + :users="accountsForEmoji[reaction.name]" + class="emoji-reaction-popover" + :trigger-attrs="counterTriggerAttrs(reaction)" + @show="fetchEmojiReactionsByIfMissing()" + > + <span class="emoji-reaction-counts">{{ reaction.count }}</span> + </UserListPopover> + </span> <a v-if="tooManyReactions" class="emoji-reaction-expand faint" @@ -28,44 +72,114 @@ <script src="./emoji_reactions.js"></script> <style lang="scss"> -@import "../../variables"; +@import "../../mixins"; .EmojiReactions { display: flex; margin-top: 0.25em; flex-wrap: wrap; - .emoji-reaction { - padding: 0 0.5em; - margin-right: 0.5em; + --emoji-size: calc(var(--emojiSize, 1.25em) * var(--emojiReactionsScale, 1)); + + .emoji-reaction-container { + display: flex; + align-items: stretch; margin-top: 0.5em; + margin-right: 0.5em; + + .emoji-reaction-popover { + padding: 0; + + .emoji-reaction-count-button { + margin: 0; + height: 100%; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + box-sizing: border-box; + min-width: 2em; + display: inline-flex; + justify-content: center; + align-items: center; + + &.-picked-reaction { + border: 1px solid var(--accent); + margin-right: -1px; + } + } + } + } + + .emoji-reaction { + padding-left: 0.5em; display: flex; align-items: center; justify-content: center; box-sizing: border-box; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + margin: 0; .reaction-emoji { - width: 1.25em; + width: var(--emoji-size); + height: var(--emoji-size); margin-right: 0.25em; + line-height: var(--emoji-size); + display: flex; + justify-content: center; + align-items: center; + } + + .reaction-emoji-content { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + line-height: inherit; + overflow: hidden; + font-size: calc(var(--emoji-size) * 0.8); + margin: 0; } &:focus { outline: none; } - &.not-clickable { - cursor: default; - - &:hover { - box-shadow: $fallback--buttonShadow; - box-shadow: var(--buttonShadow); - } + .svg-inline--fa { + color: var(--text); } &.-picked-reaction { - border: 1px solid var(--accent, $fallback--link); + border: 1px solid var(--accent); margin-left: -1px; // offset the border, can't use inset shadows either - margin-right: calc(0.5em - 1px); + margin-right: -1px; + + .svg-inline--fa { + color: var(--accent); + } + } + + @include unfocused-style { + .focus-marker { + visibility: hidden; + } + + .active-marker { + visibility: visible; + } + } + + @include focused-style { + .svg-inline--fa { + color: var(--accent); + } + + .focus-marker { + visibility: visible; + } + + .active-marker { + visibility: hidden; + } } } diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index 48b960b2..e2c88ceb 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -1,4 +1,5 @@ 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 { library } from '@fortawesome/fontawesome-svg-core' import { @@ -40,7 +41,8 @@ const ExtraButtons = { data () { return { expanded: false, - showingDeleteDialog: false + showingDeleteDialog: false, + randomSeed: genRandomSeed() } }, methods: { @@ -152,6 +154,15 @@ const ExtraButtons = { 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' + } } } } diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index c1c15c0f..7b38d974 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -2,6 +2,7 @@ <Popover class="ExtraButtons" trigger="click" + :trigger-attrs="triggerAttrs" placement="top" :offset="{ y: 5 }" :bound-to="{ x: 'container' }" @@ -10,10 +11,15 @@ @close="onClose" > <template #content="{close}"> - <div class="dropdown-menu"> + <div + :id="`popup-menu-${randomSeed}`" + class="dropdown-menu" + role="menu" + > <button v-if="canMute && !status.thread_muted" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" @click.prevent="muteConversation" > <FAIcon @@ -23,7 +29,8 @@ </button> <button v-if="canMute && status.thread_muted" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" @click.prevent="unmuteConversation" > <FAIcon @@ -33,7 +40,8 @@ </button> <button v-if="!status.pinned && canPin" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" @click.prevent="pinStatus" @click="close" > @@ -44,7 +52,8 @@ </button> <button v-if="status.pinned && canPin" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" @click.prevent="unpinStatus" @click="close" > @@ -56,7 +65,8 @@ <template v-if="canBookmark"> <button v-if="!status.bookmarked" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" @click.prevent="bookmarkStatus" @click="close" > @@ -67,7 +77,8 @@ </button> <button v-if="status.bookmarked" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" @click.prevent="unbookmarkStatus" @click="close" > @@ -79,7 +90,8 @@ </template> <button v-if="ownStatus && editingAvailable" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" @click.prevent="editStatus" @click="close" > @@ -90,7 +102,8 @@ </button> <button v-if="isEdited && editingAvailable" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" @click.prevent="showStatusHistory" @click="close" > @@ -101,7 +114,8 @@ </button> <button v-if="canDelete" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" @click.prevent="deleteStatus" @click="close" > @@ -111,7 +125,8 @@ /><span>{{ $t("status.delete") }}</span> </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" @click.prevent="copyLink" @click="close" > @@ -122,7 +137,8 @@ </button> <a v-if="!status.is_local" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" title="Source" :href="status.external_url" target="_blank" @@ -133,7 +149,8 @@ /><span>{{ $t("status.external_source") }}</span> </a> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" + role="menuitem" @click.prevent="reportStatus" @click="close" > @@ -184,7 +201,6 @@ <script src="./extra_buttons.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .ExtraButtons { @@ -194,8 +210,7 @@ margin: -10px; &:hover .svg-inline--fa { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } diff --git a/src/components/extra_notifications/extra_notifications.js b/src/components/extra_notifications/extra_notifications.js new file mode 100644 index 00000000..1bb0f837 --- /dev/null +++ b/src/components/extra_notifications/extra_notifications.js @@ -0,0 +1,48 @@ +import { mapGetters } from 'vuex' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faUserPlus, + faComments, + faBullhorn +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faUserPlus, + faComments, + faBullhorn +) + +const ExtraNotifications = { + computed: { + shouldShowChats () { + return this.mergedConfig.showExtraNotifications && this.mergedConfig.showChatsInExtraNotifications && this.unreadChatCount + }, + shouldShowAnnouncements () { + return this.mergedConfig.showExtraNotifications && this.mergedConfig.showAnnouncementsInExtraNotifications && this.unreadAnnouncementCount + }, + shouldShowFollowRequests () { + return this.mergedConfig.showExtraNotifications && this.mergedConfig.showFollowRequestsInExtraNotifications && this.followRequestCount + }, + hasAnythingToShow () { + return this.shouldShowChats || this.shouldShowAnnouncements || this.shouldShowFollowRequests + }, + shouldShowCustomizationTip () { + return this.mergedConfig.showExtraNotificationsTip && this.hasAnythingToShow + }, + currentUser () { + return this.$store.state.users.currentUser + }, + ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount', 'followRequestCount', 'mergedConfig']) + }, + methods: { + openNotificationSettings () { + return this.$store.dispatch('openSettingsModalTab', 'notifications') + }, + dismissConfigurationTip () { + return this.$store.dispatch('setOption', { name: 'showExtraNotificationsTip', value: false }) + } + } +} + +export default ExtraNotifications diff --git a/src/components/extra_notifications/extra_notifications.vue b/src/components/extra_notifications/extra_notifications.vue new file mode 100644 index 00000000..600c99bb --- /dev/null +++ b/src/components/extra_notifications/extra_notifications.vue @@ -0,0 +1,110 @@ +<template> + <div class="ExtraNotifications"> + <div + v-if="shouldShowChats" + class="notification unseen" + > + <div class="notification-overlay" /> + <router-link + class="button-unstyled -link extra-notification" + :to="{ name: 'chats', params: { username: currentUser.screen_name } }" + > + <FAIcon + fixed-width + class="fa-scale-110 icon" + icon="comments" + /> + {{ $tc('notifications.unread_chats', unreadChatCount, { num: unreadChatCount }) }} + </router-link> + </div> + <div + v-if="shouldShowAnnouncements" + class="notification unseen" + > + <div class="notification-overlay" /> + <router-link + class="button-unstyled -link extra-notification" + :to="{ name: 'announcements' }" + > + <FAIcon + fixed-width + class="fa-scale-110 icon" + icon="bullhorn" + /> + {{ $tc('notifications.unread_announcements', unreadAnnouncementCount, { num: unreadAnnouncementCount }) }} + </router-link> + </div> + <div + v-if="shouldShowFollowRequests" + class="notification unseen" + > + <div class="notification-overlay" /> + <router-link + class="button-unstyled -link extra-notification" + :to="{ name: 'friend-requests' }" + > + <FAIcon + fixed-width + class="fa-scale-110 icon" + icon="user-plus" + /> + {{ $tc('notifications.unread_follow_requests', followRequestCount, { num: followRequestCount }) }} + </router-link> + </div> + <i18n-t + v-if="shouldShowCustomizationTip" + tag="span" + class="notification tip extra-notification" + keypath="notifications.configuration_tip" + > + <template #theSettings> + <button + class="button-unstyled -link" + @click="openNotificationSettings" + > + {{ $t('notifications.configuration_tip_settings') }} + </button> + </template> + <template #dismiss> + <button + class="button-unstyled -link" + @click="dismissConfigurationTip" + > + {{ $t('notifications.configuration_tip_dismiss') }} + </button> + </template> + </i18n-t> + </div> +</template> + +<script src="./extra_notifications.js" /> + +<style lang="scss"> +.ExtraNotifications { + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + + .notification { + width: 100%; + border-bottom: 1px solid; + border-color: var(--border); + display: flex; + flex-direction: column; + align-items: stretch; + } + + .extra-notification { + padding: 1em; + } + + .icon { + margin-right: 0.5em; + } + + .tip { + display: inline; + } +} +</style> diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue index 8c883c13..2e0dd047 100644 --- a/src/components/favorite_button/favorite_button.vue +++ b/src/components/favorite_button/favorite_button.vue @@ -65,7 +65,6 @@ <script src="./favorite_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .FavoriteButton { @@ -88,8 +87,7 @@ &:hover .svg-inline--fa, &.-favorited .svg-inline--fa { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } @include unfocused-style { diff --git a/src/components/flash/flash.vue b/src/components/flash/flash.vue index 9f58d314..c9fc730b 100644 --- a/src/components/flash/flash.vue +++ b/src/components/flash/flash.vue @@ -42,8 +42,6 @@ <script src="./flash.js"></script> <style lang="scss"> -@import "../../variables"; - .Flash { display: inline-block; width: 100%; diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js index 92ee3f30..d9394945 100644 --- a/src/components/font_control/font_control.js +++ b/src/components/font_control/font_control.js @@ -1,63 +1,59 @@ -import { set } from 'lodash' import Select from '../select/select.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' +import Popover from 'src/components/popover/popover.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faExclamationTriangle, + faKeyboard, + faFont +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faExclamationTriangle, + faKeyboard, + faFont +) export default { components: { - Select + Select, + Checkbox, + Popover }, props: [ 'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit' ], + mounted () { + this.$store.dispatch('queryLocalFonts') + }, emits: ['update:modelValue'], data () { return { - lValue: this.modelValue, + manualEntry: false, availableOptions: [ this.noInherit ? '' : 'inherit', - 'custom', - ...(this.options || []), 'serif', + 'sans-serif', 'monospace', - 'sans-serif' + ...(this.options || []) ].filter(_ => _) } }, - beforeUpdate () { - this.lValue = this.modelValue + methods: { + toggleManualEntry () { + this.manualEntry = !this.manualEntry + } }, computed: { present () { - return typeof this.lValue !== 'undefined' - }, - dValue () { - return this.lValue || this.fallback || {} - }, - family: { - get () { - return this.dValue.family - }, - set (v) { - set(this.lValue, 'family', v) - this.$emit('update:modelValue', this.lValue) - } + return typeof this.modelValue !== 'undefined' }, - isCustom () { - return this.preset === 'custom' + localFontsList () { + return this.$store.state.interface.localFonts }, - preset: { - get () { - if (this.family === 'serif' || - this.family === 'sans-serif' || - this.family === 'monospace' || - this.family === 'inherit') { - return this.family - } else { - return 'custom' - } - }, - set (v) { - this.family = v === 'custom' ? '' : v - } + localFontsSize () { + return this.$store.state.interface.localFonts?.length } } } diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue index bb7e64bc..fca3b360 100644 --- a/src/components/font_control/font_control.vue +++ b/src/components/font_control/font_control.vue @@ -1,73 +1,146 @@ <template> <div - class="font-control style-control" + class="font-control" :class="{ custom: isCustom }" > <label + :id="name + '-label'" :for="preset === 'custom' ? name : name + '-font-switcher'" class="label" > {{ label }} </label> - <input + {{ ' ' }} + <Checkbox v-if="typeof fallback !== 'undefined'" :id="name + '-o'" - class="opt exlcude-disabled" - type="checkbox" - :checked="present" + :modelValue="present" @change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" > - <label - v-if="typeof fallback !== 'undefined'" - class="opt-l" - :for="name + '-o'" - /> - {{ ' ' }} - <Select - :id="name + '-font-switcher'" - v-model="preset" - :disabled="!present" - class="font-switcher" - > - <option - v-for="option in availableOptions" - :key="option" - :value="option" + {{ $t('settings.style.themes3.define') }} + </Checkbox> + <p v-if="modelValue?.family"> + <label + v-if="manualEntry" + :id="name + '-label'" + :for="preset === 'custom' ? name : name + '-font-switcher'" + class="label" > - {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} - </option> - </Select> - <input - v-if="isCustom" - :id="name" - v-model="family" - class="custom-font" - type="text" - > + <i18n-t + keypath="settings.style.themes3.font.entry" + tag="span" + > + <template #fontFamily> + <code>font-family</code> + </template> + </i18n-t> + </label> + <label + v-else + :id="name + '-label'" + :for="preset === 'custom' ? name : name + '-font-switcher'" + class="label" + > + {{ $t('settings.style.themes3.font.select') }} + </label> + {{ ' ' }} + <span + v-if="manualEntry" + class="btn-group" + > + <button + class="btn button-default" + @click="toggleManualEntry" + :title="$t('settings.style.themes3.font.lookup_local_fonts')" + > + <FAIcon + fixed-width + icon="font" + /> + </button> + <input + :id="name" + :model-value="modelValue.family" + class="input custom-font" + type="text" + @update:modelValue="$emit('update:modelValue', { ...(modelValue || {}), family: $event.target.value })" + > + </span> + <span + v-else + class="btn-group" + > + <button + class="btn button-default" + @click="toggleManualEntry" + :title="$t('settings.style.themes3.font.enter_manually')" + > + <FAIcon + fixed-width + icon="keyboard" + /> + </button> + <Select + :id="name + '-local-font-switcher'" + :model-value="modelValue?.family" + class="custom-font" + @update:modelValue="v => $emit('update:modelValue', { ...(modelValue || {}), family: v })" + > + <optgroup + :label="$t('settings.style.themes3.font.group-builtin')" + > + <option + v-for="option in availableOptions" + :key="option" + :value="option" + :style="{ fontFamily: option === 'inherit' ? null : option }" + > + {{ $t('settings.style.themes3.font.builtin.' + option) }} + </option> + </optgroup> + <optgroup + v-if="localFontsSize > 0" + :label="$t('settings.style.themes3.font.group-local')" + > + <option + v-for="option in localFontsList" + :key="option" + :value="option" + :style="{ fontFamily: option }" + > + {{ option }} + </option> + </optgroup> + <optgroup + v-else + :label="$t('settings.style.themes3.font.group-local')" + > + <option disabled> + {{ $t('settings.style.themes3.font.local-unavailable1') }} + </option> + <option disabled> + {{ $t('settings.style.themes3.font.local-unavailable2') }} + </option> + </optgroup> + </Select> + </span> + </p> </div> </template> <script src="./font_control.js"></script> <style lang="scss"> -@import "../../variables"; - .font-control { - input.custom-font { - min-width: 10em; + .custom-font { + min-width: 20em; + max-width: 20em; } +} - &.custom { - /* TODO Should make proper joiners... */ - .font-switcher { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - .custom-font { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - } +.invalid-tooltip { + margin: 0.5em 1em; + min-width: 10em; + text-align: center; } </style> diff --git a/src/components/fun_text.style.js b/src/components/fun_text.style.js new file mode 100644 index 00000000..2d3ac154 --- /dev/null +++ b/src/components/fun_text.style.js @@ -0,0 +1,40 @@ +export default { + name: 'FunText', + selector: '/*fun-text*/', + virtual: true, + variants: { + greentext: '.greentext', + cyantext: '.cyantext' + }, + states: { + faint: '.faint' + }, + defaultRules: [ + { + directives: { + textColor: '--text', + textAuto: 'preserve' + } + }, + { + state: ['faint'], + directives: { + textOpacity: 0.5 + } + }, + { + variant: 'greentext', + directives: { + textColor: '--cGreen', + textAuto: 'preserve' + } + }, + { + variant: 'cyantext', + directives: { + textColor: '--cBlue', + textAuto: 'preserve' + } + } + ] +} diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue index 96b554e3..40048285 100644 --- a/src/components/gallery/gallery.vue +++ b/src/components/gallery/gallery.vue @@ -87,8 +87,6 @@ <script src='./gallery.js'></script> <style lang="scss"> -@import "../../variables"; - .Gallery { .gallery-rows { display: flex; diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue index 0e58476f..140d0d54 100644 --- a/src/components/global_notice_list/global_notice_list.vue +++ b/src/components/global_notice_list/global_notice_list.vue @@ -4,7 +4,7 @@ v-for="(notice, index) in notices" :key="index" class="alert global-notice" - :class="{ ['global-' + notice.level]: true }" + :class="{ [notice.level]: true }" > <div class="notice-message"> {{ $t(notice.messageKey, notice.messageArgs) }} @@ -25,14 +25,12 @@ <script src="./global_notice_list.js"></script> <style lang="scss"> -@import "../../variables"; - .global-notice-list { position: fixed; top: calc(var(--navbar-height) + 0.5em); width: 100%; pointer-events: none; - z-index: var(--ZI_navbar_popovers); + z-index: var(--ZI_modals_popovers); display: flex; flex-direction: column; align-items: center; @@ -52,48 +50,8 @@ } } - .global-error { - background-color: var(--alertPopupError, $fallback--cRed); - color: var(--alertPopupErrorText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupErrorText, $fallback--text); - } - } - - .global-warning { - background-color: var(--alertPopupWarning, $fallback--cOrange); - color: var(--alertPopupWarningText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupWarningText, $fallback--text); - } - } - - .global-success { - background-color: var(--alertPopupSuccess, $fallback--cGreen); - color: var(--alertPopupSuccessText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupSuccessText, $fallback--text); - } - } - - .global-info { - background-color: var(--alertPopupNeutral, $fallback--fg); - color: var(--alertPopupNeutralText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupNeutralText, $fallback--text); - } - } - .close-notice { padding-right: 0.2em; - - .svg-inline--fa:hover { - opacity: 0.6; - } } } </style> diff --git a/src/components/icon.style.js b/src/components/icon.style.js new file mode 100644 index 00000000..6cb9e4e3 --- /dev/null +++ b/src/components/icon.style.js @@ -0,0 +1,14 @@ +export default { + name: 'Icon', + virtual: true, + selector: '.svg-inline--fa', + defaultRules: [ + { + component: 'Icon', + directives: { + textColor: '$blend(--stack, 0.5, --parent--text)', + textAuto: 'no-auto' + } + } + ] +} diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue index 8c48a387..8647ed4d 100644 --- a/src/components/image_cropper/image_cropper.vue +++ b/src/components/image_cropper/image_cropper.vue @@ -41,7 +41,7 @@ <input ref="input" type="file" - class="image-cropper-img-input" + class="input image-cropper-img-input" :accept="mimes" > </div> diff --git a/src/components/importer/importer.vue b/src/components/importer/importer.vue index 2a63b31a..12779b45 100644 --- a/src/components/importer/importer.vue +++ b/src/components/importer/importer.vue @@ -3,6 +3,7 @@ <form> <input ref="input" + class="input" type="file" @change="change" > diff --git a/src/components/input.style.js b/src/components/input.style.js new file mode 100644 index 00000000..7302cd6d --- /dev/null +++ b/src/components/input.style.js @@ -0,0 +1,60 @@ +const hoverGlow = { + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '--text', + alpha: 1 +} + +export default { + name: 'Input', + selector: '.input', + variant: { + checkbox: '.-checkbox', + radio: '.-radio' + }, + states: { + disabled: ':disabled', + hover: ':hover:not(:disabled)', + focused: ':focus-within' + }, + validInnerComponents: [ + 'Text' + ], + defaultRules: [ + { + component: 'Root', + directives: { + '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)' + } + }, + { + variant: 'checkbox', + directives: { + roundness: 1 + } + }, + { + directives: { + '--font': 'generic | inherit', + background: '--fg, -5', + roundness: 3, + shadow: [{ + x: 0, + y: 0, + blur: 2, + spread: 0, + color: '#000000', + alpha: 1 + }, '--defaultInputBevel'] + } + }, + { + state: ['hover'], + directives: { + shadow: [hoverGlow, '--defaultInputBevel'] + } + } + ] +} diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js index 1ae1d01c..fc441b90 100644 --- a/src/components/interactions/interactions.js +++ b/src/components/interactions/interactions.js @@ -3,6 +3,7 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' const tabModeDict = { mentions: ['mention'], + statuses: ['status'], 'likes+repeats': ['repeat', 'like'], follows: ['follow'], reactions: ['pleroma:emoji_reaction'], diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue index b7291c02..a2c56af4 100644 --- a/src/components/interactions/interactions.vue +++ b/src/components/interactions/interactions.vue @@ -10,10 +10,14 @@ :on-switch="onModeSwitch" > <span - key="mentions" + key="statuses" :label="$t('nav.mentions')" /> <span + key="statuses" + :label="$t('interactions.statuses')" + /> + <span key="likes+repeats" :label="$t('interactions.favs_repeats')" /> @@ -39,6 +43,7 @@ <Notifications ref="notifications" :no-heading="true" + :no-extra="true" :minimal-mode="true" :filter-mode="filterMode" /> diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue index a57e8761..f7d5ef7e 100644 --- a/src/components/interface_language_switcher/interface_language_switcher.vue +++ b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -36,7 +36,9 @@ <button class="button-default btn" @click="addLanguage" - >{{ $t('settings.add_language') }}</button> + > + {{ $t('settings.add_language') }} + </button> </li> </ul> </div> @@ -102,8 +104,6 @@ export default { </script> <style lang="scss"> -@import "../../variables"; - .interface-language-switcher { .language-select { margin-right: 1em; diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue index 09f341ac..8eb5a501 100644 --- a/src/components/link-preview/link-preview.vue +++ b/src/components/link-preview/link-preview.vue @@ -33,8 +33,6 @@ <script src="./link-preview.js"></script> <style lang="scss"> -@import "../../variables"; - .link-preview-card { display: flex; flex-direction: row; @@ -51,8 +49,7 @@ width: 100%; height: 100%; object-fit: cover; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); + border-radius: var(--roundness); } } @@ -82,13 +79,10 @@ margin: 2em 0; } - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); border-style: solid; border-width: 1px; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-radius: var(--roundness); + border-color: var(--border); } </style> diff --git a/src/components/link.style.js b/src/components/link.style.js new file mode 100644 index 00000000..d13cef33 --- /dev/null +++ b/src/components/link.style.js @@ -0,0 +1,24 @@ +export default { + name: 'Link', + selector: 'a', + virtual: true, + states: { + faint: '.faint' + }, + defaultRules: [ + { + component: 'Link', + directives: { + textColor: '--link' + } + }, + { + component: 'Link', + state: ['faint'], + directives: { + textOpacity: 0.5, + textOpacityMode: 'fake' + } + } + ] +} diff --git a/src/components/list/list.vue b/src/components/list/list.vue index f17766b4..c885bacd 100644 --- a/src/components/list/list.vue +++ b/src/components/list/list.vue @@ -1,9 +1,14 @@ <template> - <div class="list"> + <div + class="list" + role="list" + > <div v-for="item in items" :key="getKey(item)" class="list-item" + :class="[getClass(item), nonInteractive ? '-non-interactive' : '']" + role="listitem" > <slot name="item" @@ -29,24 +34,15 @@ export default { getKey: { type: Function, default: item => item.id + }, + getClass: { + type: Function, + default: item => '' + }, + nonInteractive: { + type: Boolean, + default: false } } } </script> - -<style lang="scss"> -@import "../../variables"; - -.list { - &-item:not(:last-child) { - border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); - } - - &-empty-content { - text-align: center; - padding: 10px; - } -} -</style> diff --git a/src/components/list/list_item.style.js b/src/components/list/list_item.style.js new file mode 100644 index 00000000..e82a0a83 --- /dev/null +++ b/src/components/list/list_item.style.js @@ -0,0 +1,48 @@ +export default { + name: 'ListItem', + selector: '.list-item', + states: { + active: '.-active', + hover: ':hover:not(.-non-interactive)' + }, + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'RichContent', + 'Input', + 'Avatar' + ], + defaultRules: [ + { + directives: { + background: '--bg', + opacity: 0 + } + }, + { + state: ['active'], + directives: { + background: '--inheritedBackground, 10', + opacity: 1 + } + }, + { + state: ['hover'], + directives: { + background: '--inheritedBackground, 10', + opacity: 1 + } + }, + { + state: ['hover', 'active'], + directives: { + background: '--inheritedBackground, 20', + opacity: 1 + } + } + ] +} diff --git a/src/components/lists_card/lists_card.vue b/src/components/lists_card/lists_card.vue index 925da3a5..a5dc6371 100644 --- a/src/components/lists_card/lists_card.vue +++ b/src/components/lists_card/lists_card.vue @@ -21,8 +21,6 @@ <script src="./lists_card.js"></script> <style lang="scss"> -@import "../../variables"; - .list-card { display: flex; } @@ -35,18 +33,6 @@ .button-list-edit { margin: 0; padding: 1em; - color: $fallback--link; - color: var(--link, $fallback--link); - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - } + color: var(--link); } </style> diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue index eec0f978..3c2066f6 100644 --- a/src/components/lists_edit/lists_edit.vue +++ b/src/components/lists_edit/lists_edit.vue @@ -36,6 +36,7 @@ id="list-edit-title" ref="title" v-model="titleDraft" + class="input" > <button v-if="id" @@ -164,8 +165,6 @@ <script src="./lists_edit.js"></script> <style lang="scss"> -@import "../../variables"; - .ListEdit { --panel-body-padding: 0.5em; diff --git a/src/components/lists_user_search/lists_user_search.vue b/src/components/lists_user_search/lists_user_search.vue index 6ca107e6..0f6ec125 100644 --- a/src/components/lists_user_search/lists_user_search.vue +++ b/src/components/lists_user_search/lists_user_search.vue @@ -10,6 +10,7 @@ <input ref="search" v-model="query" + class="input" :placeholder="$t('lists.search')" @input="onInput" > @@ -27,8 +28,6 @@ <script src="./lists_user_search.js"></script> <style lang="scss"> -@import "../../variables"; - .ListsUserSearch { .input-wrap { display: flex; diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue index 829e88ad..adc2dd5b 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -18,7 +18,7 @@ id="username" v-model="user.username" :disabled="loggingIn" - class="form-control" + class="input form-control" :placeholder="$t('login.placeholder')" > </div> @@ -29,7 +29,7 @@ ref="passwordInput" v-model="user.password" :disabled="loggingIn" - class="form-control" + class="input form-control" type="password" > </div> @@ -93,8 +93,6 @@ <script src="./login_form.js"></script> <style lang="scss"> -@import "../../variables"; - .login-form { display: flex; flex-direction: column; diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js index cfd42d4c..8c9e5f71 100644 --- a/src/components/media_upload/media_upload.js +++ b/src/components/media_upload/media_upload.js @@ -23,6 +23,11 @@ const mediaUpload = { } }, methods: { + onClick () { + if (this.uploadReady) { + this.$refs.input.click() + } + }, uploadFile (file) { const self = this const store = this.$store @@ -69,10 +74,15 @@ const mediaUpload = { this.multiUpload(target.files) } }, - props: [ - 'dropFiles', - 'disabled' - ], + props: { + dropFiles: Object, + disabled: Boolean, + normalButton: Boolean, + acceptTypes: { + type: String, + default: '*/*' + } + }, watch: { dropFiles: function (fileInfos) { if (!this.uploading) { diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue index 2799495b..047e3483 100644 --- a/src/components/media_upload/media_upload.vue +++ b/src/components/media_upload/media_upload.vue @@ -1,8 +1,9 @@ <template> - <label + <button class="media-upload" - :class="{ disabled: disabled }" + :class="[normalButton ? 'button-default btn' : 'button-unstyled', { disabled }]" :title="$t('tool_tip.media_upload')" + @click="onClick" > <FAIcon v-if="uploading" @@ -15,27 +16,33 @@ class="new-icon" icon="upload" /> + <template v-if="normalButton"> + {{ ' ' }} + {{ uploading ? $t('general.loading') : $t('tool_tip.media_upload') }} + </template> <input v-if="uploadReady" + ref="input" class="hidden-input-file" :disabled="disabled" type="file" multiple="true" + :accept="acceptTypes" @change="change" > - </label> + </button> </template> <script src="./media_upload.js"></script> <style lang="scss"> -@import "../../variables"; - .media-upload { - cursor: pointer; // We use <label> for interactivity... i wonder if it's fine - .hidden-input-file { display: none; } } - </style> + +label.media-upload { + cursor: pointer; // We use <label> for interactivity... i wonder if it's fine +} +</style> diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss index 69e9fed1..42f57294 100644 --- a/src/components/mention_link/mention_link.scss +++ b/src/components/mention_link/mention_link.scss @@ -1,10 +1,7 @@ -@import "../../variables"; - .MentionLink { position: relative; white-space: normal; display: inline; - color: var(--link); word-break: normal; & .new, @@ -14,7 +11,7 @@ } .mention-avatar { - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); width: 1.5em; height: 1.5em; vertical-align: middle; @@ -61,8 +58,10 @@ } &.-has-selection { - color: var(--alertNeutralText, $fallback--text); - background-color: var(--alertNeutral, $fallback--fg); + --color: var(--selectionText); + --link: var(--selectionText); + + background-color: var(--selectionBackground); } .at { @@ -102,7 +101,7 @@ } .serverName.-faded { - color: var(--faintLink, $fallback--link); + color: var(--linkFaint); } } diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue index 869a3257..5db837a2 100644 --- a/src/components/mention_link/mention_link.vue +++ b/src/components/mention_link/mention_link.vue @@ -22,7 +22,7 @@ :class="classnames" > <a - class="short button-unstyled" + class="short" :class="{ '-with-tooltip': shouldShowTooltip }" :href="url" @click.prevent="onClick" diff --git a/src/components/mentions_line/mentions_line.vue b/src/components/mentions_line/mentions_line.vue index 64c19bf1..daab64a3 100644 --- a/src/components/mentions_line/mentions_line.vue +++ b/src/components/mentions_line/mentions_line.vue @@ -22,13 +22,13 @@ /> </span><button v-if="!expanded" - class="button-unstyled showMoreLess" + class="button-unstyled -link showMoreLess" @click="toggleShowMore" > {{ $t('status.plus_more', { number: extraMentions.length }) }} </button><button v-if="expanded" - class="button-unstyled showMoreLess" + class="button-unstyled -link showMoreLess" @click="toggleShowMore" > {{ $t('general.show_less') }} diff --git a/src/components/menu_item.style.js b/src/components/menu_item.style.js new file mode 100644 index 00000000..51388155 --- /dev/null +++ b/src/components/menu_item.style.js @@ -0,0 +1,90 @@ +export default { + name: 'MenuItem', + selector: '.menu-item', + validInnerComponents: [ + 'Text', + 'Icon', + 'Input', + 'Border', + 'ButtonUnstyled', + 'Badge', + 'Avatar' + ], + states: { + hover: ':hover', + active: '.-active' + }, + defaultRules: [ + { + directives: { + background: '--bg', + opacity: 0 + } + }, + { + state: ['hover'], + directives: { + background: '$mod(--bg, 5)', + opacity: 1 + } + }, + { + state: ['active'], + directives: { + background: '$mod(--bg, 10)', + opacity: 1 + } + }, + { + state: ['active', 'hover'], + directives: { + background: '$mod(--bg, 15)', + opacity: 1 + } + }, + { + component: 'Text', + parent: { + component: 'MenuItem', + state: ['hover'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + }, + { + component: 'Text', + parent: { + component: 'MenuItem', + state: ['active'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + }, + { + component: 'Icon', + parent: { + component: 'MenuItem', + state: ['active'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + }, + { + component: 'Icon', + parent: { + component: 'MenuItem', + state: ['hover'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + } + ] +} diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue index 5988fa51..27184fb4 100644 --- a/src/components/mfa_form/recovery_form.vue +++ b/src/components/mfa_form/recovery_form.vue @@ -16,7 +16,7 @@ <input id="code" v-model="code" - class="form-control" + class="input form-control" > </div> diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue index 709eb9b8..3ec617e9 100644 --- a/src/components/mfa_form/totp_form.vue +++ b/src/components/mfa_form/totp_form.vue @@ -18,7 +18,7 @@ <input id="code" v-model="code" - class="form-control" + class="input form-control" > </div> diff --git a/src/components/mobile_drawer.style.js b/src/components/mobile_drawer.style.js new file mode 100644 index 00000000..398bc186 --- /dev/null +++ b/src/components/mobile_drawer.style.js @@ -0,0 +1,41 @@ +export default { + name: 'MobileDrawer', + selector: '.mobile-drawer', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'Input', + 'PanelHeader', + 'MenuItem', + 'Notification', + 'Alert', + 'UserCard' + ], + defaultRules: [ + { + directives: { + background: '--bg', + backgroundNoCssColor: 'yes' + } + }, + { + component: 'PanelHeader', + parent: { component: 'MobileDrawer' }, + directives: { + background: '--fg', + shadow: [{ + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }] + } + } + ] +} diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index dad1f6aa..8c9261b0 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -1,7 +1,10 @@ import SideDrawer from '../side_drawer/side_drawer.vue' import Notifications from '../notifications/notifications.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue' -import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' +import { + unseenNotificationsFromStore, + countExtraNotifications +} from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' import NavigationPins from 'src/components/navigation/navigation_pins.vue' import { mapGetters } from 'vuex' @@ -11,7 +14,8 @@ import { faBell, faBars, faArrowUp, - faMinus + faMinus, + faCheckDouble } from '@fortawesome/free-solid-svg-icons' library.add( @@ -19,7 +23,8 @@ library.add( faBell, faBars, faArrowUp, - faMinus + faMinus, + faCheckDouble ) const MobileNav = { @@ -50,8 +55,14 @@ const MobileNav = { return unseenNotificationsFromStore(this.$store) }, unseenNotificationsCount () { + return this.unseenNotifications.length + countExtraNotifications(this.$store) + }, + unseenCount () { return this.unseenNotifications.length }, + unseenCountBadgeText () { + return `${this.unseenCount ? this.unseenCount : ''}` + }, hideSitename () { return this.$store.state.instance.hideSitename }, sitename () { return this.$store.state.instance.name }, isChat () { @@ -64,6 +75,9 @@ const MobileNav = { shouldConfirmLogout () { return this.$store.getters.mergedConfig.modalOnLogout }, + closingDrawerMarksAsSeen () { + return this.$store.getters.mergedConfig.closingDrawerMarksAsSeen + }, ...mapGetters(['unreadChatCount']) }, methods: { @@ -78,7 +92,7 @@ const MobileNav = { // make sure to mark notifs seen only when the notifs were open and not // from close-calls. this.notificationsOpen = false - if (markRead) { + if (markRead && this.closingDrawerMarksAsSeen) { this.markNotificationsAsSeen() } } @@ -114,7 +128,6 @@ const MobileNav = { this.hideConfirmLogout() }, markNotificationsAsSeen () { - // this.$refs.notifications.markAsSeen() this.$store.dispatch('markNotificationsAsSeen') }, onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) { diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index c2746abe..76a90d3e 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -20,7 +20,7 @@ /> <div v-if="(unreadChatCount && !chatsPinned) || unreadAnnouncementCount" - class="alert-dot" + class="badge -dot -notification" /> </button> <NavigationPins class="pins" /> @@ -37,20 +37,26 @@ /> <div v-if="unseenNotificationsCount" - class="alert-dot" + class="badge -dot -notification" /> </button> </div> </nav> <aside v-if="currentUser" - class="mobile-notifications-drawer" + class="mobile-notifications-drawer mobile-drawer" :class="{ '-closed': !notificationsOpen }" @touchstart.stop="notificationsTouchStart" @touchmove.stop="notificationsTouchMove" > - <div class="mobile-notifications-header"> - <span class="title">{{ $t('notifications.notifications') }}</span> + <div class="panel-heading mobile-notifications-header"> + <span class="title"> + {{ $t('notifications.notifications') }} + <span + v-if="unseenCountBadgeText" + class="badge -notification unseen-count" + >{{ unseenCountBadgeText }}</span> + </span> <span class="spacer" /> <button v-if="notificationsAtTop" @@ -67,6 +73,17 @@ </FALayers> </button> <button + v-if="!closingDrawerMarksAsSeen" + class="button-unstyled mobile-nav-button" + :title="$t('nav.mobile_notifications_mark_as_seen')" + @click.stop.prevent="markNotificationsAsSeen()" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="check-double" + /> + </button> + <button class="button-unstyled mobile-nav-button" :title="$t('nav.mobile_notifications_close')" @click.stop.prevent="closeMobileNotifications(true)" @@ -106,21 +123,19 @@ <script src="./mobile_nav.js"></script> <style lang="scss"> -@import "../../variables"; - .MobileNav { z-index: var(--ZI_navbar); .mobile-nav { display: grid; line-height: var(--navbar-height); - grid-template-rows: 50px; + grid-template-rows: var(--navbar-height); grid-template-columns: 2fr auto; width: 100%; box-sizing: border-box; a { - color: var(--topBarLink, $fallback--link); + color: var(--link); } } @@ -148,19 +163,6 @@ display: flex; } - .alert-dot { - border-radius: 100%; - height: 8px; - width: 8px; - position: absolute; - left: calc(50% - 4px); - top: calc(50% - 4px); - margin-left: 6px; - margin-top: -6px; - background-color: $fallback--cRed; - background-color: var(--badgeNotification, $fallback--cRed); - } - .mobile-notifications-drawer { width: 100%; height: 100vh; @@ -168,13 +170,13 @@ position: fixed; top: 0; left: 0; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + box-shadow: var(--shadow); transition-property: transform; transition-duration: 0.25s; transform: translateX(0); z-index: var(--ZI_navbar); -webkit-overflow-scrolling: touch; + background: var(--background); &.-closed { transform: translateX(100%); @@ -188,14 +190,10 @@ justify-content: space-between; z-index: calc(var(--ZI_navbar) + 100); width: 100%; - height: 50px; - line-height: 50px; + height: 3.5em; + line-height: 3.5em; position: absolute; - color: var(--topBarText); - background-color: $fallback--fg; - background-color: var(--topBar, $fallback--fg); - box-shadow: 0 0 4px rgb(0 0 0 / 60%); - box-shadow: var(--topBarShadow); + box-shadow: var(--shadow); .spacer { flex: 1; @@ -216,15 +214,11 @@ } .mobile-notifications { - margin-top: 50px; + margin-top: 3.5em; width: 100vw; height: calc(100vh - var(--navbar-height)); overflow-x: hidden; overflow-y: scroll; - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); .notifications { padding: 0; diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.vue b/src/components/mobile_post_status_button/mobile_post_status_button.vue index ef0f51fe..ba77cb2d 100644 --- a/src/components/mobile_post_status_button/mobile_post_status_button.vue +++ b/src/components/mobile_post_status_button/mobile_post_status_button.vue @@ -13,8 +13,6 @@ <script src="./mobile_post_status_button.js"></script> <style lang="scss"> -@import "../../variables"; - .MobilePostButton { &.button-default { width: 5em; @@ -25,8 +23,6 @@ right: 1.5em; // TODO: this needs its own color, it has to stand out enough and link color // is not very optimal for this particular use. - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); display: flex; justify-content: center; align-items: center; @@ -42,8 +38,7 @@ svg { font-size: 1.5em; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } diff --git a/src/components/modal/modals.style.js b/src/components/modal/modals.style.js new file mode 100644 index 00000000..c401a0cd --- /dev/null +++ b/src/components/modal/modals.style.js @@ -0,0 +1,9 @@ +export default { + name: 'Modals', + selector: '.modal-view', + lazy: true, + validInnerComponents: [ + 'Panel' + ], + defaultRules: [] +} diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index b708cdc8..df3bb655 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -12,13 +12,13 @@ <div class="dropdown-menu"> <span v-if="canGrantRole"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleRight("admin")" > {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleRight("moderator")" > {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }} @@ -31,14 +31,14 @@ </span> <button v-if="canChangeActivationState" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleActivationStatus()" > {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }} </button> <button v-if="canDeleteAccount" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="deleteUserDialog(true)" > {{ $t('user_card.admin_menu.delete_account') }} @@ -50,74 +50,74 @@ /> <span v-if="canUseTagPolicy"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.FORCE_NSFW)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }" /> {{ $t('user_card.admin_menu.force_nsfw') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.STRIP_MEDIA)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }" /> {{ $t('user_card.admin_menu.strip_media') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.FORCE_UNLISTED)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }" /> {{ $t('user_card.admin_menu.force_unlisted') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.SANDBOX)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }" /> {{ $t('user_card.admin_menu.sandbox') }} </button> <button v-if="user.is_local" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }" /> {{ $t('user_card.admin_menu.disable_remote_subscription') }} </button> <button v-if="user.is_local" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }" /> {{ $t('user_card.admin_menu.disable_any_subscription') }} </button> <button v-if="user.is_local" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.QUARANTINE)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }" /> {{ $t('user_card.admin_menu.quarantine') }} @@ -166,8 +166,6 @@ <script src="./moderation_tools.js"></script> <style lang="scss"> -@import "../../variables"; - .moderation-tools-popover { height: 100%; diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue index 97af4787..6dc86738 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue @@ -227,6 +227,5 @@ <script src="./mrf_transparency_panel.js"></script> <style lang="scss"> -@import "../../variables"; @import "./mrf_transparency_panel"; </style> diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 1a826cc4..bf608936 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -37,7 +37,8 @@ </NavigationEntry> <div v-show="showTimelines" - class="timelines-background" + class="timelines-background menu-item-collapsible" + :class="{ '-expanded': showTimelines }" > <div class="timelines"> <NavigationEntry @@ -57,12 +58,11 @@ > <router-link :title="$t('lists.manage_lists')" - class="extra-button" + class="button-unstyled extra-button" :to="{ name: 'lists' }" @click.stop > <FAIcon - class="extra-button" fixed-width icon="wrench" /> @@ -75,7 +75,8 @@ </NavigationEntry> <div v-show="showLists" - class="timelines-background" + class="timelines-background menu-item-collapsible" + :class="{ '-expanded': showLists }" > <ListsMenuContent :show-pin="editMode || forceEditMode" @@ -102,12 +103,10 @@ <script src="./nav_panel.js"></script> <style lang="scss"> -@import "../../variables"; - .NavPanel { .panel { overflow: hidden; - box-shadow: var(--panelShadow); + box-shadow: var(--shadow); } ul { @@ -116,33 +115,6 @@ padding: 0; } - li { - position: relative; - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - } - - > li { - &:first-child .menu-item { - border-top-right-radius: $fallback--panelRadius; - border-top-right-radius: var(--panelRadius, $fallback--panelRadius); - border-top-left-radius: $fallback--panelRadius; - border-top-left-radius: var(--panelRadius, $fallback--panelRadius); - } - - &:last-child .menu-item { - border-bottom-right-radius: $fallback--panelRadius; - border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); - border-bottom-left-radius: $fallback--panelRadius; - border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius); - } - } - - li:last-child { - border: none; - } - .navigation-chevron { margin-left: 0.8em; margin-right: 0.8em; @@ -156,16 +128,6 @@ .timelines-background { padding: 0 0 0 0.6em; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - } - - .timelines { - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); } .nav-panel-heading { diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js index 7f096316..face430e 100644 --- a/src/components/navigation/navigation.js +++ b/src/components/navigation/navigation.js @@ -80,3 +80,21 @@ export const ROOT_ITEMS = { criteria: ['announcements'] } } + +export function routeTo (item, currentUser) { + if (!item.route && !item.routeObject) return null + + let route + + if (item.routeObject) { + route = item.routeObject + } else { + route = { name: (item.anon || currentUser) ? item.route : item.anonRoute } + } + + if (USERNAME_ROUTES.has(route.name)) { + route.params = { username: currentUser.screen_name, name: currentUser.screen_name } + } + + return route +} diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js index 81cc936a..22ed77d9 100644 --- a/src/components/navigation/navigation_entry.js +++ b/src/components/navigation/navigation_entry.js @@ -1,5 +1,5 @@ import { mapState } from 'vuex' -import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' +import { routeTo } from 'src/components/navigation/navigation.js' import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faThumbtack } from '@fortawesome/free-solid-svg-icons' @@ -26,17 +26,7 @@ const NavigationEntry = { }, computed: { routeTo () { - if (!this.item.route && !this.item.routeObject) return null - let route - if (this.item.routeObject) { - route = this.item.routeObject - } else { - route = { name: (this.item.anon || this.currentUser) ? this.item.route : this.item.anonRoute } - } - if (USERNAME_ROUTES.has(route.name)) { - route.params = { username: this.currentUser.screen_name, name: this.currentUser.screen_name } - } - return route + return routeTo(this.item, this.currentUser) }, getters () { return this.$store.getters diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue index 411ca536..4ea54ee3 100644 --- a/src/components/navigation/navigation_entry.vue +++ b/src/components/navigation/navigation_entry.vue @@ -1,7 +1,6 @@ <template> <OptionalRouterLink v-slot="{ isActive, href, navigate } = {}" - ass="ass" :to="routeTo" > <li @@ -11,7 +10,7 @@ > <component :is="routeTo ? 'a' : 'button'" - class="main-link button-unstyled" + class="main-link" :href="href" @click="navigate" > @@ -35,7 +34,7 @@ <slot /> <div v-if="item.badgeGetter && getters[item.badgeGetter]" - class="badge badge-notification" + class="badge -notification" > {{ getters[item.badgeGetter] }} </div> @@ -63,73 +62,53 @@ <script src="./navigation_entry.js"></script> <style lang="scss"> -@import "../../variables"; +.NavigationEntry.menu-item { + --__line-height: 2.5em; + --__horizontal-gap: 0.5em; + --__vertical-gap: 0.4em; -.NavigationEntry { + padding: 0; display: flex; - box-sizing: border-box; align-items: baseline; - height: 3.5em; - line-height: 3.5em; - padding: 0 1em; - width: 100%; - color: $fallback--link; - color: var(--link, $fallback--link); - .timelines-chevron { - margin-right: 0; + &[aria-expanded] { + padding-right: var(--__horizontal-gap); } .main-link { + line-height: var(--__line-height); + box-sizing: border-box; flex: 1; + padding: var(--__vertical-gap) var(--__horizontal-gap); } .menu-icon { - margin-right: 0.8em; + line-height: var(--__line-height); + padding: 0; + width: var(--__line-height); + margin-right: var(--__horizontal-gap); + } + + .timelines-chevron { + line-height: var(--__line-height); + padding: 0; + width: var(--__line-height); + margin-right: 0; } .extra-button { - width: 3em; + line-height: var(--__line-height); + padding: 0; + width: var(--__line-height); text-align: center; &:last-child { - margin-right: -0.8em; - } - } - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - - .menu-icon { - --icon: var(--text, $fallback--icon); + margin-right: calc(-1 * var(--__horizontal-gap)); } } - &.-active { - font-weight: bolder; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - - .menu-icon { - --icon: var(--text, $fallback--icon); - } - - &:hover { - text-decoration: underline; - } + .badge { + margin: 0 var(--__horizontal-gap); } } </style> diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js index 9dd795aa..86c33d1f 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -1,5 +1,5 @@ import { mapState } from 'vuex' -import { TIMELINES, ROOT_ITEMS, USERNAME_ROUTES } from 'src/components/navigation/navigation.js' +import { TIMELINES, ROOT_ITEMS, routeTo } from 'src/components/navigation/navigation.js' import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js' import { library } from '@fortawesome/fontawesome-svg-core' @@ -31,14 +31,7 @@ const NavPanel = { props: ['limit'], methods: { getRouteTo (item) { - if (item.routeObject) { - return item.routeObject - } - const route = { name: (item.anon || this.currentUser) ? item.route : item.anonRoute } - if (USERNAME_ROUTES.has(route.name)) { - route.params = { username: this.currentUser.screen_name } - } - return route + return routeTo(item, this.currentUser) } }, computed: { @@ -52,6 +45,7 @@ const NavPanel = { privateMode: state => state.instance.private, federating: state => state.instance.federating, pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, + supportsAnnouncements: state => state.announcements.supportsAnnouncements, pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) }), pinnedList () { @@ -63,6 +57,7 @@ const NavPanel = { ], { hasChats: this.pleromaChatMessagesAvailable, + hasAnnouncements: this.supportsAnnouncements, isFederating: this.federating, isPrivate: this.privateMode, currentUser: this.currentUser @@ -82,6 +77,7 @@ const NavPanel = { ], { hasChats: this.pleromaChatMessagesAvailable, + hasAnnouncements: this.supportsAnnouncements, isFederating: this.federating, isPrivate: this.privateMode, currentUser: this.currentUser diff --git a/src/components/navigation/navigation_pins.vue b/src/components/navigation/navigation_pins.vue index 4fbb4f95..decd1c04 100644 --- a/src/components/navigation/navigation_pins.vue +++ b/src/components/navigation/navigation_pins.vue @@ -3,7 +3,8 @@ <router-link v-for="item in pinnedList" :key="item.name" - class="pinned-item" + class="button-unstyled pinned-item" + active-class="toggled" :to="getRouteTo(item)" :title="item.labelRaw || $t(item.label)" > @@ -18,7 +19,7 @@ >{{ item.iconLetter }}</span> <div v-if="item.badgeGetter && getters[item.badgeGetter]" - class="alert-dot" + class="badge -dot -notification" /> </router-link> </span> @@ -27,25 +28,12 @@ <script src="./navigation_pins.js"></script> <style lang="scss"> -@import "../../variables"; - .NavigationPins { display: flex; flex-wrap: wrap; overflow: hidden; height: 100%; - .alert-dot { - border-radius: 100%; - height: 0.5em; - width: 0.5em; - position: absolute; - right: calc(50% - 0.75em); - top: calc(50% - 0.5em); - background-color: $fallback--cRed; - background-color: var(--badgeNotification, $fallback--cRed); - } - .pinned-item { position: relative; flex: 1 0 3em; @@ -60,15 +48,9 @@ margin: 0; } - &.router-link-active { - color: $fallback--text; - color: var(--panelText, $fallback--text); + &.toggled { + margin-bottom: -4px; border-bottom: 4px solid; - - & .svg-inline--fa, - & .iconLetter { - color: inherit; - } } } } diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 420db4f0..0e938c42 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -50,6 +50,7 @@ const Notification = { } }, props: ['notification'], + emits: ['interacted'], components: { StatusContent, UserAvatar, @@ -72,6 +73,9 @@ const Notification = { getUser (notification) { return this.$store.state.users.usersObject[notification.from_profile.id] }, + interacted () { + this.$emit('interacted') + }, toggleMute () { this.unmuted = !this.unmuted }, @@ -95,6 +99,7 @@ const Notification = { } }, doApprove () { + this.$emit('interacted') this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id }) @@ -114,6 +119,7 @@ const Notification = { } }, doDeny () { + this.$emit('interacted') this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) .then(() => { this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id }) diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss index 654aca3c..2dbced09 100644 --- a/src/components/notification/notification.scss +++ b/src/components/notification/notification.scss @@ -1,13 +1,15 @@ -@import "../../variables"; - // TODO Copypaste from Status, should unify it somehow .Notification { border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); word-wrap: break-word; word-break: break-word; + &.Status { + /* stylelint-disable-next-line declaration-no-important */ + background-color: transparent !important; + } + --emoji-size: 14px; &:hover { @@ -71,28 +73,22 @@ } &.-type--repeat .type-icon { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--cGreen); } &.-type--follow .type-icon { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } &.-type--follow-request .type-icon { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } &.-type--like .type-icon { - color: orange; - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } &.-type--move .type-icon { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } } diff --git a/src/components/notification/notification.style.js b/src/components/notification/notification.style.js new file mode 100644 index 00000000..c6d317d1 --- /dev/null +++ b/src/components/notification/notification.style.js @@ -0,0 +1,18 @@ +export default { + name: 'Notification', + selector: '.Notification', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'RichContent', + 'Input', + 'Avatar', + 'Attachment', + 'PollGraph' + ], + defaultRules: [] +} diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index e1ea42ad..f84b75de 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -1,11 +1,12 @@ <template> <article - v-if="notification.type === 'mention'" + v-if="notification.type === 'mention' || notification.type === 'status'" > <Status class="Notification" :compact="true" :statusoid="notification.status" + @interacted="interacted" /> </article> <article v-else> @@ -121,7 +122,17 @@ scope="global" keypath="notifications.reacted_with" > - <span class="emoji-reaction-emoji">{{ notification.emoji }}</span> + <img + v-if="notification.emoji_url" + class="emoji-reaction-emoji emoji-reaction-emoji-image" + :src="notification.emoji_url" + :alt="notification.emoji" + :title="notification.emoji" + > + <span + v-else + class="emoji-reaction-emoji" + >{{ notification.emoji }}</span> </i18n-t> </small> </span> @@ -144,7 +155,7 @@ <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" - class="timeago-link faint-link" + class="timeago-link faint" > <Timeago :time="notification.created_at" @@ -153,9 +164,9 @@ </router-link> <button class="button-unstyled expand-icon" - @click.prevent="toggleStatusExpanded" :title="$t('tool_tip.toggle_expand')" :aria-expanded="statusExpanded" + @click.prevent="toggleStatusExpanded" > <FAIcon class="fa-scale-110" @@ -236,9 +247,8 @@ /> <template v-else> <StatusContent - :class="{ faint: !statusExpanded }" :compact="!statusExpanded" - :status="notification.action" + :status="notification.status" /> </template> </div> diff --git a/src/components/notifications/notification_filters.vue b/src/components/notifications/notification_filters.vue index 1315b51a..497a5156 100644 --- a/src/components/notifications/notification_filters.vue +++ b/src/components/notifications/notification_filters.vue @@ -8,65 +8,74 @@ <template #content> <div class="dropdown-menu"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('likes')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.likes }" />{{ $t('settings.notification_visibility_likes') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('repeats')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.repeats }" />{{ $t('settings.notification_visibility_repeats') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('follows')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.follows }" />{{ $t('settings.notification_visibility_follows') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('mentions')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.mentions }" />{{ $t('settings.notification_visibility_mentions') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" + @click="toggleNotificationFilter('statuses')" + > + <span + class="input menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.statuses }" + />{{ $t('settings.notification_visibility_statuses') }} + </button> + <button + class="menu-item dropdown-item" @click="toggleNotificationFilter('emojiReactions')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.emojiReactions }" />{{ $t('settings.notification_visibility_emoji_reactions') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('moves')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.moves }" />{{ $t('settings.notification_visibility_moves') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('polls')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.polls }" />{{ $t('settings.notification_visibility_polls') }} </button> diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index d499d3d6..85d3662e 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -1,12 +1,15 @@ import { computed } from 'vue' import { mapGetters } from 'vuex' import Notification from '../notification/notification.vue' +import ExtraNotifications from '../extra_notifications/extra_notifications.vue' import NotificationFilters from './notification_filters.vue' import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js' import { notificationsFromStore, filteredNotificationsFromStore, - unseenNotificationsFromStore + unseenNotificationsFromStore, + countExtraNotifications, + ACTIONABLE_NOTIFICATION_TYPES } from '../../services/notification_utils/notification_utils.js' import FaviconService from '../../services/favicon_service/favicon_service.js' import { library } from '@fortawesome/fontawesome-svg-core' @@ -23,14 +26,20 @@ const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30 const Notifications = { components: { Notification, - NotificationFilters + NotificationFilters, + ExtraNotifications }, props: { // Disables panel styles, unread mark, potentially other notification-related actions // meant for "Interactions" timeline minimalMode: Boolean, - // Custom filter mode, an array of strings, possible values 'mention', 'repeat', 'like', 'follow', used to override global filter for use in "Interactions" timeline + // Custom filter mode, an array of strings, possible values 'mention', 'status', 'repeat', 'like', 'follow', used to override global filter for use in "Interactions" timeline filterMode: Array, + // Do not show extra notifications + noExtra: { + type: Boolean, + default: false + }, // Disable teleporting (i.e. for /users/user/notifications) disableTeleport: Boolean }, @@ -57,22 +66,36 @@ const Notifications = { return notificationsFromStore(this.$store) }, error () { - return this.$store.state.statuses.notifications.error + return this.$store.state.notifications.error }, unseenNotifications () { return unseenNotificationsFromStore(this.$store) }, filteredNotifications () { - return filteredNotificationsFromStore(this.$store, this.filterMode) + if (this.unseenAtTop) { + return [ + ...filteredNotificationsFromStore(this.$store).filter(n => this.shouldShowUnseen(n)), + ...filteredNotificationsFromStore(this.$store).filter(n => !this.shouldShowUnseen(n)) + ] + } else { + return filteredNotificationsFromStore(this.$store, this.filterMode) + } + }, + unseenCountBadgeText () { + return `${this.unseenCount ? this.unseenCount : ''}${this.extraNotificationsCount ? '*' : ''}` }, unseenCount () { return this.unseenNotifications.length }, + ignoreInactionableSeen () { return this.$store.getters.mergedConfig.ignoreInactionableSeen }, + extraNotificationsCount () { + return countExtraNotifications(this.$store) + }, unseenCountTitle () { - return this.unseenCount + (this.unreadChatCount) + this.unreadAnnouncementCount + return this.unseenNotifications.length + (this.unreadChatCount) + this.unreadAnnouncementCount }, loading () { - return this.$store.state.statuses.notifications.loading + return this.$store.state.notifications.loading }, noHeading () { const { layoutType } = this.$store.state.interface @@ -94,6 +117,10 @@ const Notifications = { return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount) }, noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders }, + unseenAtTop () { return this.$store.getters.mergedConfig.unseenAtTop }, + showExtraNotifications () { + return !this.noExtra + }, ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']) }, mounted () { @@ -137,11 +164,28 @@ const Notifications = { scrollToTop () { const scrollable = this.scrollerRef scrollable.scrollTo({ top: this.$refs.root.offsetTop }) - // this.$refs.root.scrollIntoView({ behavior: 'smooth', block: 'start' }) }, updateScrollPosition () { this.showScrollTop = this.$refs.root.offsetTop < this.scrollerRef.scrollTop }, + shouldShowUnseen (notification) { + if (notification.seen) return false + + const actionable = ACTIONABLE_NOTIFICATION_TYPES.has(notification.type) + return this.ignoreInactionableSeen ? actionable : true + }, + /* "Interacted" really refers to "actionable" notifications that require user input, + * everything else (likes/repeats/reacts) cannot be acted and therefore we just clear + * the "seen" status upon any clicks on them + */ + notificationClicked (notification) { + const { id } = notification + this.$store.dispatch('notificationClicked', { id }) + }, + notificationInteracted (notification) { + const { id } = notification + this.$store.dispatch('markSingleNotificationAsSeen', { id }) + }, markAsSeen () { this.$store.dispatch('markNotificationsAsSeen') this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index 41cfcef0..cfc1f3d6 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Notifications { &:not(.minimal) { // a bit of a hack to allow scrolling below notifications @@ -7,8 +5,7 @@ } .loadmore-error { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } .notification { @@ -25,7 +22,7 @@ &.unseen { .notification-overlay { - background-image: linear-gradient(135deg, var(--badgeNotification, $fallback--cRed) 4px, transparent 10px); + background-image: linear-gradient(135deg, var(--badgeNotification) 4px, transparent 10px); } } } @@ -35,6 +32,11 @@ .notification { box-sizing: border-box; + /* TODO cleanup this */ + .Status { + flex: 1; + } + &:hover .animated.Avatar { canvas { display: none; @@ -60,24 +62,17 @@ width: 32px; height: 32px; } - - .faint { - --link: var(--faintLink); - --text: var(--faint); - } } .follow-request-accept { &:hover { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } .follow-request-reject { &:hover { - color: $fallback--cRed; - color: var(--cRed, $fallback--cRed); + color: var(--cRed); } } @@ -97,11 +92,6 @@ } } - /* TODO cleanup this */ - .Status { - flex: 1; - } - time { white-space: nowrap; } @@ -129,6 +119,14 @@ .emoji-reaction-emoji { font-size: 1.3em; + max-width: 1.25em; + height: 1.25em; + width: auto; + } + + .emoji-reaction-emoji-image { + vertical-align: middle; + object-fit: contain; } .notification-details { diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index 633efca6..87e7b68d 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -17,9 +17,9 @@ <div class="title"> {{ $t('notifications.notifications') }} <span - v-if="unseenCount" - class="badge badge-notification unseen-count" - >{{ unseenCount }}</span> + v-if="unseenCountBadgeText" + class="badge -notification unseen-count" + >{{ unseenCountBadgeText }}</span> </div> <div v-if="showScrollTop" @@ -55,14 +55,25 @@ role="feed" > <div + v-if="showExtraNotifications" + role="listitem" + class="notification" + > + <extra-notifications /> + </div> + <div v-for="notification in notificationsToDisplay" :key="notification.id" role="listitem" class="notification" - :class="{unseen: !minimalMode && !notification.seen}" + :class="{unseen: !minimalMode && shouldShowUnseen(notification)}" + @click="e => notificationClicked(notification)" > <div class="notification-overlay" /> - <notification :notification="notification" /> + <notification + :notification="notification" + @interacted="e => notificationInteracted(notification)" + /> </div> </div> <div class="panel-footer"> @@ -74,7 +85,7 @@ </div> <button v-else-if="!loading" - class="button-unstyled -link -fullwidth" + class="button-unstyled -link text-center" @click.prevent="fetchOlderNotifications()" > <div class="new-status-notification text-center"> diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue index 15d08e04..a45bdd92 100644 --- a/src/components/opacity_input/opacity_input.vue +++ b/src/components/opacity_input/opacity_input.vue @@ -18,7 +18,7 @@ /> <input :id="name" - class="input-number" + class="input input-number" type="number" :value="modelValue || fallback" :disabled="!present || disabled" diff --git a/src/components/panel.style.js b/src/components/panel.style.js new file mode 100644 index 00000000..1bba4766 --- /dev/null +++ b/src/components/panel.style.js @@ -0,0 +1,51 @@ +export default { + name: 'Panel', + selector: '.panel', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'Input', + 'PanelHeader', + 'MenuItem', + 'Post', + 'Notification', + 'Alert', + 'UserCard', + 'Chat', + 'Attachment', + 'Tab', + 'ListItem' + ], + validInnerComponentsLite: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'Input', + 'PanelHeader', + 'Alert' + ], + defaultRules: [ + { + directives: { + backgroundNoCssColor: 'yes', + background: '--bg', + roundness: 3, + blur: '5px', + shadow: [{ + x: 1, + y: 1, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }] + } + } + ] +} diff --git a/src/components/panel_header.style.js b/src/components/panel_header.style.js new file mode 100644 index 00000000..010e42cd --- /dev/null +++ b/src/components/panel_header.style.js @@ -0,0 +1,23 @@ +export default { + name: 'PanelHeader', + selector: '.panel-heading', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Button', + 'ButtonUnstyled', + 'Badge', + 'Alert', + 'Avatar' + ], + defaultRules: [ + { + component: 'PanelHeader', + directives: { + backgroundNoCssColor: 'yes', + background: '--fg' + } + } + ] +} diff --git a/src/components/panel_loading/panel_loading.vue b/src/components/panel_loading/panel_loading.vue index 17458e49..7a832122 100644 --- a/src/components/panel_loading/panel_loading.vue +++ b/src/components/panel_loading/panel_loading.vue @@ -23,22 +23,18 @@ export default {} </script> <style lang="scss"> -@import "src/variables"; - .panel-loading { display: flex; height: 100%; align-items: center; justify-content: center; font-size: 2em; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); .loading-text svg { line-height: 0; vertical-align: middle; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } </style> diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue index d6e10250..037b859e 100644 --- a/src/components/password_reset/password_reset.vue +++ b/src/components/password_reset/password_reset.vue @@ -30,7 +30,7 @@ <div v-else> <p v-if="passwordResetRequested" - class="password-reset-required error" + class="alert password-reset-required error" > {{ $t('password_reset.password_reset_required') }} </p> @@ -43,7 +43,7 @@ v-model="user.email" :disabled="isPending" :placeholder="$t('password_reset.placeholder')" - class="form-control" + class="input form-control" type="input" > </div> @@ -77,8 +77,6 @@ <script src="./password_reset.js"></script> <style lang="scss"> -@import "../../variables"; - .password-reset-form { display: flex; flex-direction: column; @@ -117,11 +115,6 @@ margin: 0.3em 0 1em; } - .password-reset-required { - background-color: var(--alertError, $fallback--alertError); - padding: 10px 0; - } - .notice-dismissible { padding-right: 2rem; } diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js index eda1733a..9ce0e29e 100644 --- a/src/components/poll/poll.js +++ b/src/components/poll/poll.js @@ -1,4 +1,5 @@ import Timeago from 'components/timeago/timeago.vue' +import genRandomSeed from '../../services/random_seed/random_seed.service.js' import RichContent from 'components/rich_content/rich_content.jsx' import { forEach, map } from 'lodash' @@ -12,7 +13,8 @@ export default { data () { return { loading: false, - choices: [] + choices: [], + randomSeed: genRandomSeed() } }, created () { @@ -36,7 +38,7 @@ export default { return (this.poll && this.poll.options) || [] }, expiresAt () { - return (this.poll && this.poll.expires_at) || 0 + return (this.poll && this.poll.expires_at) || null }, expired () { return (this.poll && this.poll.expired) || false diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index cacc3298..e12f3e61 100644 --- a/src/components/poll/poll.vue +++ b/src/components/poll/poll.vue @@ -4,53 +4,66 @@ :class="containerClass" > <div - v-for="(option, index) in options" - :key="index" - class="poll-option" + :role="showResults ? 'section' : (poll.multiple ? 'group' : 'radiogroup')" > <div - v-if="showResults" - :title="resultTitle(option)" - class="option-result" + v-for="(option, index) in options" + :key="index" + class="poll-option" > - <div class="option-result-label"> - <span class="result-percentage"> - {{ percentageForOption(option.votes_count) }}% - </span> - <RichContent - :html="option.title_html" - :handle-links="false" - :emoji="emoji" + <div + v-if="showResults" + :title="resultTitle(option)" + class="option-result" + > + <div class="option-result-label"> + <span class="result-percentage"> + {{ percentageForOption(option.votes_count) }}% + </span> + <RichContent + :html="option.title_html" + :handle-links="false" + :emoji="emoji" + /> + </div> + <div + class="result-fill" + :style="{ 'width': `${percentageForOption(option.votes_count)}%` }" /> </div> <div - class="result-fill" - :style="{ 'width': `${percentageForOption(option.votes_count)}%` }" - /> - </div> - <div - v-else - @click="activateOption(index)" - > - <input - v-if="poll.multiple" - type="checkbox" - :disabled="loading" - :value="index" - > - <input v-else - type="radio" - :disabled="loading" - :value="index" + tabindex="0" + :role="poll.multiple ? 'checkbox' : 'radio'" + :aria-labelledby="`option-vote-${randomSeed}-${index}`" + :aria-checked="choices[index]" + class="input unstyled" + @click="activateOption(index)" > - <label class="option-vote"> - <RichContent - :html="option.title_html" - :handle-links="false" - :emoji="emoji" - /> - </label> + <!-- TODO: USE CHECKBOX --> + <input + v-if="poll.multiple" + type="checkbox" + class="input -checkbox poll-checkbox" + :disabled="loading" + :value="index" + > + <input + v-else + type="radio" + :disabled="loading" + :value="index" + class="input -radio" + > + <label class="option-vote"> + <RichContent + :id="`option-vote-${randomSeed}-${index}`" + :html="option.title_html" + :handle-links="false" + :emoji="emoji" + /> + </label> + </div> </div> </div> <div class="footer faint"> @@ -63,15 +76,25 @@ > {{ $t('polls.vote') }} </button> + <span + v-if="poll.pleroma?.non_anonymous" + :title="$t('polls.non_anonymous_title')" + > + {{ $t('polls.non_anonymous') }} + · + </span> <div class="total"> <template v-if="typeof poll.voters_count === 'number'"> - {{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }} · + {{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }} </template> <template v-else> - {{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }} · + {{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }} </template> + <span v-if="expiresAt !== null"> + · + </span> </div> - <span> + <span v-if="expiresAt !== null"> <i18n-t scope="global" :keypath="expired ? 'polls.expired' : 'polls.expires_in'" @@ -90,8 +113,6 @@ <script src="./poll.js"></script> <style lang="scss"> -@import "../../variables"; - .poll { .votes { display: flex; @@ -101,6 +122,10 @@ .poll-option { margin: 0.75em 0.5em; + + .input { + line-height: inherit; + } } .option-result { @@ -108,8 +133,7 @@ display: flex; flex-direction: row; position: relative; - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); + color: var(--textLight); } .option-result-label { @@ -128,12 +152,7 @@ .result-fill { height: 100%; position: absolute; - color: $fallback--text; - color: var(--pollText, $fallback--text); - background-color: $fallback--lightBg; - background-color: var(--poll, $fallback--lightBg); - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); top: 0; left: 0; transition: width 0.5s; @@ -161,5 +180,9 @@ padding: 0 0.5em; margin-right: 0.5em; } + + .poll-checkbox { + display: none; + } } </style> diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue index 09d411ca..4eb9d594 100644 --- a/src/components/poll/poll_form.vue +++ b/src/components/poll/poll_form.vue @@ -13,7 +13,7 @@ :id="`poll-${index}`" v-model="options[index]" size="1" - class="poll-option-input" + class="input poll-option-input" type="text" :placeholder="$t('polls.option')" :maxlength="maxLength" @@ -67,7 +67,7 @@ <input v-model="expiryAmount" type="number" - class="expiry-amount hide-number-spinner" + class="input expiry-amount hide-number-spinner" :min="minExpirationInCurrentUnit" :max="maxExpirationInCurrentUnit" @change="expiryAmountChange" @@ -95,8 +95,6 @@ <script src="./poll_form.js"></script> <style lang="scss"> -@import "../../variables"; - .poll-form { display: flex; flex-direction: column; diff --git a/src/components/poll/poll_graph.style.js b/src/components/poll/poll_graph.style.js new file mode 100644 index 00000000..247a266a --- /dev/null +++ b/src/components/poll/poll_graph.style.js @@ -0,0 +1,12 @@ +export default { + name: 'PollGraph', + selector: '.result-fill', + defaultRules: [ + { + directives: { + background: '--accent', + opacity: 0.5 + } + } + ] +} diff --git a/src/components/popover.style.js b/src/components/popover.style.js new file mode 100644 index 00000000..0197271b --- /dev/null +++ b/src/components/popover.style.js @@ -0,0 +1,36 @@ +export default { + name: 'Popover', + selector: '.popover', + lazy: true, + variants: { + modal: '.modal' + }, + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'Input', + 'MenuItem', + 'Post', + 'UserCard' + ], + defaultRules: [ + { + directives: { + background: '--bg', + blur: '10px', + shadow: [{ + x: 2, + y: 2, + blur: 3, + spread: 0, + color: '#000000', + alpha: 0.5 + }] + } + } + ] +} diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js index d44b266b..bc078533 100644 --- a/src/components/popover/popover.js +++ b/src/components/popover/popover.js @@ -45,6 +45,9 @@ const Popover = { // Lets hover popover stay when clicking inside of it stayOnClick: Boolean, + // Use styled button (to avoid nested buttons) + normalButton: Boolean, + triggerAttrs: { type: Object, default: {} diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue index fd0fd821..0c5b372e 100644 --- a/src/components/popover/popover.vue +++ b/src/components/popover/popover.vue @@ -5,7 +5,8 @@ > <button ref="trigger" - class="button-unstyled popover-trigger-button" + class="popover-trigger-button" + :class="normalButton ? 'button-default btn' : 'button-unstyled'" type="button" v-bind="triggerAttrs" @click="onClick" @@ -41,8 +42,6 @@ <script src="./popover.js" /> <style lang="scss"> -@import "../../variables"; - .popover-trigger-button { display: inline-block; } @@ -52,81 +51,54 @@ position: fixed; min-width: 0; max-width: calc(100vw - 20px); - box-shadow: 2px 2px 3px rgb(0 0 0 / 50%); - box-shadow: var(--popupShadow); + box-shadow: var(--shadow); } .popover-default { &::after { content: ""; position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 3; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + top: -1px; + bottom: -1px; + left: -1px; + right: -1px; + z-index: -1px; + box-shadow: var(--shadow); pointer-events: none; } - border-radius: $fallback--btnRadius; - border-radius: var(--btnRadius, $fallback--btnRadius); - background-color: $fallback--bg; - background-color: var(--popover, $fallback--bg); - color: $fallback--text; - color: var(--popoverText, $fallback--text); - - --faint: var(--popoverFaintText, $fallback--faint); - --faintLink: var(--popoverFaintLink, $fallback--faint); - --lightText: var(--popoverLightText, $fallback--lightText); - --postLink: var(--popoverPostLink, $fallback--link); - --postFaintLink: var(--popoverPostFaintLink, $fallback--link); - --icon: var(--popoverIcon, $fallback--icon); + border-radius: var(--roundness); + border-color: var(--border); + border-style: solid; + border-width: 1px; + background-color: var(--background); } .dropdown-menu { display: block; - padding: 0.5rem 0; + 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 $fallback--border; - border-top: 1px solid var(--border, $fallback--border); + border-top: 1px solid var(--border); } .dropdown-item { - line-height: 21px; - overflow: hidden; - display: block; - padding: 0.5em 0.75em; - clear: both; - font-weight: 400; - text-align: inherit; - white-space: nowrap; border: none; - border-radius: 0; - background-color: transparent; - box-shadow: none; - width: 100%; - height: 100%; - box-sizing: border-box; - - --btnText: var(--popoverText, $fallback--text); &-icon { svg { - width: 22px; - margin-right: 0.75rem; - color: var(--menuPopoverIcon, $fallback--icon); + width: var(--__line-height); + margin-right: var(--__horizontal-gap); } } @@ -137,40 +109,18 @@ } } - &:active, - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenuPopover, $fallback--lightBg); - box-shadow: none; - - --btnText: var(--selectedMenuPopoverText, $fallback--link); - --faint: var(--selectedMenuPopoverFaintText, $fallback--faint); - --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); - --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - - svg { - color: var(--selectedMenuPopoverIcon, $fallback--icon); - - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - } - } - .menu-checkbox { display: inline-block; vertical-align: middle; - min-width: 22px; - max-width: 22px; - min-height: 22px; - max-height: 22px; - line-height: 22px; + 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; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); - margin-right: 0.75em; + box-shadow: var(--shadow); + margin-right: var(--__horizontal-gap); &.menu-checkbox-checked::after { font-size: 1.25em; @@ -187,30 +137,5 @@ } } } - - .button-default.dropdown-item { - &, - i[class*="icon-"] { - color: $fallback--text; - color: var(--btnText, $fallback--text); - } - - &:active { - background-color: $fallback--lightBg; - background-color: var(--selectedMenuPopover, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuPopoverText, $fallback--link); - } - - &:disabled { - color: $fallback--text; - color: var(--btnDisabledText, $fallback--text); - } - - &.toggled { - color: $fallback--text; - color: var(--btnToggledText, $fallback--text); - } - } } </style> diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index b75fee69..563dfb96 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -1,4 +1,5 @@ import statusPoster from '../../services/status_poster/status_poster.service.js' +import genRandomSeed from '../../services/random_seed/random_seed.service.js' import MediaUpload from '../media_upload/media_upload.vue' import ScopeSelector from '../scope_selector/scope_selector.vue' import EmojiInput from '../emoji_input/emoji_input.vue' @@ -86,7 +87,8 @@ const PostStatusForm = { 'fileLimit', 'submitOnEnter', 'emojiPickerPlacement', - 'optimisticPosting' + 'optimisticPosting', + 'profileMention' ], emits: [ 'posted', @@ -124,7 +126,7 @@ const PostStatusForm = { const { scopeCopy } = this.$store.getters.mergedConfig - if (this.replyTo) { + if (this.replyTo || this.profileMention) { const currentUser = this.$store.state.users.currentUser statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser) } @@ -156,11 +158,13 @@ const PostStatusForm = { poll: this.statusPoll || {}, mediaDescriptions: this.statusMediaDescriptions || {}, visibility: this.statusScope || scope, - contentType: statusContentType + contentType: statusContentType, + quoting: false } } return { + randomSeed: genRandomSeed(), dropFiles: [], uploadingFiles: false, error: null, @@ -265,6 +269,30 @@ const PostStatusForm = { isEdit () { return typeof this.statusId !== 'undefined' && this.statusId.trim() !== '' }, + quotable () { + if (!this.$store.state.instance.quotingAvailable) { + return false + } + + if (!this.replyTo) { + return false + } + + const repliedStatus = this.$store.state.statuses.allStatusesObject[this.replyTo] + if (!repliedStatus) { + return false + } + + if (repliedStatus.visibility === 'public' || + repliedStatus.visibility === 'unlisted' || + repliedStatus.visibility === 'local') { + return true + } else if (repliedStatus.visibility === 'private') { + return repliedStatus.user.id === this.$store.state.users.currentUser.id + } + + return false + }, ...mapGetters(['mergedConfig']), ...mapState({ mobileLayout: state => state.interface.mobileLayout @@ -292,7 +320,8 @@ const PostStatusForm = { visibility: newStatus.visibility, contentType: newStatus.contentType, poll: {}, - mediaDescriptions: {} + mediaDescriptions: {}, + quoting: false } this.pollFormVisible = false this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile() @@ -340,6 +369,8 @@ const PostStatusForm = { return } + const replyOrQuoteAttr = newStatus.quoting ? 'quoteId' : 'inReplyToStatusId' + const postingOptions = { status: newStatus.status, spoilerText: newStatus.spoilerText || null, @@ -347,7 +378,7 @@ const PostStatusForm = { sensitive: newStatus.nsfw, media: newStatus.files, store: this.$store, - inReplyToStatusId: this.replyTo, + [replyOrQuoteAttr]: this.replyTo, contentType: newStatus.contentType, poll, idempotencyKey: this.idempotencyKey @@ -373,6 +404,7 @@ const PostStatusForm = { } const newStatus = this.newStatus this.previewLoading = true + const replyOrQuoteAttr = newStatus.quoting ? 'quoteId' : 'inReplyToStatusId' statusPoster.postStatus({ status: newStatus.status, spoilerText: newStatus.spoilerText || null, @@ -380,7 +412,7 @@ const PostStatusForm = { sensitive: newStatus.nsfw, media: [], store: this.$store, - inReplyToStatusId: this.replyTo, + [replyOrQuoteAttr]: this.replyTo, contentType: newStatus.contentType, poll: {}, preview: true diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 64a8887c..cf411ff1 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -126,12 +126,42 @@ class="preview-status" /> </div> + <div + v-if="quotable" + role="radiogroup" + class="btn-group reply-or-quote-selector" + > + <button + :id="`reply-or-quote-option-${randomSeed}-reply`" + class="btn button-default reply-or-quote-option" + :class="{ toggled: !newStatus.quoting }" + tabindex="0" + role="radio" + :aria-labelledby="`reply-or-quote-option-${randomSeed}-reply`" + :aria-checked="!newStatus.quoting" + @click="newStatus.quoting = false" + > + {{ $t('post_status.reply_option') }} + </button> + <button + :id="`reply-or-quote-option-${randomSeed}-quote`" + class="btn button-default reply-or-quote-option" + :class="{ toggled: newStatus.quoting }" + tabindex="0" + role="radio" + :aria-labelledby="`reply-or-quote-option-${randomSeed}-quote`" + :aria-checked="newStatus.quoting" + @click="newStatus.quoting = true" + > + {{ $t('post_status.quote_option') }} + </button> + </div> <EmojiInput v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)" v-model="newStatus.spoilerText" enable-emoji-picker :suggest="emojiSuggestor" - class="form-control" + class="input form-control" > <template #default="inputProps"> <input @@ -141,7 +171,7 @@ :disabled="posting && !optimisticPosting" v-bind="propsToNative(inputProps)" size="1" - class="form-post-subject" + class="input form-post-subject" > </template> </EmojiInput> @@ -150,7 +180,7 @@ v-model="newStatus.status" :suggest="emojiUserSuggestor" :placement="emojiPickerPlacement" - class="form-control main-input" + class="input form-control main-input" enable-emoji-picker hide-emoji-button :newline-on-ctrl-enter="submitOnEnter" @@ -168,7 +198,7 @@ rows="1" cols="1" :disabled="posting && !optimisticPosting" - class="form-post-body" + class="input form-post-body" :class="{ 'scrollable-form': !!maxHeight }" v-bind="propsToNative(inputProps)" @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" @@ -207,7 +237,7 @@ <Select id="post-content-type" v-model="newStatus.contentType" - class="form-control" + class="input form-control" :attrs="{ 'aria-label': $t('post_status.content_type_selection') }" > <option @@ -281,12 +311,10 @@ > {{ $t('post_status.post') }} </button> - <!-- touchstart is used to keep the OSK at the same position after a message send --> <button v-else :disabled="uploadingFiles || disableSubmit" class="btn button-default" - @touchstart.stop.prevent="postStatus($event, newStatus)" @click.stop.prevent="postStatus($event, newStatus)" > {{ $t('post_status.post') }} @@ -347,8 +375,6 @@ <script src="./post_status_form.js"></script> <style lang="scss"> -@import "../../variables"; - .post-status-form { position: relative; @@ -409,23 +435,23 @@ .preview-error { font-style: italic; - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--textFaint); } .preview-status { - border: 1px solid $fallback--border; - border: 1px solid var(--border, $fallback--border); - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border: 1px solid var(--border); + border-radius: var(--roundness); padding: 0.5em; margin: 0; } + .reply-or-quote-selector { + margin-bottom: 0.5em; + } + .text-format { .only-format { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--textFaint); } } @@ -471,31 +497,6 @@ padding: 0 0.1em; display: flex; align-items: center; - - &.selected, - &:hover { - // needs to be specific to override icon default color - svg, - i, - label { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } - } - - &.disabled { - svg, - i { - cursor: not-allowed; - color: $fallback--icon; - color: var(--btnDisabledText, $fallback--icon); - - &:hover { - color: $fallback--icon; - color: var(--btnDisabledText, $fallback--icon); - } - } - } } .error { @@ -548,7 +549,7 @@ line-height: 1.85; } - .form-post-body { + .input.form-post-body { // TODO: make a resizable textarea component? box-sizing: content-box; // needed for easier computation of dynamic size overflow: hidden; @@ -559,6 +560,7 @@ height: calc(var(--post-line-height) * 1em); min-height: calc(var(--post-line-height) * 1em); resize: none; + background: transparent; &.scrollable-form { overflow-y: auto; @@ -577,8 +579,7 @@ margin: 0 0.5em; &.error { - color: $fallback--cRed; - color: var(--cRed, $fallback--cRed); + color: var(--cRed); } } @@ -601,14 +602,10 @@ align-items: center; justify-content: center; opacity: 0.6; - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - border: 2px dashed $fallback--text; - border: 2px dashed var(--text, $fallback--text); + color: var(--text); + background-color: var(--bg); + border-radius: var(--roundness); + border: 2px dashed var(--text); } } </style> diff --git a/src/components/post_status_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js index b44354db..8970dd9b 100644 --- a/src/components/post_status_modal/post_status_modal.js +++ b/src/components/post_status_modal/post_status_modal.js @@ -44,6 +44,10 @@ const PostStatusModal = { methods: { closeModal () { this.$store.dispatch('closePostStatusModal') + }, + resetAndClose () { + this.$store.dispatch('resetPostStatusModal') + this.$store.dispatch('closePostStatusModal') } } } diff --git a/src/components/post_status_modal/post_status_modal.vue b/src/components/post_status_modal/post_status_modal.vue index dbcd321e..bc2cad4a 100644 --- a/src/components/post_status_modal/post_status_modal.vue +++ b/src/components/post_status_modal/post_status_modal.vue @@ -12,7 +12,7 @@ <PostStatusForm class="panel-body" v-bind="params" - @posted="closeModal" + @posted="resetAndClose" /> </div> </Modal> diff --git a/src/components/quick_filter_settings/quick_filter_settings.js b/src/components/quick_filter_settings/quick_filter_settings.js index e67e3a4b..0d667df5 100644 --- a/src/components/quick_filter_settings/quick_filter_settings.js +++ b/src/components/quick_filter_settings/quick_filter_settings.js @@ -63,6 +63,13 @@ const QuickFilterSettings = { const value = !this.muteBotStatuses this.$store.dispatch('setOption', { name: 'muteBotStatuses', value }) } + }, + muteSensitiveStatuses: { + get () { return this.mergedConfig.muteSensitiveStatuses }, + set () { + const value = !this.muteSensitiveStatuses + this.$store.dispatch('setOption', { name: 'muteSensitiveStatuses', value }) + } } } } diff --git a/src/components/quick_filter_settings/quick_filter_settings.vue b/src/components/quick_filter_settings/quick_filter_settings.vue index b81215a1..adb96916 100644 --- a/src/components/quick_filter_settings/quick_filter_settings.vue +++ b/src/components/quick_filter_settings/quick_filter_settings.vue @@ -16,39 +16,39 @@ > <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilityAll" role="menuitemradio" @click="replyVisibilityAll = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilityAll }" :aria-hidden="true" />{{ $t('settings.reply_visibility_all') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilityFollowing" role="menuitemradio" @click="replyVisibilityFollowing = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilityFollowing }" :aria-hidden="true" />{{ $t('settings.reply_visibility_following_short') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilitySelf" role="menuitemradio" @click="replyVisibilitySelf = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilitySelf }" :aria-hidden="true" />{{ $t('settings.reply_visibility_self_short') }} @@ -60,43 +60,55 @@ /> </div> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="muteBotStatuses" @click="muteBotStatuses = !muteBotStatuses" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': muteBotStatuses }" :aria-hidden="true" />{{ $t('settings.mute_bot_posts') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" + role="menuitemcheckbox" + :aria-checked="muteSensitiveStatuses" + @click="muteSensitiveStatuses = !muteSensitiveStatuses" + > + <span + class="input menu-checkbox" + :class="{ 'menu-checkbox-checked': muteSensitiveStatuses }" + :aria-hidden="true" + />{{ $t('settings.mute_sensitive_posts') }} + </button> + <button + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="hideMedia" @click="hideMedia = !hideMedia" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hideMedia }" :aria-hidden="true" />{{ $t('settings.hide_media_previews') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="hideMutedPosts" @click="hideMutedPosts = !hideMutedPosts" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hideMutedPosts }" :aria-hidden="true" />{{ $t('settings.hide_all_muted_posts') }} </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click="openTab('filtering')" > diff --git a/src/components/quick_view_settings/quick_view_settings.js b/src/components/quick_view_settings/quick_view_settings.js index 2798f37a..e35fd579 100644 --- a/src/components/quick_view_settings/quick_view_settings.js +++ b/src/components/quick_view_settings/quick_view_settings.js @@ -52,7 +52,6 @@ const QuickViewSettings = { get () { return this.mergedConfig.mentionLinkShowAvatar }, set () { const value = !this.showUserAvatars - console.log(value) this.$store.dispatch('setOption', { name: 'mentionLinkShowAvatar', value }) } }, @@ -62,6 +61,13 @@ const QuickViewSettings = { const value = !this.muteBotStatuses this.$store.dispatch('setOption', { name: 'muteBotStatuses', value }) } + }, + muteSensitiveStatuses: { + get () { return this.mergedConfig.muteSensitiveStatuses }, + set () { + const value = !this.muteSensitiveStatuses + this.$store.dispatch('setOption', { name: 'muteSensitiveStatuses', value }) + } } } } diff --git a/src/components/quick_view_settings/quick_view_settings.vue b/src/components/quick_view_settings/quick_view_settings.vue index 9f5cdabc..e93dd8e0 100644 --- a/src/components/quick_view_settings/quick_view_settings.vue +++ b/src/components/quick_view_settings/quick_view_settings.vue @@ -12,13 +12,13 @@ > <div role="group"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="conversationDisplay === 'tree'" role="menuitemradio" @click="conversationDisplay = 'tree'" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :aria-hidden="true" :class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }" /><FAIcon @@ -27,13 +27,13 @@ /> {{ $t('settings.conversation_display_tree_quick') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="conversationDisplay === 'linear'" role="menuitemradio" @click="conversationDisplay = 'linear'" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }" :aria-hidden="true" /><FAIcon @@ -47,45 +47,45 @@ class="dropdown-divider" /> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="showUserAvatars" @click="showUserAvatars = !showUserAvatars" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': showUserAvatars }" :aria-hidden="true" />{{ $t('settings.mention_link_show_avatar_quick') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="autoUpdate" @click="autoUpdate = !autoUpdate" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': autoUpdate }" :aria-hidden="true" />{{ $t('settings.auto_update') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="collapseWithSubjects" @click="collapseWithSubjects = !collapseWithSubjects" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': collapseWithSubjects }" :aria-hidden="true" />{{ $t('settings.collapse_subject') }} </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click="openTab('general')" > diff --git a/src/components/quotes_timeline/quotes_timeline.js b/src/components/quotes_timeline/quotes_timeline.js new file mode 100644 index 00000000..a5f42da5 --- /dev/null +++ b/src/components/quotes_timeline/quotes_timeline.js @@ -0,0 +1,26 @@ +import Timeline from '../timeline/timeline.vue' + +const QuotesTimeline = { + created () { + this.$store.commit('clearTimeline', { timeline: 'quotes' }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'quotes', statusId: this.statusId }) + }, + components: { + Timeline + }, + computed: { + statusId () { return this.$route.params.id }, + timeline () { return this.$store.state.statuses.timelines.quotes } + }, + watch: { + statusId () { + this.$store.commit('clearTimeline', { timeline: 'quotes' }) + this.$store.dispatch('startFetchingTimeline', { timeline: 'quotes', statusId: this.statusId }) + } + }, + unmounted () { + this.$store.dispatch('stopFetchingTimeline', 'quotes') + } +} + +export default QuotesTimeline diff --git a/src/components/quotes_timeline/quotes_timeline.vue b/src/components/quotes_timeline/quotes_timeline.vue new file mode 100644 index 00000000..835abd12 --- /dev/null +++ b/src/components/quotes_timeline/quotes_timeline.vue @@ -0,0 +1,10 @@ +<template> + <Timeline + :title="$t('nav.quotes')" + :timeline="timeline" + :timeline-name="'quotes'" + :status-id="statusId" + /> +</template> + +<script src='./quotes_timeline.js'></script> diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue index 1e7e42d5..2f8645c0 100644 --- a/src/components/range_input/range_input.vue +++ b/src/components/range_input/range_input.vue @@ -4,6 +4,7 @@ :class="{ disabled: !present || disabled }" > <label + :id="name + '-label'" :for="name" class="label" > @@ -12,7 +13,8 @@ <input v-if="typeof fallback !== 'undefined'" :id="name + '-o'" - class="opt" + :aria-labelledby="name + '-label'" + class="input -checkbox opt visible-for-screenreader-only" type="checkbox" :checked="present" @change="$emit('update:modelValue', !present ? fallback : undefined)" @@ -21,10 +23,11 @@ v-if="typeof fallback !== 'undefined'" class="opt-l" :for="name + '-o'" + :aria-hidden="true" /> <input :id="name" - class="input-number" + class="input input-number" type="range" :value="modelValue || fallback" :disabled="!present || disabled" @@ -34,9 +37,10 @@ @input="$emit('update:modelValue', $event.target.value)" > <input - :id="name" - class="input-number" + :id="name + '-numeric'" + class="input input-number" type="number" + :aria-labelledby="name + '-label'" :value="modelValue || fallback" :disabled="!present || disabled" :max="hardMax" diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js index 47a48623..0d252155 100644 --- a/src/components/react_button/react_button.js +++ b/src/components/react_button/react_button.js @@ -1,9 +1,8 @@ import Popover from '../popover/popover.vue' -import { ensureFinalFallback } from '../../i18n/languages.js' +import EmojiPicker from '../emoji_picker/emoji_picker.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons' import { faSmileBeam } from '@fortawesome/free-regular-svg-icons' -import { trim } from 'lodash' library.add( faPlus, @@ -20,105 +19,34 @@ const ReactButton = { } }, components: { - Popover + Popover, + EmojiPicker }, methods: { - addReaction (event, emoji, close) { + addReaction (event) { + const emoji = event.insertion const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji) if (existingReaction && existingReaction.me) { this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) } else { this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) } - close() + }, + show () { + if (!this.expanded) { + this.$refs.picker.showPicker() + } }, onShow () { this.expanded = true - this.focusInput() }, onClose () { this.expanded = false - }, - focusInput () { - this.$nextTick(() => { - const input = document.querySelector('.reaction-picker-filter > input') - if (input) input.focus() - }) - }, - // Vaguely adjusted copypaste from emoji_input and emoji_picker! - maybeLocalizedEmojiNamesAndKeywords (emoji) { - const names = [emoji.displayText] - const keywords = [] - - if (emoji.displayTextI18n) { - names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)) - } - - if (emoji.annotations) { - this.languages.forEach(lang => { - names.push(emoji.annotations[lang]?.name) - - keywords.push(...(emoji.annotations[lang]?.keywords || [])) - }) - } - - return { - names: names.filter(k => k), - keywords: keywords.filter(k => k) - } - }, - maybeLocalizedEmojiName (emoji) { - if (!emoji.annotations) { - return emoji.displayText - } - - if (emoji.displayTextI18n) { - return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args) - } - - for (const lang of this.languages) { - if (emoji.annotations[lang]?.name) { - return emoji.annotations[lang].name - } - } - - return emoji.displayText } }, computed: { - commonEmojis () { - const hardcodedSet = new Set(['👍', '😠', '👀', '😂', '🔥']) - return this.$store.getters.standardEmojiList.filter(emoji => hardcodedSet.has(emoji.replacement)) - }, - languages () { - return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage) - }, - emojis () { - if (this.filterWord !== '') { - const keywordLowercase = trim(this.filterWord.toLowerCase()) - - const orderedEmojiList = [] - for (const emoji of this.$store.getters.standardEmojiList) { - const indices = this.maybeLocalizedEmojiNamesAndKeywords(emoji) - .keywords - .map(k => k.toLowerCase().indexOf(keywordLowercase)) - .filter(k => k > -1) - - const indexOfKeyword = indices.length ? Math.min(...indices) : -1 - - if (indexOfKeyword > -1) { - if (!Array.isArray(orderedEmojiList[indexOfKeyword])) { - orderedEmojiList[indexOfKeyword] = [] - } - orderedEmojiList[indexOfKeyword].push(emoji) - } - } - return orderedEmojiList.flat() - } - return this.$store.getters.standardEmojiList || [] - }, - mergedConfig () { - return this.$store.getters.mergedConfig + hideCustomEmoji () { + return !this.$store.state.instance.pleromaCustomEmojiReactionsAvailable } } } diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue index a813b6fd..e648e7e3 100644 --- a/src/components/react_button/react_button.vue +++ b/src/components/react_button/react_button.vue @@ -1,79 +1,46 @@ <template> - <Popover - trigger="click" - class="ReactButton" - placement="top" - :offset="{ y: 5 }" - :bound-to="{ x: 'container' }" - remove-padding - popover-class="ReactButton popover-default" - @show="onShow" - @close="onClose" - > - <template #content="{close}"> - <div class="reaction-picker-filter"> - <input - v-model="filterWord" - size="1" - :placeholder="$t('emoji.search_emoji')" - @input="$event.target.composing = false" - > - </div> - <div class="reaction-picker"> - <span - v-for="emoji in commonEmojis" - :key="emoji.replacement" - class="emoji-button" - :title="maybeLocalizedEmojiName(emoji)" - @click="addReaction($event, emoji.replacement, close)" - > - {{ emoji.replacement }} - </span> - <div class="reaction-picker-divider" /> - <span - v-for="(emoji, key) in emojis" - :key="key" - class="emoji-button" - :title="maybeLocalizedEmojiName(emoji)" - @click="addReaction($event, emoji.replacement, close)" - > - {{ emoji.replacement }} - </span> - <div class="reaction-bottom-fader" /> - </div> - </template> - <template #trigger> - <span - class="button-unstyled popover-trigger" - :title="$t('tool_tip.add_reaction')" - > - <FALayers> - <FAIcon - class="fa-scale-110 fa-old-padding" - :icon="['far', 'smile-beam']" - /> - <FAIcon - v-show="!expanded" - class="focus-marker" - transform="shrink-6 up-9 right-17" - icon="plus" - /> - <FAIcon - v-show="expanded" - class="focus-marker" - transform="shrink-6 up-9 right-17" - icon="times" - /> - </FALayers> - </span> - </template> - </Popover> + <span class="ReactButton"> + <EmojiPicker + ref="picker" + :enable-sticker-picker="enableStickerPicker" + :hide-custom-emoji="hideCustomEmoji" + class="emoji-picker-panel" + @emoji="addReaction" + @show="onShow" + @close="onClose" + /> + <span + class="button-unstyled popover-trigger" + role="button" + :tabindex="0" + :title="$t('tool_tip.add_reaction')" + @click.stop.prevent="show" + > + <FALayers> + <FAIcon + class="fa-scale-110 fa-old-padding" + :icon="['far', 'smile-beam']" + /> + <FAIcon + v-show="!expanded" + class="focus-marker" + transform="shrink-6 up-9 right-17" + icon="plus" + /> + <FAIcon + v-show="expanded" + class="focus-marker" + transform="shrink-6 up-9 right-17" + icon="times" + /> + </FALayers> + </span> + </span> </template> <script src="./react_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .ReactButton { @@ -90,7 +57,7 @@ height: 1px; width: 100%; margin: 0.5em; - background-color: var(--border, $fallback--border); + background-color: var(--border); } .reaction-picker { @@ -131,16 +98,6 @@ padding: 10px; margin: -10px; - &:hover .svg-inline--fa { - color: $fallback--text; - color: var(--text, $fallback--text); - } - } - - .popover-trigger-button { - /* override of popover internal stuff */ - width: auto; - @include unfocused-style { .focus-marker { visibility: hidden; diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js index 22ca6ad6..78d31980 100644 --- a/src/components/registration/registration.js +++ b/src/components/registration/registration.js @@ -16,7 +16,7 @@ const registration = { confirm: '', birthday: '', reason: '', - language: '' + language: [''] }, captcha: {} }), @@ -83,6 +83,8 @@ const registration = { signedIn: (state) => !!state.users.currentUser, isPending: (state) => state.users.signUpPending, serverValidationErrors: (state) => state.users.signUpErrors, + signUpNotice: (state) => state.users.signUpNotice, + hasSignUpNotice: (state) => !!state.users.signUpNotice.message, termsOfService: (state) => state.instance.tos, accountActivationRequired: (state) => state.instance.accountActivationRequired, accountApprovalRequired: (state) => state.instance.accountApprovalRequired, @@ -100,15 +102,19 @@ const registration = { this.user.captcha_token = this.captcha.token this.user.captcha_answer_data = this.captcha.answer_data if (this.user.language) { - this.user.language = localeService.internalToBackendLocale(this.user.language) + this.user.language = localeService.internalToBackendLocaleMulti(this.user.language.filter(k => k)) } this.v$.$touch() if (!this.v$.$invalid) { try { - await this.signUp(this.user) - this.$router.push({ name: 'friends' }) + const status = await this.signUp(this.user) + if (status === 'ok') { + this.$router.push({ name: 'friends' }) + } + // If status is not 'ok' (i.e. it needs further actions to be done + // before you can login), display sign up notice, do not switch anywhere } catch (error) { console.warn('Registration failed: ', error) this.setCaptcha() diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index 5701b05e..a2008d87 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -3,7 +3,10 @@ <div class="panel-heading"> {{ $t('registration.registration') }} </div> - <div class="panel-body"> + <div + v-if="!hasSignUpNotice" + class="panel-body" + > <form class="registration-form" @submit.prevent="submit(user)" @@ -22,7 +25,7 @@ id="sign-up-username" v-model.trim="v$.user.username.$model" :disabled="isPending" - class="form-control" + class="input form-control" :aria-required="true" :placeholder="$t('registration.username_placeholder')" > @@ -50,7 +53,7 @@ id="sign-up-fullname" v-model.trim="v$.user.fullname.$model" :disabled="isPending" - class="form-control" + class="input form-control" :aria-required="true" :placeholder="$t('registration.fullname_placeholder')" > @@ -78,7 +81,7 @@ id="email" v-model="v$.user.email.$model" :disabled="isPending" - class="form-control" + class="input form-control" type="email" :aria-required="accountActivationRequired" > @@ -103,7 +106,7 @@ id="bio" v-model="user.bio" :disabled="isPending" - class="form-control" + class="input form-control" :placeholder="bioPlaceholder" /> </div> @@ -120,7 +123,7 @@ id="sign-up-password" v-model="user.password" :disabled="isPending" - class="form-control" + class="input form-control" type="password" :aria-required="true" > @@ -148,7 +151,7 @@ id="sign-up-password-confirmation" v-model="user.confirm" :disabled="isPending" - class="form-control" + class="input form-control" type="password" :aria-required="true" > @@ -181,7 +184,7 @@ id="sign-up-birthday" v-model="user.birthday" :disabled="isPending" - class="form-control" + class="input form-control" type="date" :max="birthdayRequired ? birthdayMinAttr : undefined" :aria-required="birthdayRequired" @@ -210,6 +213,7 @@ :prompt-text="$t('registration.email_language')" :language="v$.user.language.$model" :set-language="val => v$.user.language.$model = val" + @click.stop.prevent /> </div> @@ -225,7 +229,7 @@ id="reason" v-model="user.reason" :disabled="isPending" - class="form-control" + class="input form-control" :placeholder="reasonPlaceholder" /> </div> @@ -252,7 +256,7 @@ id="captcha-answer" v-model="captcha.solution" :disabled="isPending" - class="form-control" + class="input form-control" type="text" autocomplete="off" autocorrect="off" @@ -271,7 +275,7 @@ id="token" v-model="token" disabled="true" - class="form-control" + class="input form-control" type="text" > </div> @@ -306,14 +310,16 @@ </div> </form> </div> + <div v-else> + <p class="registration-notice"> + {{ signUpNotice.message }} + </p> + </div> </div> </template> <script src="./registration.js"></script> <style lang="scss"> -@import "../../variables"; -$validations-cRed: #f04124; - .registration-form { display: flex; flex-direction: column; @@ -360,8 +366,7 @@ $validations-cRed: #f04124; } .form-group--error .form--label { - color: $validations-cRed; - color: var(--cRed, $validations-cRed); + color: var(--cRed); } .form-error { @@ -403,6 +408,10 @@ $validations-cRed: #f04124; } } +.registration-notice { + margin: 0.6em; +} + @media all and (max-width: 800px) { .registration-form .container { flex-direction: column-reverse; diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue index 60a40a08..87c06e39 100644 --- a/src/components/reply_button/reply_button.vue +++ b/src/components/reply_button/reply_button.vue @@ -59,7 +59,6 @@ <script src="./reply_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .ReplyButton { @@ -78,8 +77,7 @@ .interactive { &:hover .svg-inline--fa, &.-active .svg-inline--fa { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } @include unfocused-style { diff --git a/src/components/report/report.js b/src/components/report/report.js index 76055764..f8675c0f 100644 --- a/src/components/report/report.js +++ b/src/components/report/report.js @@ -1,6 +1,7 @@ import Select from '../select/select.vue' import StatusContent from '../status_content/status_content.vue' import Timeago from '../timeago/timeago.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' const Report = { @@ -10,7 +11,8 @@ const Report = { components: { Select, StatusContent, - Timeago + Timeago, + RichContent }, computed: { report () { diff --git a/src/components/report/report.scss b/src/components/report/report.scss index 9762400b..4249b850 100644 --- a/src/components/report/report.scss +++ b/src/components/report/report.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Report { .report-content { margin: 0.5em 0 1em; @@ -10,12 +8,8 @@ } .reported-status { - border: 1px solid $fallback--faint; - border-color: var(--faint, $fallback--faint); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); - color: $fallback--text; - color: var(--text, $fallback--text); + border: 1px solid var(--border); + border-radius: var(--roundness); display: block; padding: 0.5em; margin: 0.5em 0; diff --git a/src/components/report/report.vue b/src/components/report/report.vue index 1f19cc25..32aaaffd 100644 --- a/src/components/report/report.vue +++ b/src/components/report/report.vue @@ -17,7 +17,7 @@ <Select :id="report-state" v-model="state" - class="form-control" + class="input form-control" > <option v-for="state in ['open', 'closed', 'resolved']" diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index e1b6b153..adda9a42 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -84,7 +84,6 @@ <script src="./retweet_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .RetweetButton { @@ -107,8 +106,7 @@ &:hover .svg-inline--fa, &.-repeated .svg-inline--fa { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--cGreen); } @include unfocused-style { diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index 7881e365..99d7daca 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -8,6 +8,27 @@ import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue' import './rich_content.scss' +const MAYBE_LINE_BREAKING_ELEMENTS = [ + 'blockquote', + 'br', + 'hr', + 'ul', + 'ol', + 'li', + 'p', + 'table', + 'tbody', + 'td', + 'th', + 'thead', + 'tr', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5' +] + /** * RichContent, The Über-powered component for rendering Post HTML. * @@ -58,6 +79,12 @@ export default { required: false, type: Boolean, default: false + }, + // Faint style (for notifs) + faint: { + required: false, + type: Boolean, + default: false } }, // NEVER EVER TOUCH DATA INSIDE RENDER @@ -149,7 +176,9 @@ export default { // Handle tag nodes if (Array.isArray(item)) { const [opener, children, closer] = item - const Tag = getTagName(opener) + let Tag = getTagName(opener) + if (Tag.toLowerCase() === 'script') Tag = 'js-exploit' + if (Tag.toLowerCase() === 'style') Tag = 'css-exploit' const fullAttrs = getAttrs(opener, () => true) const attrs = getAttrs(opener) const previouslyMentions = currentMentions !== null @@ -164,25 +193,22 @@ export default { !(children && typeof children[0] === 'string' && children[0].match(/^\s/)) ? lastSpacing : '' - switch (Tag) { - case 'br': + if (MAYBE_LINE_BREAKING_ELEMENTS.includes(Tag)) { + // all the elements that can cause a line change + currentMentions = null + } else if (Tag === 'img') { // replace images with StillImage + return ['', [mentionsLinePadding, renderImage(opener)], ''] + } else if (Tag === 'a' && this.handleLinks) { // replace mentions with MentionLink + if (fullAttrs.class && fullAttrs.class.includes('mention')) { + // Handling mentions here + return renderMention(attrs, children) + } else { currentMentions = null - break - case 'img': // replace images with StillImage - return ['', [mentionsLinePadding, renderImage(opener)], ''] - case 'a': // replace mentions with MentionLink - if (!this.handleLinks) break - if (fullAttrs.class && fullAttrs.class.includes('mention')) { - // Handling mentions here - return renderMention(attrs, children) - } else { - currentMentions = null - break - } - case 'span': - if (this.handleLinks && fullAttrs.class && fullAttrs.class.includes('h-card')) { - return ['', children.map(processItem), ''] - } + } + } else if (Tag === 'span') { + if (this.handleLinks && fullAttrs.class && fullAttrs.class.includes('h-card')) { + return ['', children.map(processItem), ''] + } } if (children !== undefined) { @@ -257,7 +283,7 @@ export default { // DO NOT USE SLOTS they cause a re-render feedback loop here. // slots updated -> rerender -> emit -> update up the tree -> rerender -> ... // at least until vue3? - const result = <span class="RichContent"> + const result = <span class={['RichContent', this.faint ? '-faint' : '']}> { pass2 } </span> diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss index e5d353ac..118b6acf 100644 --- a/src/components/rich_content/rich_content.scss +++ b/src/components/rich_content/rich_content.scss @@ -1,10 +1,19 @@ -@import "../../variables"; - .RichContent { + font-family: var(--font); + + &.-faint { + /* stylelint-disable declaration-no-important */ + --text: var(--textFaint) !important; + --link: var(--linkFaint) !important; + --funtextGreentext: var(--funtextGreentextFaint) !important; + --funtextCyantext: var(--funtextCyantextFaint) !important; + /* stylelint-enable declaration-no-important */ + } + blockquote { margin: 0.2em 0 0.2em 0.2em; font-style: italic; - border-left: 0.2em solid var(--faint, $fallback--faint); + border-left: 0.2em solid var(--textFaint); padding-left: 1em; } @@ -17,7 +26,7 @@ kbd, var, pre { - font-family: var(--postCodeFont, monospace); + font-family: var(--monoFont); } p { @@ -65,4 +74,17 @@ vertical-align: middle; object-fit: contain; } + + .greentext { + color: var(--funtextGreentext); + } + + .cyantext { + color: var(--funtextCyantext); + } +} + +a .RichContent { + /* stylelint-disable-next-line declaration-no-important */ + color: var(--link) !important; } diff --git a/src/components/rich_content/rich_content.style.js b/src/components/rich_content/rich_content.style.js new file mode 100644 index 00000000..c8314000 --- /dev/null +++ b/src/components/rich_content/rich_content.style.js @@ -0,0 +1,18 @@ +export default { + name: 'RichContent', + selector: '.RichContent', + validInnerComponents: [ + 'Text', + 'FunText', + 'Link' + ], + defaultRules: [ + { + directives: { + '--font': 'generic | inherit', + '--monoFont': 'generic | monospace', + textNoCssColor: 'yes' + } + } + ] +} diff --git a/src/components/root.style.js b/src/components/root.style.js new file mode 100644 index 00000000..4bd735aa --- /dev/null +++ b/src/components/root.style.js @@ -0,0 +1,49 @@ +export default { + name: 'Root', + selector: ':root', + validInnerComponents: [ + 'Underlay', + 'Modals', + 'Popover', + 'TopBar', + 'Scrollbar', + 'ScrollbarElement', + 'MobileDrawer', + 'Alert', + 'Button' // mobile post button + ], + validInnerComponentsLite: [ + 'Underlay', + 'Scrollbar', + 'ScrollbarElement' + ], + defaultRules: [ + { + directives: { + // These are here just to establish order, + // themes should override those + '--bg': 'color | #121a24', + '--fg': 'color | #182230', + '--text': 'color | #b9b9ba', + '--link': 'color | #d8a070', + '--accent': 'color | #d8a070', + '--cRed': 'color | #FF0000', + '--cBlue': 'color | #0095ff', + '--cGreen': 'color | #0fa00f', + '--cOrange': 'color | #ffa500', + + // Fonts + '--font': 'generic | sans-serif', + '--monoFont': 'generic | monospace', + + // Fallback no-background-image color + // (also useful in some other places like scrollbars) + '--wallpaper': 'color | --bg, -2', + + // Selection colors + '--selectionBackground': 'color | --accent', + '--selectionText': 'color | $textColor(--accent, --text, no-preserve)' + } + } + ] +} diff --git a/src/components/scope_selector/scope_selector.js b/src/components/scope_selector/scope_selector.js index 74bf7284..52cda368 100644 --- a/src/components/scope_selector/scope_selector.js +++ b/src/components/scope_selector/scope_selector.js @@ -44,10 +44,10 @@ const ScopeSelector = { }, css () { return { - public: { selected: this.currentScope === 'public' }, - unlisted: { selected: this.currentScope === 'unlisted' }, - private: { selected: this.currentScope === 'private' }, - direct: { selected: this.currentScope === 'direct' } + public: { toggled: this.currentScope === 'public' }, + unlisted: { toggled: this.currentScope === 'unlisted' }, + private: { toggled: this.currentScope === 'private' }, + direct: { toggled: this.currentScope === 'direct' } } } }, diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue index d6e7265b..b90ae020 100644 --- a/src/components/scope_selector/scope_selector.vue +++ b/src/components/scope_selector/scope_selector.vue @@ -64,8 +64,6 @@ <script src="./scope_selector.js"></script> <style lang="scss"> -@import "../../variables"; - .ScopeSelector { .scope { display: inline-block; @@ -73,11 +71,6 @@ min-width: 1.3em; min-height: 1.3em; text-align: center; - - &.selected svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } } } </style> diff --git a/src/components/screen_reader_notice/screen_reader_notice.js b/src/components/screen_reader_notice/screen_reader_notice.js index 3b8eaf37..794b855a 100644 --- a/src/components/screen_reader_notice/screen_reader_notice.js +++ b/src/components/screen_reader_notice/screen_reader_notice.js @@ -2,7 +2,7 @@ const ScreenReaderNotice = { props: { ariaLive: { type: String, - defualt: 'assertive' + default: 'assertive' } }, data () { diff --git a/src/components/screen_reader_notice/screen_reader_notice.vue b/src/components/screen_reader_notice/screen_reader_notice.vue index 5098b94f..8384ae6b 100644 --- a/src/components/screen_reader_notice/screen_reader_notice.vue +++ b/src/components/screen_reader_notice/screen_reader_notice.vue @@ -1,6 +1,6 @@ <template> <div - class="screen-reader-text" + class="visible-for-screenreader-only" :aria-live="ariaLive" > {{ currentText }} @@ -8,14 +8,3 @@ </template> <script src="./screen_reader_notice.js"></script> - -<style lang="scss"> -.screen-reader-text { - display: block; - width: 1px; - height: 1px; - margin: -1px; - overflow: hidden; - visibility: visible; -} -</style> diff --git a/src/components/scrollbar.style.js b/src/components/scrollbar.style.js new file mode 100644 index 00000000..94e6135d --- /dev/null +++ b/src/components/scrollbar.style.js @@ -0,0 +1,11 @@ +export default { + name: 'Scrollbar', + selector: '::-webkit-scrollbar', + defaultRules: [ + { + directives: { + background: '--wallpaper' + } + } + ] +} diff --git a/src/components/scrollbar_element.style.js b/src/components/scrollbar_element.style.js new file mode 100644 index 00000000..da942ab2 --- /dev/null +++ b/src/components/scrollbar_element.style.js @@ -0,0 +1,101 @@ +const border = (top, shadow) => ({ + x: 0, + y: top ? 1 : -1, + blur: 0, + spread: 0, + color: shadow ? '#000000' : '#FFFFFF', + alpha: 0.2, + inset: true +}) + +const buttonInsetFakeBorders = [border(true, false), border(false, true)] +const inputInsetFakeBorders = [border(true, true), border(false, false)] +const buttonOuterShadow = { + x: 0, + y: 0, + blur: 2, + spread: 0, + color: '#000000', + alpha: 1 +} + +const hoverGlow = { + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '--text', + alpha: 1 +} + +export default { + name: 'ScrollbarElement', + selector: '::-webkit-scrollbar-button', + states: { + pressed: ':active', + hover: ':hover:not(:disabled)', + disabled: ':disabled' + }, + validInnerComponents: [ + 'Text' + ], + defaultRules: [ + { + directives: { + background: '--fg', + shadow: [buttonOuterShadow, ...buttonInsetFakeBorders], + roundness: 3 + } + }, + { + state: ['hover'], + directives: { + shadow: [hoverGlow, ...buttonInsetFakeBorders] + } + }, + { + state: ['pressed'], + directives: { + shadow: [buttonOuterShadow, ...inputInsetFakeBorders] + } + }, + { + state: ['hover', 'pressed'], + directives: { + shadow: [hoverGlow, ...inputInsetFakeBorders] + } + }, + { + state: ['toggled'], + directives: { + background: '--accent,-24.2', + shadow: [buttonOuterShadow, ...inputInsetFakeBorders] + } + }, + { + state: ['toggled', 'hover'], + directives: { + background: '--accent,-24.2', + shadow: [hoverGlow, ...inputInsetFakeBorders] + } + }, + { + state: ['disabled'], + directives: { + background: '$blend(--inheritedBackground, 0.25, --parent)', + shadow: [...buttonInsetFakeBorders] + } + }, + { + component: 'Text', + parent: { + component: 'Button', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + } + ] +} diff --git a/src/components/search/search.vue b/src/components/search/search.vue index 19b9c577..bd384ad3 100644 --- a/src/components/search/search.vue +++ b/src/components/search/search.vue @@ -1,15 +1,15 @@ <template> - <div class="panel panel-default"> + <div class="Search panel panel-default"> <div class="panel-heading"> <div class="title"> {{ $t('nav.search') }} </div> </div> - <div class="search-input-container"> + <div class="panel-body search-input-container"> <input ref="searchInput" v-model="searchTerm" - class="search-input" + class="input search-input" :placeholder="$t('nav.search')" @keyup.enter="newQuery(searchTerm)" > @@ -23,7 +23,7 @@ </div> <div v-if="loading && statusesOffset == 0" - class="text-center loading-icon" + class="panel-body text-center loading-icon" > <FAIcon icon="circle-notch" @@ -67,7 +67,7 @@ /> <button v-if="!loading && loaded && lastStatusFetchCount > 0" - class="more-statuses-button button-unstyled -link -fullwidth" + class="more-statuses-button button-unstyled -link" @click.prevent="search(searchTerm, 'statuses')" > <div class="new-status-notification text-center"> @@ -148,11 +148,8 @@ <script src="./search.js"></script> <style lang="scss"> -@import "../../variables"; - .search-result-heading { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--faint); padding: 0.75rem; text-align: center; } @@ -171,17 +168,7 @@ .search-result { box-sizing: border-box; border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); -} - -.search-result-footer { - border-width: 1px 0 0; - border-style: solid; - border-color: var(--border, $fallback--border); - padding: 10px; - background-color: $fallback--fg; - background-color: var(--panel, $fallback--fg); + border-color: var(--border); } .search-input-container { @@ -212,8 +199,7 @@ .hashtag { flex: 1 1 auto; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -226,14 +212,14 @@ line-height: 2.25rem; font-weight: 500; text-align: center; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } .more-statuses-button { height: 3.5em; line-height: 3.5em; + width: 100%; } </style> diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue index 9da2b272..d06b4e77 100644 --- a/src/components/search_bar/search_bar.vue +++ b/src/components/search_bar/search_bar.vue @@ -22,7 +22,7 @@ id="search-bar-input" ref="searchInput" v-model="searchTerm" - class="search-bar-input" + class="input search-bar-input" :placeholder="$t('nav.search')" type="text" @keyup.enter="find(searchTerm)" @@ -60,8 +60,6 @@ <script src="./search_bar.js"></script> <style lang="scss"> -@import "../../variables"; - .SearchBar { display: inline-flex; align-items: baseline; @@ -86,8 +84,7 @@ } .cancel-icon { - color: $fallback--text; - color: var(--btnTopBarText, $fallback--text); + color: var(--text); } } diff --git a/src/components/select/select.vue b/src/components/select/select.vue index 1797afc8..32832126 100644 --- a/src/components/select/select.vue +++ b/src/components/select/select.vue @@ -22,8 +22,6 @@ <script src="./select.js"> </script> <style lang="scss"> -@import "../../variables"; - /* TODO fix order of styles */ label.Select { padding: 0; @@ -32,12 +30,10 @@ label.Select { appearance: none; background: transparent; border: none; - color: $fallback--text; - color: var(--inputText, --text, $fallback--text); + color: var(--text); margin: 0; padding: 0 2em 0 0.2em; - font-family: sans-serif; - font-family: var(--inputFont, sans-serif); + font-family: var(--font); font-size: 1em; width: 100%; z-index: 1; @@ -52,8 +48,7 @@ label.Select { right: 5px; height: 100%; width: 0.875em; - color: $fallback--text; - color: var(--inputText, $fallback--text); + font-family: var(--font); line-height: 2; z-index: 0; pointer-events: none; diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue index 14910fc5..3d3a5ff0 100644 --- a/src/components/selectable_list/selectable_list.vue +++ b/src/components/selectable_list/selectable_list.vue @@ -23,16 +23,19 @@ <List :items="items" :get-key="getKey" + :get-class="item => isSelected(item) ? '-active' : ''" > <template #item="{item}"> <div class="selectable-list-item-inner" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }" + @click.stop="toggle(!isSelected(item), item)" > <div class="selectable-list-checkbox-wrapper"> <Checkbox :model-value="isSelected(item)" @update:model-value="checked => toggle(checked, item)" + @click.stop /> </div> <slot @@ -51,9 +54,11 @@ <script src="./selectable_list.js"></script> <style lang="scss"> -@import "../../variables"; - .selectable-list { + --__line-height: 1.5em; + --__horizontal-gap: 0.75em; + --__vertical-gap: 0.5em; + &-item-inner { display: flex; align-items: center; @@ -63,24 +68,12 @@ } } - &-item-selected-inner { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: var(--selectedMenuText, $fallback--text); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - } - &-header { display: flex; align-items: center; - padding: 0.6em 0; - border-bottom: 2px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + padding: var(--__vertical-gap) var(--__horizontal-gap); + border-bottom: 1px solid; + border-bottom-color: var(--border); &-actions { flex: 1; @@ -88,7 +81,7 @@ } &-checkbox-wrapper { - padding: 0 10px; + padding-right: var(--__horizontal-gap); flex: none; } } diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.js b/src/components/settings_modal/admin_tabs/emoji_tab.js new file mode 100644 index 00000000..58e1468f --- /dev/null +++ b/src/components/settings_modal/admin_tabs/emoji_tab.js @@ -0,0 +1,257 @@ +import { clone, assign } from 'lodash' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' +import StringSetting from '../helpers/string_setting.vue' +import Checkbox from 'components/checkbox/checkbox.vue' +import StillImage from 'components/still-image/still-image.vue' +import Select from 'components/select/select.vue' +import Popover from 'components/popover/popover.vue' +import ConfirmModal from 'components/confirm_modal/confirm_modal.vue' +import ModifiedIndicator from '../helpers/modified_indicator.vue' +import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue' + +const EmojiTab = { + components: { + TabSwitcher, + StringSetting, + Checkbox, + StillImage, + Select, + Popover, + ConfirmModal, + ModifiedIndicator, + EmojiEditingPopover + }, + + data () { + return { + knownLocalPacks: { }, + knownRemotePacks: { }, + editedMetadata: { }, + packName: '', + newPackName: '', + deleteModalVisible: false, + remotePackInstance: '', + remotePackDownloadAs: '' + } + }, + + provide () { + return { emojiAddr: this.emojiAddr } + }, + + computed: { + pack () { + return this.packName !== '' ? this.knownPacks[this.packName] : undefined + }, + packMeta () { + if (this.editedMetadata[this.packName] === undefined) { + this.editedMetadata[this.packName] = clone(this.pack.pack) + } + + return this.editedMetadata[this.packName] + }, + knownPacks () { + // Copy the object itself but not the children, so they are still passed by reference and modified + const result = clone(this.knownLocalPacks) + for (const instName in this.knownRemotePacks) { + for (const instPack in this.knownRemotePacks[instName]) { + result[`${instPack}@${instName}`] = this.knownRemotePacks[instName][instPack] + } + } + + return result + }, + downloadWillReplaceLocal () { + return (this.remotePackDownloadAs.trim() === '' && this.pack.remote && this.pack.remote.baseName in this.knownLocalPacks) || + (this.remotePackDownloadAs in this.knownLocalPacks) + } + }, + + methods: { + reloadEmoji () { + this.$store.state.api.backendInteractor.reloadEmoji() + }, + importFromFS () { + this.$store.state.api.backendInteractor.importEmojiFromFS() + }, + emojiAddr (name) { + if (this.pack.remote !== undefined) { + // Remote pack + return `${this.pack.remote.instance}/emoji/${encodeURIComponent(this.pack.remote.baseName)}/${name}` + } else { + return `${this.$store.state.instance.server}/emoji/${encodeURIComponent(this.packName)}/${name}` + } + }, + + createEmojiPack () { + this.$store.state.api.backendInteractor.createEmojiPack( + { name: this.newPackName } + ).then(resp => resp.json()).then(resp => { + if (resp === 'ok') { + return this.refreshPackList() + } else { + this.displayError(resp.error) + return Promise.reject(resp) + } + }).then(done => { + this.$refs.createPackPopover.hidePopover() + + this.packName = this.newPackName + this.newPackName = '' + }) + }, + deleteEmojiPack () { + this.$store.state.api.backendInteractor.deleteEmojiPack( + { name: this.packName } + ).then(resp => resp.json()).then(resp => { + if (resp === 'ok') { + return this.refreshPackList() + } else { + this.displayError(resp.error) + return Promise.reject(resp) + } + }).then(done => { + delete this.editedMetadata[this.packName] + + this.deleteModalVisible = false + this.packName = '' + }) + }, + + metaEdited (prop) { + if (!this.pack) return + + const def = this.pack.pack[prop] || '' + const edited = this.packMeta[prop] || '' + return edited !== def + }, + savePackMetadata () { + this.$store.state.api.backendInteractor.saveEmojiPackMetadata({ name: this.packName, newData: this.packMeta }).then( + resp => resp.json() + ).then(resp => { + if (resp.error !== undefined) { + this.displayError(resp.error) + return + } + + // Update actual pack data + this.pack.pack = resp + // Delete edited pack data, should auto-update itself + delete this.editedMetadata[this.packName] + }) + }, + + updatePackFiles (newFiles) { + this.pack.files = newFiles + this.sortPackFiles(this.packName) + }, + + loadPacksPaginated (listFunction) { + const pageSize = 25 + const allPacks = {} + + return listFunction({ instance: this.remotePackInstance, page: 1, pageSize: 0 }) + .then(data => data.json()) + .then(data => { + if (data.error !== undefined) { return Promise.reject(data.error) } + + let resultingPromise = Promise.resolve({}) + for (let i = 0; i < Math.ceil(data.count / pageSize); i++) { + resultingPromise = resultingPromise.then(() => listFunction({ instance: this.remotePackInstance, page: i, pageSize }) + ).then(data => data.json()).then(pageData => { + if (pageData.error !== undefined) { return Promise.reject(pageData.error) } + + assign(allPacks, pageData.packs) + }) + } + + return resultingPromise + }) + .then(finished => allPacks) + .catch(data => { + this.displayError(data) + }) + }, + + refreshPackList () { + this.loadPacksPaginated(this.$store.state.api.backendInteractor.listEmojiPacks) + .then(allPacks => { + this.knownLocalPacks = allPacks + for (const name of Object.keys(this.knownLocalPacks)) { + this.sortPackFiles(name) + } + }) + }, + listRemotePacks () { + this.loadPacksPaginated(this.$store.state.api.backendInteractor.listRemoteEmojiPacks) + .then(allPacks => { + let inst = this.remotePackInstance + if (!inst.startsWith('http')) { inst = 'https://' + inst } + const instUrl = new URL(inst) + inst = instUrl.host + + for (const packName in allPacks) { + allPacks[packName].remote = { + baseName: packName, + instance: instUrl.origin + } + } + + this.knownRemotePacks[inst] = allPacks + for (const pack in this.knownRemotePacks[inst]) { + this.sortPackFiles(`${pack}@${inst}`) + } + + this.$refs.remotePackPopover.hidePopover() + }) + .catch(data => { + this.displayError(data) + }) + }, + downloadRemotePack () { + if (this.remotePackDownloadAs.trim() === '') { + this.remotePackDownloadAs = this.pack.remote.baseName + } + + this.$store.state.api.backendInteractor.downloadRemoteEmojiPack({ + instance: this.pack.remote.instance, packName: this.pack.remote.baseName, as: this.remotePackDownloadAs + }) + .then(data => data.json()) + .then(resp => { + if (resp === 'ok') { + this.$refs.dlPackPopover.hidePopover() + + return this.refreshPackList() + } else { + this.displayError(resp.error) + return Promise.reject(resp) + } + }).then(done => { + this.packName = this.remotePackDownloadAs + this.remotePackDownloadAs = '' + }) + }, + displayError (msg) { + this.$store.dispatch('pushGlobalNotice', { + messageKey: 'admin_dash.emoji.error', + messageArgs: [msg], + level: 'error' + }) + }, + sortPackFiles (nameOfPack) { + // Sort by key + const sorted = Object.keys(this.knownPacks[nameOfPack].files).sort().reduce((acc, key) => { + if (key.length === 0) return acc + acc[key] = this.knownPacks[nameOfPack].files[key] + return acc + }, {}) + this.knownPacks[nameOfPack].files = sorted + } + }, + + mounted () { + this.refreshPackList() + } +} + +export default EmojiTab diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.scss b/src/components/settings_modal/admin_tabs/emoji_tab.scss new file mode 100644 index 00000000..3e77e019 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/emoji_tab.scss @@ -0,0 +1,59 @@ +.emoji-tab { + .btn-group .btn:not(:first-child) { + margin-left: 0.5em; + } + + .pack-info-wrapper { + margin-top: 1em; + } + + .emoji-info-input { + width: 100%; + } + + .emoji-data-input { + width: 40%; + margin-left: 0.5em; + margin-right: 0.5em; + } + + .emoji { + width: 32px; + height: 32px; + } + + .emoji-unsaved { + box-shadow: 0 3px 5px var(--cBlue); + } + + .emoji-list { + display: flex; + flex-wrap: wrap; + gap: 1em 1em; + } +} + +.emoji-tab-popover-button:not(:first-child) { + margin-left: 0.5em; +} + +.emoji-tab-popover-input { + margin-bottom: 0.5em; + + label { + display: block; + margin-bottom: 0.5em; + } + + input { + width: 20em; + } + + .emoji-tab-popover-file { + padding-top: 3px; + } + + .warning { + color: var(--cOrange); + } +} diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.vue b/src/components/settings_modal/admin_tabs/emoji_tab.vue new file mode 100644 index 00000000..5742d2ce --- /dev/null +++ b/src/components/settings_modal/admin_tabs/emoji_tab.vue @@ -0,0 +1,358 @@ +<template> + <div + class="emoji-tab" + :label="$t('admin_dash.tabs.emoji')" + > + <div class="setting-item"> + <h2>{{ $t('admin_dash.tabs.emoji') }}</h2> + + <ul class="setting-list"> + <h3>{{ $t('admin_dash.emoji.global_actions') }}</h3> + + <li class="btn-group setting-item"> + <button + class="button button-default btn" + type="button" + @click="reloadEmoji" + > + {{ $t('admin_dash.emoji.reload') }} + </button> + <button + class="button button-default btn" + type="button" + @click="importFromFS" + > + {{ $t('admin_dash.emoji.importFS') }} + </button> + </li> + + <li class="btn-group setting-item"> + <button + class="button button-default btn" + type="button" + @click="$refs.remotePackPopover.showPopover" + > + {{ $t('admin_dash.emoji.remote_packs') }} + + <Popover + ref="remotePackPopover" + popover-class="emoji-tab-edit-popover popover-default" + trigger="click" + placement="bottom" + bound-to-selector=".emoji-tab" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + > + <template #content> + <div class="emoji-tab-popover-input"> + <h3>{{ $t('admin_dash.emoji.remote_pack_instance') }}</h3> + <input + v-model="remotePackInstance" + class="input" + :placeholder="$t('admin_dash.emoji.remote_pack_instance')" + > + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="listRemotePacks" + > + {{ $t('admin_dash.emoji.do_list') }} + </button> + </div> + </template> + </Popover> + </button> + </li> + + <h3>{{ $t('admin_dash.emoji.emoji_packs') }}</h3> + + <li> + <h4>{{ $t('admin_dash.emoji.edit_pack') }}</h4> + + <Select + v-model="packName" + class="form-control" + > + <option + value="" + disabled + hidden + > + {{ $t('admin_dash.emoji.emoji_pack') }} + </option> + <option + v-for="(pack, listPackName) in knownPacks" + :key="listPackName" + :label="listPackName" + > + {{ listPackName }} + </option> + </Select> + + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="$refs.createPackPopover.showPopover" + > + {{ $t('admin_dash.emoji.create_pack') }} + </button> + <Popover + ref="createPackPopover" + popover-class="emoji-tab-edit-popover popover-default" + trigger="click" + placement="bottom" + bound-to-selector=".emoji-tab" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + > + <template #content> + <div class="emoji-tab-popover-input"> + <h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3> + <input + v-model="newPackName" + :placeholder="$t('admin_dash.emoji.new_pack_name')" + class="input" + > + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="createEmojiPack" + > + {{ $t('admin_dash.emoji.create') }} + </button> + </div> + </template> + </Popover> + </li> + </ul> + + <div v-if="pack"> + <div class="pack-info-wrapper"> + <ul class="setting-list"> + <li> + <label> + {{ $t('admin_dash.emoji.description') }} + <ModifiedIndicator + :changed="metaEdited('description')" + message-key="admin_dash.emoji.metadata_changed" + /> + + <textarea + v-model="packMeta.description" + :disabled="pack.remote !== undefined" + class="bio resize-height input" + /> + </label> + </li> + <li> + <label> + {{ $t('admin_dash.emoji.homepage') }} + <ModifiedIndicator + :changed="metaEdited('homepage')" + message-key="admin_dash.emoji.metadata_changed" + /> + + <input + v-model="packMeta.homepage" + class="emoji-info-input input" + :disabled="pack.remote !== undefined" + > + </label> + </li> + <li> + <label> + {{ $t('admin_dash.emoji.fallback_src') }} + <ModifiedIndicator + :changed="metaEdited('fallback-src')" + message-key="admin_dash.emoji.metadata_changed" + /> + + <input + v-model="packMeta['fallback-src']" + class="emoji-info-input input" + :disabled="pack.remote !== undefined" + > + </label> + </li> + <li> + <label> + {{ $t('admin_dash.emoji.fallback_sha256') }} + + <input + v-model="packMeta['fallback-src-sha256']" + :disabled="true" + class="emoji-info-input input" + > + </label> + </li> + <li> + <Checkbox + v-model="packMeta['share-files']" + :disabled="pack.remote !== undefined" + > + {{ $t('admin_dash.emoji.share') }} + </Checkbox> + + <ModifiedIndicator + :changed="metaEdited('share-files')" + message-key="admin_dash.emoji.metadata_changed" + /> + </li> + <li class="btn-group"> + <button + v-if="pack.remote === undefined" + class="button button-default btn" + type="button" + @click="savePackMetadata" + > + {{ $t('admin_dash.emoji.save_meta') }} + </button> + <button + v-if="pack.remote === undefined" + class="button button-default btn" + type="button" + @click="savePackMetadata" + > + {{ $t('admin_dash.emoji.revert_meta') }} + </button> + + <button + v-if="pack.remote === undefined" + class="button button-default btn" + type="button" + @click="deleteModalVisible = true" + > + {{ $t('admin_dash.emoji.delete_pack') }} + + <ConfirmModal + v-if="deleteModalVisible" + :title="$t('admin_dash.emoji.delete_title')" + :cancel-text="$t('status.delete_confirm_cancel_button')" + :confirm-text="$t('status.delete_confirm_accept_button')" + @cancelled="deleteModalVisible = false" + @accepted="deleteEmojiPack" + > + {{ $t('admin_dash.emoji.delete_confirm', [packName]) }} + </ConfirmModal> + </button> + + <button + v-if="pack.remote !== undefined" + class="button button-default btn" + type="button" + @click="$refs.dlPackPopover.showPopover" + > + {{ $t('admin_dash.emoji.download_pack') }} + + <Popover + ref="dlPackPopover" + trigger="click" + placement="bottom" + bound-to-selector=".emoji-tab" + popover-class="emoji-tab-edit-popover popover-default" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + > + <template #content> + <h3>{{ $t('admin_dash.emoji.downloading_pack', [packName]) }}</h3> + <div> + <div> + <div class="emoji-tab-popover-input"> + <label> + {{ $t('admin_dash.emoji.download_as_name') }} + <input + v-model="remotePackDownloadAs" + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.download_as_name_full')" + > + </label> + + <div + v-if="downloadWillReplaceLocal" + class="warning" + > + <em>{{ $t('admin_dash.emoji.replace_warning') }}</em> + </div> + </div> + + <button + class="button button-default btn" + type="button" + @click="downloadRemotePack" + > + {{ $t('admin_dash.emoji.download') }} + </button> + </div> + </div> + </template> + </Popover> + </button> + </li> + </ul> + </div> + + <ul class="setting-list"> + <h4> + {{ $t('admin_dash.emoji.files') }} + + <ModifiedIndicator + v-if="pack" + :changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)" + message-key="admin_dash.emoji.emoji_changed" + /> + </h4> + + <div + v-if="pack" + class="emoji-list" + > + <EmojiEditingPopover + v-if="pack.remote === undefined" + placement="bottom" + new-upload + :title="$t('admin_dash.emoji.adding_new')" + :pack-name="packName" + @updatePackFiles="updatePackFiles" + @displayError="displayError" + > + <template #trigger> + <FAIcon + icon="plus" + size="2x" + :title="$t('admin_dash.emoji.add_file')" + /> + </template> + </EmojiEditingPopover> + + <EmojiEditingPopover + v-for="(file, shortcode) in pack.files" + ref="emojiPopovers" + :key="shortcode" + placement="top" + :title="$t('admin_dash.emoji.editing', [shortcode])" + :disabled="pack.remote !== undefined" + :shortcode="shortcode" + :file="file" + :pack-name="packName" + @updatePackFiles="updatePackFiles" + @displayError="displayError" + > + <template #trigger> + <StillImage + class="emoji" + :src="emojiAddr(file)" + :title="`:${shortcode}:`" + :alt="`:${shortcode}:`" + /> + </template> + </EmojiEditingPopover> + </div> + </ul> + </div> + </div> + </div> +</template> + +<script src="./emoji_tab.js"></script> + +<style lang="scss" src="./emoji_tab.scss"></style> diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js new file mode 100644 index 00000000..f57310ee --- /dev/null +++ b/src/components/settings_modal/admin_tabs/frontends_tab.js @@ -0,0 +1,113 @@ +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' +import IntegerSetting from '../helpers/integer_setting.vue' +import StringSetting from '../helpers/string_setting.vue' +import GroupSetting from '../helpers/group_setting.vue' +import Popover from 'src/components/popover/popover.vue' +import PanelLoading from 'src/components/panel_loading/panel_loading.vue' + +import SharedComputedObject from '../helpers/shared_computed_object.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faGlobe +) + +const FrontendsTab = { + provide () { + return { + defaultDraftMode: true, + defaultSource: 'admin' + } + }, + data () { + return { + working: false + } + }, + components: { + BooleanSetting, + ChoiceSetting, + IntegerSetting, + StringSetting, + GroupSetting, + PanelLoading, + Popover + }, + created () { + if (this.user.rights.admin) { + this.$store.dispatch('loadFrontendsStuff') + } + }, + computed: { + frontends () { + return this.$store.state.adminSettings.frontends + }, + ...SharedComputedObject() + }, + methods: { + canInstall (frontend) { + const fe = this.frontends.find(f => f.name === frontend.name) + if (!fe) return false + return fe.refs.includes(frontend.ref) + }, + getSuggestedRef (frontend) { + if (this.adminDraft) { + const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary'] + if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) { + return defaultFe.ref + } else { + return frontend.refs[0] + } + } else { + return frontend.refs[0] + } + }, + update (frontend, suggestRef) { + const ref = suggestRef || this.getSuggestedRef(frontend) + const { name } = frontend + const payload = { name, ref } + + this.working = true + this.$store.state.api.backendInteractor.installFrontend({ payload }) + .finally(() => { + this.working = false + }) + .then(async (response) => { + this.$store.dispatch('loadFrontendsStuff') + if (response.error) { + const reason = await response.error.json() + this.$store.dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'admin_dash.frontend.failure_installing_frontend', + messageArgs: { + version: name + '/' + ref, + reason: reason.error + }, + timeout: 5000 + }) + } else { + this.$store.dispatch('pushGlobalNotice', { + level: 'success', + messageKey: 'admin_dash.frontend.success_installing_frontend', + messageArgs: { + version: name + '/' + ref + }, + timeout: 2000 + }) + } + }) + }, + setDefault (frontend, suggestRef) { + const ref = suggestRef || this.getSuggestedRef(frontend) + const { name } = frontend + + this.$store.commit('updateAdminDraft', { path: [':pleroma', ':frontends', ':primary'], value: { name, ref } }) + } + } +} + +export default FrontendsTab diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.scss b/src/components/settings_modal/admin_tabs/frontends_tab.scss new file mode 100644 index 00000000..420d20b3 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/frontends_tab.scss @@ -0,0 +1,29 @@ +.frontends-tab { + .cards-list { + padding: 0; + } + + .relative { + position: relative; + } + + .overlay { + position: absolute; + background: var(--bg); + // fix buttons showing through + z-index: 2; + opacity: 0.9; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + dd { + text-overflow: ellipsis; + word-wrap: nowrap; + white-space: nowrap; + overflow-x: hidden; + max-width: 10em; + } +} diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue new file mode 100644 index 00000000..8fb3d399 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/frontends_tab.vue @@ -0,0 +1,209 @@ +<template> + <div + class="frontends-tab" + :label="$t('admin_dash.tabs.frontends')" + > + <div class="setting-item"> + <h2>{{ $t('admin_dash.tabs.frontends') }}</h2> + <p>{{ $t('admin_dash.frontend.wip_notice') }}</p> + <ul + v-if="adminDraft" + class="setting-list" + > + <li> + <h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3> + <p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p> + <ul class="setting-list"> + <li> + <StringSetting path=":pleroma.:frontends.:primary.name" /> + </li> + <li> + <StringSetting path=":pleroma.:frontends.:primary.ref" /> + </li> + <li> + <GroupSetting path=":pleroma.:frontends.:primary" /> + </li> + </ul> + </li> + </ul> + <div + v-else + class="setting-list" + > + {{ $t('admin_dash.frontend.default_frontend_unavail') }} + </div> + + <div class="setting-list relative"> + <PanelLoading + v-if="working" + class="overlay" + /> + <h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3> + <ul class="cards-list"> + <li + v-for="frontend in frontends" + :key="frontend.name" + > + <strong>{{ frontend.name }}</strong> + {{ ' ' }} + <span v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name"> + <i18n-t + v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]" + keypath="admin_dash.frontend.is_default" + /> + <i18n-t + v-else + keypath="admin_dash.frontend.is_default_custom" + > + <template #version> + <code>{{ adminDraft && adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code> + </template> + </i18n-t> + </span> + <dl> + <dt>{{ $t('admin_dash.frontend.repository') }}</dt> + <dd> + <a + :href="frontend.git" + target="_blank" + >{{ frontend.git }}</a> + </dd> + <template v-if="expertLevel"> + <dt>{{ $t('admin_dash.frontend.versions') }}</dt> + <dd + v-for="ref in frontend.refs" + :key="ref" + > + <code>{{ ref }}</code> + </dd> + </template> + <dt v-if="expertLevel"> + {{ $t('admin_dash.frontend.build_url') }} + </dt> + <dd v-if="expertLevel"> + <a + :href="frontend.build_url" + target="_blank" + >{{ frontend.build_url }}</a> + </dd> + </dl> + <div> + <span class="btn-group"> + <button + class="button button-default btn" + type="button" + @click="update(frontend)" + > + {{ + frontend.installed + ? $t('admin_dash.frontend.reinstall') + : $t('admin_dash.frontend.install') + }} + <code> + {{ + getSuggestedRef(frontend) + }} + </code> + </button> + <Popover + v-if="frontend.refs.length > 1" + trigger="click" + class="button-dropdown" + placement="bottom" + > + <template #content="{close}"> + <div class="dropdown-menu"> + <button + v-for="ref in frontend.refs" + :key="ref" + class="menu-item dropdown-item" + @click.prevent="update(frontend, ref)" + @click="close" + > + <i18n-t keypath="admin_dash.frontend.install_version"> + <template #version> + <code>{{ ref }}</code> + </template> + </i18n-t> + </button> + </div> + </template> + <template #trigger> + <button + class="button button-default btn dropdown-button" + type="button" + :title="$t('admin_dash.frontend.more_install_options')" + > + <FAIcon icon="chevron-down" /> + </button> + </template> + </Popover> + </span> + <span + v-if="frontend.installed && frontend.name !== 'admin-fe'" + class="btn-group" + > + <button + class="button button-default btn" + type="button" + :disabled=" + !adminDraft || adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name && + adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0] + " + @click="setDefault(frontend)" + > + {{ + $t('admin_dash.frontend.set_default') + }} + <code> + {{ + getSuggestedRef(frontend) + }} + </code> + </button> + {{ ' ' }} + <Popover + v-if="frontend.refs.length > 1" + trigger="click" + class="button-dropdown" + placement="bottom" + > + <template #content="{close}"> + <div class="dropdown-menu"> + <button + v-for="ref in frontend.installedRefs || frontend.refs" + :key="ref" + class="menu-item dropdown-item" + @click.prevent="setDefault(frontend, ref)" + @click="close" + > + <i18n-t keypath="admin_dash.frontend.set_default_version"> + <template #version> + <code>{{ ref }}</code> + </template> + </i18n-t> + </button> + </div> + </template> + <template #trigger> + <button + class="button button-default btn dropdown-button" + type="button" + :title="$t('admin_dash.frontend.more_default_options')" + > + <FAIcon icon="chevron-down" /> + </button> + </template> + </Popover> + </span> + </div> + </li> + </ul> + </div> + </div> + </div> +</template> + +<script src="./frontends_tab.js"></script> + +<style lang="scss" src="./frontends_tab.scss"></style> diff --git a/src/components/settings_modal/admin_tabs/instance_tab.js b/src/components/settings_modal/admin_tabs/instance_tab.js new file mode 100644 index 00000000..b07bafe8 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/instance_tab.js @@ -0,0 +1,38 @@ +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' +import IntegerSetting from '../helpers/integer_setting.vue' +import StringSetting from '../helpers/string_setting.vue' +import GroupSetting from '../helpers/group_setting.vue' +import AttachmentSetting from '../helpers/attachment_setting.vue' + +import SharedComputedObject from '../helpers/shared_computed_object.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faGlobe +) + +const InstanceTab = { + provide () { + return { + defaultDraftMode: true, + defaultSource: 'admin' + } + }, + components: { + BooleanSetting, + ChoiceSetting, + IntegerSetting, + StringSetting, + AttachmentSetting, + GroupSetting + }, + computed: { + ...SharedComputedObject() + } +} + +export default InstanceTab diff --git a/src/components/settings_modal/admin_tabs/instance_tab.vue b/src/components/settings_modal/admin_tabs/instance_tab.vue new file mode 100644 index 00000000..32e8df25 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/instance_tab.vue @@ -0,0 +1,206 @@ +<template> + <div :label="$t('admin_dash.tabs.instance')"> + <div class="setting-item"> + <h2>{{ $t('admin_dash.instance.instance') }}</h2> + <ul class="setting-list"> + <li> + <StringSetting path=":pleroma.:instance.:name" /> + </li> + <!-- See https://git.pleroma.social/pleroma/pleroma/-/merge_requests/3963 --> + <li v-if="adminDraft[':pleroma'][':instance'][':favicon'] !== undefined"> + <AttachmentSetting + compact + path=":pleroma.:instance.:favicon" + /> + </li> + <li> + <StringSetting path=":pleroma.:instance.:email" /> + </li> + <li> + <StringSetting path=":pleroma.:instance.:description" /> + </li> + <li> + <StringSetting path=":pleroma.:instance.:short_description" /> + </li> + <li> + <AttachmentSetting + compact + path=":pleroma.:instance.:instance_thumbnail" + /> + </li> + <li> + <AttachmentSetting path=":pleroma.:instance.:background_image" /> + </li> + </ul> + </div> + <div class="setting-item"> + <h2>{{ $t('admin_dash.instance.registrations') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting path=":pleroma.:instance.:registrations_open" /> + <ul class="setting-list suboptions"> + <li> + <BooleanSetting + path=":pleroma.:instance.:invites_enabled" + parent-path=":pleroma.:instance.:registrations_open" + parent-invert + /> + </li> + </ul> + </li> + <li> + <BooleanSetting path=":pleroma.:instance.:birthday_required" /> + <ul class="setting-list suboptions"> + <li> + <IntegerSetting + path=":pleroma.:instance.:birthday_min_age" + parent-path=":pleroma.:instance.:birthday_required" + /> + </li> + </ul> + </li> + <li> + <BooleanSetting path=":pleroma.:instance.:account_activation_required" /> + </li> + <li> + <BooleanSetting path=":pleroma.:instance.:account_approval_required" /> + </li> + <li> + <h3>{{ $t('admin_dash.instance.captcha_header') }}</h3> + <ul class="setting-list"> + <li> + <BooleanSetting :path="[':pleroma', 'Pleroma.Captcha', ':enabled']" /> + <ul class="setting-list suboptions"> + <li> + <ChoiceSetting + :path="[':pleroma', 'Pleroma.Captcha', ':method']" + :parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']" + :option-label-map="{ + 'Pleroma.Captcha.Native': $t('admin_dash.captcha.native'), + 'Pleroma.Captcha.Kocaptcha': $t('admin_dash.captcha.kocaptcha') + }" + /> + <IntegerSetting + :path="[':pleroma', 'Pleroma.Captcha', ':seconds_valid']" + :parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']" + /> + </li> + <li + v-if="adminDraft[':pleroma']['Pleroma.Captcha'][':enabled'] && adminDraft[':pleroma']['Pleroma.Captcha'][':method'] === 'Pleroma.Captcha.Kocaptcha'" + > + <h4>{{ $t('admin_dash.instance.kocaptcha') }}</h4> + <ul class="setting-list"> + <li> + <StringSetting :path="[':pleroma', 'Pleroma.Captcha.Kocaptcha', ':endpoint']" /> + </li> + </ul> + </li> + </ul> + </li> + </ul> + </li> + </ul> + </div> + <div class="setting-item"> + <h2>{{ $t('admin_dash.instance.access') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting + override-backend-description + override-backend-description-label + path=":pleroma.:instance.:public" + /> + </li> + <li> + <ChoiceSetting + override-backend-description + override-backend-description-label + path=":pleroma.:instance.:limit_to_local_content" + /> + </li> + <li v-if="expertLevel"> + <h3>{{ $t('admin_dash.instance.restrict.header') }}</h3> + <p> + {{ $t('admin_dash.instance.restrict.description') }} + </p> + <ul class="setting-list"> + <li> + <h4>{{ $t('admin_dash.instance.restrict.timelines') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:timelines.:local" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:timelines.:federated" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <GroupSetting path=":pleroma.:restrict_unauthenticated.:timelines" /> + </li> + </ul> + </li> + <li> + <h4>{{ $t('admin_dash.instance.restrict.profiles') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:profiles.:local" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:profiles.:remote" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <GroupSetting path=":pleroma.:restrict_unauthenticated.:profiles" /> + </li> + </ul> + </li> + <li> + <h4>{{ $t('admin_dash.instance.restrict.activities') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:activities.:local" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <BooleanSetting + path=":pleroma.:restrict_unauthenticated.:activities.:remote" + indeterminate-state=":if_instance_is_private" + swap-description-and-label + hide-description + /> + </li> + <li> + <GroupSetting path=":pleroma.:restrict_unauthenticated.:activities" /> + </li> + </ul> + </li> + </ul> + </li> + </ul> + </div> + </div> +</template> + +<script src="./instance_tab.js"></script> diff --git a/src/components/settings_modal/admin_tabs/limits_tab.js b/src/components/settings_modal/admin_tabs/limits_tab.js new file mode 100644 index 00000000..684739c3 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/limits_tab.js @@ -0,0 +1,29 @@ +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' +import IntegerSetting from '../helpers/integer_setting.vue' +import StringSetting from '../helpers/string_setting.vue' + +import SharedComputedObject from '../helpers/shared_computed_object.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faGlobe +) + +const LimitsTab = { + data () {}, + components: { + BooleanSetting, + ChoiceSetting, + IntegerSetting, + StringSetting + }, + computed: { + ...SharedComputedObject() + } +} + +export default LimitsTab diff --git a/src/components/settings_modal/admin_tabs/limits_tab.vue b/src/components/settings_modal/admin_tabs/limits_tab.vue new file mode 100644 index 00000000..ef4b9271 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/limits_tab.vue @@ -0,0 +1,136 @@ +<template> + <div :label="$t('admin_dash.tabs.limits')"> + <div class="setting-item"> + <h2>{{ $t('admin_dash.limits.arbitrary_limits') }}</h2> + <ul class="setting-list"> + <li> + <h3>{{ $t('admin_dash.limits.posts') }}</h3> + <ul class="setting-list"> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:limit" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:remote_limit" + expert="1" + draft-mode + /> + </li> + </ul> + </li> + <li> + <h3>{{ $t('admin_dash.limits.uploads') }}</h3> + <ul class="setting-list"> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:description_limit" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:upload_limit" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_media_attachments" + draft-mode + /> + </li> + </ul> + </li> + <li> + <h3>{{ $t('admin_dash.limits.users') }}</h3> + <ul class="setting-list"> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_pinned_statuses" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:user_bio_length" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:user_name_length" + draft-mode + /> + </li> + <li> + <h4>{{ $t('admin_dash.limits.profile_fields') }}</h4> + <ul class="setting-list"> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_account_fields" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:max_remote_account_fields" + draft-mode + expert="1" + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:account_field_name_length" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:account_field_value_length" + draft-mode + /> + </li> + </ul> + </li> + <li> + <h4>{{ $t('admin_dash.limits.user_uploads') }}</h4> + <ul class="setting-list"> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:avatar_upload_limit" + draft-mode + /> + </li> + <li> + <IntegerSetting + source="admin" + path=":pleroma.:instance.:banner_upload_limit" + draft-mode + /> + </li> + </ul> + </li> + </ul> + </li> + </ul> + </div> + </div> +</template> + +<script src="./limits_tab.js"></script> diff --git a/src/components/settings_modal/helpers/attachment_setting.js b/src/components/settings_modal/helpers/attachment_setting.js new file mode 100644 index 00000000..c4c04b2b --- /dev/null +++ b/src/components/settings_modal/helpers/attachment_setting.js @@ -0,0 +1,44 @@ +import Setting from './setting.js' +import { fileTypeExt } from 'src/services/file_type/file_type.service.js' +import MediaUpload from 'src/components/media_upload/media_upload.vue' +import Attachment from 'src/components/attachment/attachment.vue' + +export default { + ...Setting, + props: { + ...Setting.props, + compact: Boolean, + acceptTypes: { + type: String, + required: false, + default: 'image/*' + } + }, + components: { + ...Setting.components, + MediaUpload, + Attachment + }, + computed: { + ...Setting.computed, + attachment () { + const path = this.realDraftMode ? this.draft : this.state + // The "server" part is primarily for local dev, but could be useful for alt-domain or multiuser usage. + const url = path.includes('://') ? path : this.$store.state.instance.server + path + return { + mimetype: fileTypeExt(url), + url + } + } + }, + methods: { + ...Setting.methods, + setMediaFile (fileInfo) { + if (this.realDraftMode) { + this.draft = fileInfo.url + } else { + this.configSink(this.path, fileInfo.url) + } + } + } +} diff --git a/src/components/settings_modal/helpers/attachment_setting.vue b/src/components/settings_modal/helpers/attachment_setting.vue new file mode 100644 index 00000000..96c80ab1 --- /dev/null +++ b/src/components/settings_modal/helpers/attachment_setting.vue @@ -0,0 +1,126 @@ +<template> + <span + v-if="matchesExpertLevel" + class="AttachmentSetting" + :class="{ '-compact': compact }" + > + <label + :for="path" + :class="{ 'faint': shouldBeDisabled }" + > + <template v-if="backendDescriptionLabel"> + {{ backendDescriptionLabel + ' ' }} + </template> + <template v-else-if="source === 'admin'"> + MISSING LABEL FOR {{ path }} + </template> + <slot v-else /> + + </label> + <p + v-if="backendDescriptionDescription" + class="setting-description" + :class="{ 'faint': shouldBeDisabled }" + > + {{ backendDescriptionDescription + ' ' }} + </p> + <div class="attachment-input"> + <div class="controls control-field"> + <label for="path">{{ $t('settings.url') }}</label> + <input + :id="path" + class="input string-input" + :disabled="shouldBeDisabled" + :value="realDraftMode ? draft : state" + @change="update" + > + {{ ' ' }} + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + </div> + <div v-if="!compact">{{ $t('settings.preview') }}</div> + <Attachment + class="attachment" + :compact="compact" + :attachment="attachment" + size="small" + hide-description + @setMedia="onMedia" + @naturalSizeLoad="onNaturalSizeLoad" + /> + <div class="controls control-upload"> + <MediaUpload + ref="mediaUpload" + class="media-upload-icon" + :drop-files="dropFiles" + normal-button + :accept-types="acceptTypes" + @uploaded="setMediaFile" + @upload-failed="uploadFailed" + /> + </div> + </div> + <DraftButtons /> + </span> +</template> + +<script src="./attachment_setting.js"></script> + +<style lang="scss"> +.AttachmentSetting { + .attachment { + display: block; + width: 100%; + height: 15em; + margin-bottom: 0.5em; + } + + .attachment-input { + margin-left: 1em; + display: flex; + flex-direction: column; + width: 20em; + } + + &.-compact { + .attachment-input { + flex-direction: row; + align-items: flex-end; + } + + .attachment { + flex: 0; + order: 0; + display: block; + min-width: 4em; + height: 4em; + align-self: center; + margin-bottom: 0; + } + + .control-field { + order: 1; + min-width: 12em; + margin-left: 0.5em; + } + + .control-upload { + order: 2; + min-width: 12em; + padding: 0 0.5em; + } + } + + .controls { + margin-bottom: 0.5em; + + input, + button { + width: 100%; + } + } +} +</style> diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js index 2e6992cb..199d3d0f 100644 --- a/src/components/settings_modal/helpers/boolean_setting.js +++ b/src/components/settings_modal/helpers/boolean_setting.js @@ -1,56 +1,31 @@ -import { get, set } from 'lodash' import Checkbox from 'src/components/checkbox/checkbox.vue' -import ModifiedIndicator from './modified_indicator.vue' -import ServerSideIndicator from './server_side_indicator.vue' +import Setting from './setting.js' + export default { + ...Setting, + props: { + ...Setting.props, + indeterminateState: [String, Object] + }, components: { - Checkbox, - ModifiedIndicator, - ServerSideIndicator + ...Setting.components, + Checkbox }, - props: [ - 'path', - 'disabled', - 'expert' - ], computed: { - pathDefault () { - const [firstSegment, ...rest] = this.path.split('.') - return [firstSegment + 'DefaultValue', ...rest].join('.') - }, - state () { - const value = get(this.$parent, this.path) - if (value === undefined) { - return this.defaultState - } else { - return value - } - }, - defaultState () { - return get(this.$parent, this.pathDefault) - }, - isServerSide () { - return this.path.startsWith('serverSide_') - }, - isChanged () { - return !this.path.startsWith('serverSide_') && this.state !== this.defaultState - }, - matchesExpertLevel () { - return (this.expert || 0) <= this.$parent.expertLevel + ...Setting.computed, + isIndeterminate () { + return this.visibleState === this.indeterminateState } }, methods: { - update (e) { - const [firstSegment, ...rest] = this.path.split('.') - set(this.$parent, this.path, e) - // Updating nested properties does not trigger update on its parent. - // probably still not as reliable, but works for depth=1 at least - if (rest.length > 0) { - set(this.$parent, firstSegment, { ...get(this.$parent, firstSegment) }) + ...Setting.methods, + getValue (e) { + // Basic tri-state toggle implementation + if (!!this.indeterminateState && !e && this.visibleState === true) { + // If we have indeterminate state, switching from true to false first goes through indeterminate + return this.indeterminateState } - }, - reset () { - set(this.$parent, this.path, this.defaultState) + return e } } } diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue index 41142966..5a9eab34 100644 --- a/src/components/settings_modal/helpers/boolean_setting.vue +++ b/src/components/settings_modal/helpers/boolean_setting.vue @@ -4,23 +4,37 @@ class="BooleanSetting" > <Checkbox - :model-value="state" - :disabled="disabled" + :model-value="visibleState" + :disabled="shouldBeDisabled" + :indeterminate="isIndeterminate" @update:modelValue="update" > <span - v-if="!!$slots.default" class="label" + :class="{ 'faint': shouldBeDisabled }" > - <slot /> + <template v-if="backendDescriptionLabel"> + {{ backendDescriptionLabel }} + </template> + <template v-else-if="source === 'admin'"> + MISSING LABEL FOR {{ path }} + </template> + <slot v-else /> </span> - {{ ' ' }} - <ModifiedIndicator - :changed="isChanged" - :onclick="reset" - /> - <ServerSideIndicator :server-side="isServerSide" /> </Checkbox> + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + <p + v-if="backendDescriptionDescription" + class="setting-description" + :class="{ 'faint': shouldBeDisabled }" + > + {{ backendDescriptionDescription + ' ' }} + </p> </label> </template> diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js index 3da559fe..bdeece76 100644 --- a/src/components/settings_modal/helpers/choice_setting.js +++ b/src/components/settings_modal/helpers/choice_setting.js @@ -1,51 +1,41 @@ -import { get, set } from 'lodash' import Select from 'src/components/select/select.vue' -import ModifiedIndicator from './modified_indicator.vue' -import ServerSideIndicator from './server_side_indicator.vue' +import Setting from './setting.js' + export default { + ...Setting, components: { - Select, - ModifiedIndicator, - ServerSideIndicator + ...Setting.components, + Select }, - props: [ - 'path', - 'disabled', - 'options', - 'expert' - ], - computed: { - pathDefault () { - const [firstSegment, ...rest] = this.path.split('.') - return [firstSegment + 'DefaultValue', ...rest].join('.') + props: { + ...Setting.props, + options: { + type: Array, + required: false }, - state () { - const value = get(this.$parent, this.path) - if (value === undefined) { - return this.defaultState - } else { - return value + optionLabelMap: { + type: Object, + required: false, + default: {} + } + }, + computed: { + ...Setting.computed, + realOptions () { + if (this.realSource === 'admin') { + return this.backendDescriptionSuggestions.map(x => ({ + key: x, + value: x, + label: this.optionLabelMap[x] || x + })) } - }, - defaultState () { - return get(this.$parent, this.pathDefault) - }, - isServerSide () { - return this.path.startsWith('serverSide_') - }, - isChanged () { - return !this.path.startsWith('serverSide_') && this.state !== this.defaultState - }, - matchesExpertLevel () { - return (this.expert || 0) <= this.$parent.expertLevel + return this.options } }, methods: { - update (e) { - set(this.$parent, this.path, e) - }, - reset () { - set(this.$parent, this.path, this.defaultState) + ...Setting.methods, + getValue (e) { + return e } } } diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue index 8fdbb5d3..114e9b7d 100644 --- a/src/components/settings_modal/helpers/choice_setting.vue +++ b/src/components/settings_modal/helpers/choice_setting.vue @@ -3,15 +3,20 @@ v-if="matchesExpertLevel" class="ChoiceSetting" > - <slot /> + <template v-if="backendDescriptionLabel"> + {{ backendDescriptionLabel }} + </template> + <template v-else> + <slot /> + </template> {{ ' ' }} <Select - :model-value="state" + :model-value="realDraftMode ? draft :state" :disabled="disabled" @update:modelValue="update" > <option - v-for="option in options" + v-for="option in realOptions" :key="option.key" :value="option.value" > @@ -23,7 +28,14 @@ :changed="isChanged" :onclick="reset" /> - <ServerSideIndicator :server-side="isServerSide" /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + <p + v-if="backendDescriptionDescription" + class="setting-description" + > + {{ backendDescriptionDescription + ' ' }} + </p> </label> </template> diff --git a/src/components/settings_modal/helpers/draft_buttons.vue b/src/components/settings_modal/helpers/draft_buttons.vue new file mode 100644 index 00000000..46a70e86 --- /dev/null +++ b/src/components/settings_modal/helpers/draft_buttons.vue @@ -0,0 +1,88 @@ +<!-- this is a helper exclusive to Setting components --> +<!-- TODO make it reusable --> +<template> + <span + class="DraftButtons" + > + <Popover + v-if="$parent.isDirty" + trigger="hover" + normal-button + :trigger-attrs="{ 'aria-label': $t('settings.commit_value_tooltip') }" + @click="$parent.commitDraft" + > + <template #trigger> + {{ $t('settings.commit_value') }} + </template> + <template #content> + <div class="modified-tooltip"> + {{ $t('settings.commit_value_tooltip') }} + </div> + </template> + </Popover> + <Popover + v-if="$parent.isDirty" + trigger="hover" + normal-button + :trigger-attrs="{ 'aria-label': $t('settings.reset_value_tooltip') }" + @click="$parent.reset" + > + <template #trigger> + {{ $t('settings.reset_value') }} + </template> + <template #content> + <div class="modified-tooltip"> + {{ $t('settings.reset_value_tooltip') }} + </div> + </template> + </Popover> + <Popover + v-if="$parent.canHardReset" + trigger="hover" + normal-button + :trigger-attrs="{ 'aria-label': $t('settings.hard_reset_value_tooltip') }" + @click="$parent.hardReset" + > + <template #trigger> + {{ $t('settings.hard_reset_value') }} + </template> + <template #content> + <div class="modified-tooltip"> + {{ $t('settings.hard_reset_value_tooltip') }} + </div> + </template> + </Popover> + </span> +</template> + +<script> +import Popover from 'src/components/popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faWrench } from '@fortawesome/free-solid-svg-icons' + +library.add( + faWrench +) + +export default { + components: { Popover }, + props: ['changed'] +} +</script> + +<style lang="scss"> +.DraftButtons { + display: inline-block; + position: relative; + + .button-default { + margin-left: 0.5em; + } +} + +.draft-tooltip { + margin: 0.5em 1em; + min-width: 10em; + text-align: center; +} +</style> diff --git a/src/components/settings_modal/helpers/emoji_editing_popover.vue b/src/components/settings_modal/helpers/emoji_editing_popover.vue new file mode 100644 index 00000000..f0465dd5 --- /dev/null +++ b/src/components/settings_modal/helpers/emoji_editing_popover.vue @@ -0,0 +1,227 @@ +<template> + <Popover + ref="emojiPopover" + trigger="click" + :placement="placement" + bound-to-selector=".emoji-list" + popover-class="emoji-tab-edit-popover popover-default" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + :disabled="disabled" + :class="{'emoji-unsaved': isEdited}" + > + <template #trigger> + <slot name="trigger" /> + </template> + <template #content> + <h3> + {{ title }} + </h3> + + <StillImage + v-if="emojiPreview" + class="emoji" + :src="emojiPreview" + /> + <div + v-else + class="emoji" + /> + + <div + v-if="newUpload" + class="emoji-tab-popover-input" + > + <input + type="file" + accept="image/*" + class="emoji-tab-popover-file input" + @change="uploadFile = $event.target.files" + > + </div> + <div> + <div class="emoji-tab-popover-input"> + <label> + {{ $t('admin_dash.emoji.shortcode') }} + <input + v-model="editedShortcode" + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.new_shortcode')" + > + </label> + </div> + + <div class="emoji-tab-popover-input"> + <label> + {{ $t('admin_dash.emoji.filename') }} + + <input + v-model="editedFile" + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.new_filename')" + > + </label> + </div> + + <button + class="button button-default btn" + type="button" + :disabled="newUpload ? uploadFile.length == 0 : !isEdited" + @click="newUpload ? uploadEmoji() : saveEditedEmoji()" + > + {{ $t('admin_dash.emoji.save') }} + </button> + + <template v-if="!newUpload"> + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="deleteModalVisible = true" + > + {{ $t('admin_dash.emoji.delete') }} + </button> + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="revertEmoji" + > + {{ $t('admin_dash.emoji.revert') }} + </button> + <ConfirmModal + v-if="deleteModalVisible" + :title="$t('admin_dash.emoji.delete_title')" + :cancel-text="$t('status.delete_confirm_cancel_button')" + :confirm-text="$t('status.delete_confirm_accept_button')" + @cancelled="deleteModalVisible = false" + @accepted="deleteEmoji" + > + {{ $t('admin_dash.emoji.delete_confirm', [shortcode]) }} + </ConfirmModal> + </template> + </div> + </template> + </Popover> +</template> + +<script> +import Popover from 'components/popover/popover.vue' +import ConfirmModal from 'components/confirm_modal/confirm_modal.vue' +import StillImage from 'components/still-image/still-image.vue' + +export default { + components: { Popover, ConfirmModal, StillImage }, + inject: ['emojiAddr'], + props: { + placement: String, + disabled: { + type: Boolean, + default: false + }, + + newUpload: Boolean, + + title: String, + packName: String, + shortcode: { + type: String, + // Only exists when this is not a new upload + default: '' + }, + file: { + type: String, + // Only exists when this is not a new upload + default: '' + } + }, + emits: ['updatePackFiles', 'displayError'], + data () { + return { + uploadFile: [], + editedShortcode: this.shortcode, + editedFile: this.file, + deleteModalVisible: false + } + }, + computed: { + emojiPreview () { + if (this.newUpload && this.uploadFile.length > 0) { + return URL.createObjectURL(this.uploadFile[0]) + } else if (!this.newUpload) { + return this.emojiAddr(this.file) + } + + return null + }, + isEdited () { + return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file) + } + }, + methods: { + saveEditedEmoji () { + if (!this.isEdited) return + + this.$store.state.api.backendInteractor.updateEmojiFile( + { packName: this.packName, shortcode: this.shortcode, newShortcode: this.editedShortcode, newFilename: this.editedFile, force: false } + ).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return Promise.reject(resp.error) + } + + return resp.json() + }).then(resp => this.$emit('updatePackFiles', resp)) + }, + uploadEmoji () { + this.$store.state.api.backendInteractor.addNewEmojiFile({ + packName: this.packName, + file: this.uploadFile[0], + shortcode: this.editedShortcode, + filename: this.editedFile + }).then(resp => resp.json()).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return + } + + this.$emit('updatePackFiles', resp) + this.$refs.emojiPopover.hidePopover() + + this.editedFile = '' + this.editedShortcode = '' + this.uploadFile = [] + }) + }, + revertEmoji () { + this.editedFile = this.file + this.editedShortcode = this.shortcode + }, + deleteEmoji () { + this.deleteModalVisible = false + + this.$store.state.api.backendInteractor.deleteEmojiFile( + { packName: this.packName, shortcode: this.shortcode } + ).then(resp => resp.json()).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return + } + + this.$emit('updatePackFiles', resp) + }) + } + } +} +</script> + +<style lang="scss"> + .emoji-tab-edit-popover { + padding-left: 0.5em; + padding-right: 0.5em; + padding-bottom: 0.5em; + + .emoji { + width: 32px; + height: 32px; + } + } +</style> diff --git a/src/components/settings_modal/helpers/float_setting.vue b/src/components/settings_modal/helpers/float_setting.vue new file mode 100644 index 00000000..15edb3c3 --- /dev/null +++ b/src/components/settings_modal/helpers/float_setting.vue @@ -0,0 +1,16 @@ +<template> + <NumberSetting + v-bind="$attrs" + > + <slot /> + </NumberSetting> +</template> + +<script> +import NumberSetting from './number_setting.vue' +export default { + components: { + NumberSetting + } +} +</script> diff --git a/src/components/settings_modal/helpers/group_setting.js b/src/components/settings_modal/helpers/group_setting.js new file mode 100644 index 00000000..23a2a202 --- /dev/null +++ b/src/components/settings_modal/helpers/group_setting.js @@ -0,0 +1,13 @@ +import { isEqual } from 'lodash' + +import Setting from './setting.js' + +export default { + ...Setting, + computed: { + ...Setting.computed, + isDirty () { + return !isEqual(this.state, this.draft) + } + } +} diff --git a/src/components/settings_modal/helpers/group_setting.vue b/src/components/settings_modal/helpers/group_setting.vue new file mode 100644 index 00000000..a4df4bf3 --- /dev/null +++ b/src/components/settings_modal/helpers/group_setting.vue @@ -0,0 +1,15 @@ +<template> + <span + v-if="matchesExpertLevel" + class="GroupSetting" + > + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + </span> +</template> + +<script src="./group_setting.js"></script> diff --git a/src/components/settings_modal/helpers/integer_setting.js b/src/components/settings_modal/helpers/integer_setting.js deleted file mode 100644 index e64d0cee..00000000 --- a/src/components/settings_modal/helpers/integer_setting.js +++ /dev/null @@ -1,44 +0,0 @@ -import { get, set } from 'lodash' -import ModifiedIndicator from './modified_indicator.vue' -export default { - components: { - ModifiedIndicator - }, - props: { - path: String, - disabled: Boolean, - min: Number, - expert: [Number, String] - }, - computed: { - pathDefault () { - const [firstSegment, ...rest] = this.path.split('.') - return [firstSegment + 'DefaultValue', ...rest].join('.') - }, - state () { - const value = get(this.$parent, this.path) - if (value === undefined) { - return this.defaultState - } else { - return value - } - }, - defaultState () { - return get(this.$parent, this.pathDefault) - }, - isChanged () { - return this.state !== this.defaultState - }, - matchesExpertLevel () { - return (this.expert || 0) <= this.$parent.expertLevel - } - }, - methods: { - update (e) { - set(this.$parent, this.path, parseInt(e.target.value)) - }, - reset () { - set(this.$parent, this.path, this.defaultState) - } - } -} diff --git a/src/components/settings_modal/helpers/integer_setting.vue b/src/components/settings_modal/helpers/integer_setting.vue index 695e2673..43fa7e1a 100644 --- a/src/components/settings_modal/helpers/integer_setting.vue +++ b/src/components/settings_modal/helpers/integer_setting.vue @@ -1,27 +1,17 @@ <template> - <span - v-if="matchesExpertLevel" - class="IntegerSetting" + <NumberSetting + v-bind="$attrs" + truncate="1" > - <label :for="path"> - <slot /> - </label> - <input - :id="path" - class="number-input" - type="number" - step="1" - :disabled="disabled" - :min="min || 0" - :value="state" - @change="update" - > - {{ ' ' }} - <ModifiedIndicator - :changed="isChanged" - :onclick="reset" - /> - </span> + <slot /> + </NumberSetting> </template> -<script src="./integer_setting.js"></script> +<script> +import NumberSetting from './number_setting.vue' +export default { + components: { + NumberSetting + } +} +</script> diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue index 8311533a..a747cebd 100644 --- a/src/components/settings_modal/helpers/modified_indicator.vue +++ b/src/components/settings_modal/helpers/modified_indicator.vue @@ -5,17 +5,17 @@ > <Popover trigger="hover" + :trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }" > <template #trigger> <FAIcon icon="wrench" - :aria-label="$t('settings.setting_changed')" /> </template> <template #content> <div class="modified-tooltip"> - {{ $t('settings.setting_changed') }} + {{ $t(messageKey) }} </div> </template> </Popover> @@ -33,7 +33,13 @@ library.add( export default { components: { Popover }, - props: ['changed'] + props: { + changed: Boolean, + messageKey: { + type: String, + default: 'settings.setting_changed' + } + } } </script> diff --git a/src/components/settings_modal/helpers/number_setting.js b/src/components/settings_modal/helpers/number_setting.js new file mode 100644 index 00000000..676a0d22 --- /dev/null +++ b/src/components/settings_modal/helpers/number_setting.js @@ -0,0 +1,24 @@ +import Setting from './setting.js' + +export default { + ...Setting, + props: { + ...Setting.props, + truncate: { + type: Number, + required: false, + default: 1 + } + }, + methods: { + ...Setting.methods, + getValue (e) { + if (!this.truncate === 1) { + return parseInt(e.target.value) + } else if (this.truncate > 1) { + return Math.trunc(e.target.value / this.truncate) * this.truncate + } + return parseFloat(e.target.value) + } + } +} diff --git a/src/components/settings_modal/helpers/number_setting.vue b/src/components/settings_modal/helpers/number_setting.vue new file mode 100644 index 00000000..32dc6f83 --- /dev/null +++ b/src/components/settings_modal/helpers/number_setting.vue @@ -0,0 +1,46 @@ +<template> + <span + v-if="matchesExpertLevel" + class="NumberSetting" + > + <label + :for="path" + :class="{ 'faint': shouldBeDisabled }" + > + <template v-if="backendDescriptionLabel"> + {{ backendDescriptionLabel + ' ' }} + </template> + <template v-else-if="source === 'admin'"> + MISSING LABEL FOR {{ path }} + </template> + <slot v-else /> + </label> + {{ ' ' }} + <input + :id="path" + class="input number-input" + type="number" + :step="step || 1" + :disabled="shouldBeDisabled" + :min="min || 0" + :value="realDraftMode ? draft :state" + @change="update" + > + {{ ' ' }} + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + <p + v-if="backendDescriptionDescription" + class="setting-description" + :class="{ 'faint': shouldBeDisabled }" + > + {{ backendDescriptionDescription + ' ' }} + </p> + </span> +</template> + +<script src="./number_setting.js"></script> diff --git a/src/components/settings_modal/helpers/server_side_indicator.vue b/src/components/settings_modal/helpers/profile_setting_indicator.vue index bf181959..d160781b 100644 --- a/src/components/settings_modal/helpers/server_side_indicator.vue +++ b/src/components/settings_modal/helpers/profile_setting_indicator.vue @@ -1,7 +1,7 @@ <template> <span - v-if="serverSide" - class="ServerSideIndicator" + v-if="isProfile" + class="ProfileSettingIndicator" > <Popover trigger="hover" @@ -14,7 +14,7 @@ /> </template> <template #content> - <div class="serverside-tooltip"> + <div class="profilesetting-tooltip"> {{ $t('settings.setting_server_side') }} </div> </template> @@ -33,17 +33,17 @@ library.add( export default { components: { Popover }, - props: ['serverSide'] + props: ['isProfile'] } </script> <style lang="scss"> -.ServerSideIndicator { +.ProfileSettingIndicator { display: inline-block; position: relative; } -.serverside-tooltip { +.profilesetting-tooltip { margin: 0.5em 1em; min-width: 10em; text-align: center; diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js new file mode 100644 index 00000000..3b3e6268 --- /dev/null +++ b/src/components/settings_modal/helpers/setting.js @@ -0,0 +1,246 @@ +import ModifiedIndicator from './modified_indicator.vue' +import ProfileSettingIndicator from './profile_setting_indicator.vue' +import DraftButtons from './draft_buttons.vue' +import { get, set, cloneDeep } from 'lodash' + +export default { + components: { + ModifiedIndicator, + DraftButtons, + ProfileSettingIndicator + }, + props: { + path: { + type: [String, Array], + required: true + }, + disabled: { + type: Boolean, + default: false + }, + parentPath: { + type: [String, Array] + }, + parentInvert: { + type: Boolean, + default: false + }, + expert: { + type: [Number, String], + default: 0 + }, + source: { + type: String, + default: undefined + }, + hideDescription: { + type: Boolean + }, + swapDescriptionAndLabel: { + type: Boolean + }, + overrideBackendDescription: { + type: Boolean + }, + overrideBackendDescriptionLabel: { + type: Boolean + }, + draftMode: { + type: Boolean, + default: undefined + }, + timedApplyMode: { + type: Boolean, + default: false + } + }, + inject: { + defaultSource: { + default: 'default' + }, + defaultDraftMode: { + default: false + } + }, + data () { + return { + localDraft: null + } + }, + created () { + if (this.realDraftMode && this.realSource !== 'admin') { + this.draft = this.state + } + }, + computed: { + draft: { + // TODO allow passing shared draft object? + get () { + if (this.realSource === 'admin') { + return get(this.$store.state.adminSettings.draft, this.canonPath) + } else { + return this.localDraft + } + }, + set (value) { + if (this.realSource === 'admin') { + this.$store.commit('updateAdminDraft', { path: this.canonPath, value }) + } else { + this.localDraft = value + } + } + }, + state () { + const value = get(this.configSource, this.canonPath) + if (value === undefined) { + return this.defaultState + } else { + return value + } + }, + visibleState () { + return this.realDraftMode ? this.draft : this.state + }, + realSource () { + return this.source || this.defaultSource + }, + realDraftMode () { + return typeof this.draftMode === 'undefined' ? this.defaultDraftMode : this.draftMode + }, + backendDescription () { + return get(this.$store.state.adminSettings.descriptions, this.path) + }, + backendDescriptionLabel () { + if (this.realSource !== 'admin') return '' + if (!this.backendDescription || this.overrideBackendDescriptionLabel) { + return this.$t([ + 'admin_dash', + 'temp_overrides', + ...this.canonPath.map(p => p.replace(/\./g, '_DOT_')), + 'label' + ].join('.')) + } else { + return this.swapDescriptionAndLabel + ? this.backendDescription?.description + : this.backendDescription?.label + } + }, + backendDescriptionDescription () { + if (this.realSource !== 'admin') return '' + if (this.hideDescription) return null + if (!this.backendDescription || this.overrideBackendDescription) { + return this.$t([ + 'admin_dash', + 'temp_overrides', + ...this.canonPath.map(p => p.replace(/\./g, '_DOT_')), + 'description' + ].join('.')) + } else { + return this.swapDescriptionAndLabel + ? this.backendDescription?.label + : this.backendDescription?.description + } + }, + backendDescriptionSuggestions () { + return this.backendDescription?.suggestions + }, + shouldBeDisabled () { + const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null + return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false) + }, + configSource () { + switch (this.realSource) { + case 'profile': + return this.$store.state.profileConfig + case 'admin': + return this.$store.state.adminSettings.config + default: + return this.$store.getters.mergedConfig + } + }, + configSink () { + switch (this.realSource) { + case 'profile': + return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v }) + case 'admin': + return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v }) + default: + if (this.timedApplyMode) { + return (k, v) => this.$store.dispatch('setOptionTemporarily', { name: k, value: v }) + } else { + return (k, v) => this.$store.dispatch('setOption', { name: k, value: v }) + } + } + }, + defaultState () { + switch (this.realSource) { + case 'profile': + return {} + default: + return get(this.$store.getters.defaultConfig, this.path) + } + }, + isProfileSetting () { + return this.realSource === 'profile' + }, + isChanged () { + switch (this.realSource) { + case 'profile': + case 'admin': + return false + default: + return this.state !== this.defaultState + } + }, + canonPath () { + return Array.isArray(this.path) ? this.path : this.path.split('.') + }, + isDirty () { + if (this.realSource === 'admin' && this.canonPath.length > 3) { + return false // should not show draft buttons for "grouped" values + } else { + return this.realDraftMode && this.draft !== this.state + } + }, + canHardReset () { + return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths && + this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> ')) + }, + matchesExpertLevel () { + return (this.expert || 0) <= this.$store.state.config.expertLevel > 0 + } + }, + methods: { + getValue (e) { + return e.target.value + }, + update (e) { + if (this.realDraftMode) { + this.draft = this.getValue(e) + } else { + this.configSink(this.path, this.getValue(e)) + } + }, + commitDraft () { + if (this.realDraftMode) { + this.configSink(this.path, this.draft) + } + }, + reset () { + if (this.realDraftMode) { + this.draft = cloneDeep(this.state) + } else { + set(this.$store.getters.mergedConfig, this.path, cloneDeep(this.defaultState)) + } + }, + hardReset () { + switch (this.realSource) { + case 'admin': + return this.$store.dispatch('resetAdminSetting', { path: this.path }) + .then(() => { this.draft = this.state }) + default: + console.warn('Hard reset not implemented yet!') + } + } + } +} diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js index 12431dca..bb3d36ac 100644 --- a/src/components/settings_modal/helpers/shared_computed_object.js +++ b/src/components/settings_modal/helpers/shared_computed_object.js @@ -1,52 +1,18 @@ -import { defaultState as configDefaultState } from 'src/modules/config.js' -import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js' - const SharedComputedObject = () => ({ user () { return this.$store.state.users.currentUser }, - // Getting values for default properties - ...Object.keys(configDefaultState) - .map(key => [ - key + 'DefaultValue', - function () { - return this.$store.getters.defaultConfig[key] - } - ]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), - // Generating computed values for vuex properties - ...Object.keys(configDefaultState) - .map(key => [key, { - get () { return this.$store.getters.mergedConfig[key] }, - set (value) { - this.$store.dispatch('setOption', { name: key, value }) - } - }]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), - ...Object.keys(serverSideConfigDefaultState) - .map(key => ['serverSide_' + key, { - get () { return this.$store.state.serverSideConfig[key] }, - set (value) { - this.$store.dispatch('setServerSideOption', { name: key, value }) - } - }]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), - // Special cases (need to transform values or perform actions first) - useStreamingApi: { - get () { return this.$store.getters.mergedConfig.useStreamingApi }, - set (value) { - const promise = value - ? this.$store.dispatch('enableMastoSockets') - : this.$store.dispatch('disableMastoSockets') - - promise.then(() => { - this.$store.dispatch('setOption', { name: 'useStreamingApi', value }) - }).catch((e) => { - console.error('Failed starting MastoAPI Streaming socket', e) - this.$store.dispatch('disableMastoSockets') - this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false }) - }) - } + expertLevel () { + return this.$store.getters.mergedConfig.expertLevel > 0 + }, + mergedConfig () { + return this.$store.getters.mergedConfig + }, + adminConfig () { + return this.$store.state.adminSettings.config + }, + adminDraft () { + return this.$store.state.adminSettings.draft } }) diff --git a/src/components/settings_modal/helpers/size_setting.js b/src/components/settings_modal/helpers/size_setting.js deleted file mode 100644 index 58697412..00000000 --- a/src/components/settings_modal/helpers/size_setting.js +++ /dev/null @@ -1,67 +0,0 @@ -import { get, set } from 'lodash' -import ModifiedIndicator from './modified_indicator.vue' -import Select from 'src/components/select/select.vue' - -export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%'] -export const defaultHorizontalUnits = ['px', 'rem', 'vw'] -export const defaultVerticalUnits = ['px', 'rem', 'vh'] - -export default { - components: { - ModifiedIndicator, - Select - }, - props: { - path: String, - disabled: Boolean, - min: Number, - units: { - type: [String], - default: () => allCssUnits - }, - expert: [Number, String] - }, - computed: { - pathDefault () { - const [firstSegment, ...rest] = this.path.split('.') - return [firstSegment + 'DefaultValue', ...rest].join('.') - }, - stateUnit () { - return (this.state || '').replace(/\d+/, '') - }, - stateValue () { - return (this.state || '').replace(/\D+/, '') - }, - state () { - const value = get(this.$parent, this.path) - if (value === undefined) { - return this.defaultState - } else { - return value - } - }, - defaultState () { - return get(this.$parent, this.pathDefault) - }, - isChanged () { - return this.state !== this.defaultState - }, - matchesExpertLevel () { - return (this.expert || 0) <= this.$parent.expertLevel - } - }, - methods: { - update (e) { - set(this.$parent, this.path, e) - }, - reset () { - set(this.$parent, this.path, this.defaultState) - }, - updateValue (e) { - set(this.$parent, this.path, parseInt(e.target.value) + this.stateUnit) - }, - updateUnit (e) { - set(this.$parent, this.path, this.stateValue + e.target.value) - } - } -} diff --git a/src/components/settings_modal/helpers/string_setting.js b/src/components/settings_modal/helpers/string_setting.js new file mode 100644 index 00000000..b368cfc8 --- /dev/null +++ b/src/components/settings_modal/helpers/string_setting.js @@ -0,0 +1,5 @@ +import Setting from './setting.js' + +export default { + ...Setting +} diff --git a/src/components/settings_modal/helpers/string_setting.vue b/src/components/settings_modal/helpers/string_setting.vue new file mode 100644 index 00000000..7b30d1b9 --- /dev/null +++ b/src/components/settings_modal/helpers/string_setting.vue @@ -0,0 +1,42 @@ +<template> + <label + v-if="matchesExpertLevel" + class="StringSetting" + > + <label + :for="path" + :class="{ 'faint': shouldBeDisabled }" + > + <template v-if="backendDescriptionLabel"> + {{ backendDescriptionLabel + ' ' }} + </template> + <template v-else-if="source === 'admin'"> + MISSING LABEL FOR {{ path }} + </template> + <slot v-else /> + </label> + <input + :id="path" + class="input string-input" + :disabled="shouldBeDisabled" + :value="realDraftMode ? draft : state" + @change="update" + > + {{ ' ' }} + <ModifiedIndicator + :changed="isChanged" + :onclick="reset" + /> + <ProfileSettingIndicator :is-profile="isProfileSetting" /> + <DraftButtons /> + <p + v-if="backendDescriptionDescription" + class="setting-description" + :class="{ 'faint': shouldBeDisabled }" + > + {{ backendDescriptionDescription + ' ' }} + </p> + </label> +</template> + +<script src="./string_setting.js"></script> diff --git a/src/components/settings_modal/helpers/unit_setting.js b/src/components/settings_modal/helpers/unit_setting.js new file mode 100644 index 00000000..daeddd81 --- /dev/null +++ b/src/components/settings_modal/helpers/unit_setting.js @@ -0,0 +1,64 @@ +import Select from 'src/components/select/select.vue' +import Setting from './setting.js' + +export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%'] +export const defaultHorizontalUnits = ['px', 'rem', 'vw'] +export const defaultVerticalUnits = ['px', 'rem', 'vh'] + +export default { + ...Setting, + components: { + ...Setting.components, + Select + }, + props: { + ...Setting.props, + min: Number, + units: { + type: Array, + default: () => allCssUnits + }, + unitSet: { + type: String, + default: 'none' + }, + step: { + type: Number, + default: 1 + }, + resetDefault: { + type: Object, + default: null + } + }, + computed: { + ...Setting.computed, + stateUnit () { + return typeof this.state === 'string' ? this.state.replace(/[0-9,.]+/, '') : '' + }, + stateValue () { + return typeof this.state === 'string' ? this.state.replace(/[^0-9,.]+/, '') : '' + } + }, + methods: { + ...Setting.methods, + getUnitString (value) { + if (this.unitSet === 'none') return value + return this.$t(['settings', 'units', this.unitSet, value].join('.')) + }, + updateValue (e) { + this.configSink(this.path, parseFloat(e.target.value) + this.stateUnit) + }, + updateUnit (e) { + let value = this.stateValue + const newUnit = e.target.value + if (this.resetDefault) { + const replaceValue = this.resetDefault[newUnit] + if (replaceValue != null) { + value = replaceValue + } + } + this.configSink(this.path, value + newUnit) + } + } +} diff --git a/src/components/settings_modal/helpers/size_setting.vue b/src/components/settings_modal/helpers/unit_setting.vue index 5a78f100..40ab6880 100644 --- a/src/components/settings_modal/helpers/size_setting.vue +++ b/src/components/settings_modal/helpers/unit_setting.vue @@ -1,7 +1,7 @@ <template> <span v-if="matchesExpertLevel" - class="SizeSetting" + class="UnitSetting" > <label :for="path" @@ -9,11 +9,12 @@ > <slot /> </label> + {{ ' ' }} <input :id="path" - class="number-input" + class="input number-input" type="number" - step="1" + :step="step" :disabled="disabled" :min="min || 0" :value="stateValue" @@ -23,7 +24,7 @@ :id="path" :model-value="stateUnit" :disabled="disabled" - class="css-unit-input" + class="unit-input unstyled" @change="updateUnit" > <option @@ -31,7 +32,7 @@ :key="option" :value="option" > - {{ option }} + {{ getUnitString(option) }} </option> </Select> {{ ' ' }} @@ -42,14 +43,20 @@ </span> </template> -<script src="./size_setting.js"></script> +<script src="./unit_setting.js"></script> <style lang="scss"> -.css-unit-input, -.css-unit-input select { - margin-left: 0.5em; - width: 4em; - max-width: 4em; - min-width: 4em; +.UnitSetting { + .number-input { + max-width: 6.5em; + text-align: right; + } + + .unit-input, + .unit-input select { + min-width: 4em; + width: auto; + } } + </style> diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js index 0a72dca1..63c9b24a 100644 --- a/src/components/settings_modal/settings_modal.js +++ b/src/components/settings_modal/settings_modal.js @@ -4,8 +4,9 @@ import AsyncComponentError from 'src/components/async_component_error/async_comp import getResettableAsyncComponent from 'src/services/resettable_async_component.js' import Popover from '../popover/popover.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' +import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' -import { cloneDeep } from 'lodash' +import { cloneDeep, isEqual } from 'lodash' import { newImporter, newExporter @@ -53,8 +54,17 @@ const SettingsModal = { Modal, Popover, Checkbox, - SettingsModalContent: getResettableAsyncComponent( - () => import('./settings_modal_content.vue'), + ConfirmModal, + SettingsModalUserContent: getResettableAsyncComponent( + () => import('./settings_modal_user_content.vue'), + { + loadingComponent: PanelLoading, + errorComponent: AsyncComponentError, + delay: 0 + } + ), + SettingsModalAdminContent: getResettableAsyncComponent( + () => import('./settings_modal_admin_content.vue'), { loadingComponent: PanelLoading, errorComponent: AsyncComponentError, @@ -147,17 +157,30 @@ const SettingsModal = { PLEROMAFE_SETTINGS_MINOR_VERSION ] return clone + }, + resetAdminDraft () { + this.$store.commit('resetAdminDraft') + }, + pushAdminDraft () { + this.$store.dispatch('pushAdminDraft') } }, computed: { currentSaveStateNotice () { + console.log(this.$store.state.interface.settings.currentSaveStateNotice) return this.$store.state.interface.settings.currentSaveStateNotice }, modalActivated () { return this.$store.state.interface.settingsModalState !== 'hidden' }, - modalOpenedOnce () { - return this.$store.state.interface.settingsModalLoaded + modalMode () { + return this.$store.state.interface.settingsModalMode + }, + modalOpenedOnceUser () { + return this.$store.state.interface.settingsModalLoadedUser + }, + modalOpenedOnceAdmin () { + return this.$store.state.interface.settingsModalLoadedAdmin }, modalPeeked () { return this.$store.state.interface.settingsModalState === 'minimized' @@ -167,9 +190,14 @@ const SettingsModal = { return this.$store.state.config.expertLevel > 0 }, set (value) { - console.log(value) this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 }) } + }, + adminDraftAny () { + return !isEqual( + this.$store.state.adminSettings.config, + this.$store.state.adminSettings.draft + ) } } } diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss index f5861229..d01553db 100644 --- a/src/components/settings_modal/settings_modal.scss +++ b/src/components/settings_modal/settings_modal.scss @@ -1,8 +1,10 @@ -@import "src/variables"; - .settings-modal { overflow: hidden; + h4 { + margin-bottom: 0.5em; + } + .setting-list, .option-list { list-style-type: none; @@ -15,6 +17,20 @@ .suboptions { margin-top: 0.3em; } + + &.two-column { + column-count: 2; + + > li { + break-inside: avoid; + } + } + } + + .setting-description { + margin-top: 0.2em; + margin-bottom: 2em; + font-size: 70%; } .settings-modal-panel { @@ -37,7 +53,9 @@ .btn { min-height: 2em; - min-width: 10em; + } + + .btn:not(.dropdown-button) { padding: 0 2em; } } @@ -45,6 +63,8 @@ .settings-footer { display: flex; + flex-wrap: wrap; + line-height: 2; >* { margin-right: 0.5em; diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue index 7b457371..90dbbde0 100644 --- a/src/components/settings_modal/settings_modal.vue +++ b/src/components/settings_modal/settings_modal.vue @@ -8,13 +8,13 @@ <div class="settings-modal-panel panel"> <div class="panel-heading"> <span class="title"> - {{ $t('settings.settings') }} + {{ modalMode === 'user' ? $t('settings.settings') : $t('admin_dash.window_title') }} </span> <transition name="fade"> <div v-if="currentSaveStateNotice" class="alert" - :class="{ transparent: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}" + :class="{ success: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}" @click.prevent > {{ currentSaveStateNotice.error ? $t('settings.saving_err') : $t('settings.saving_ok') }} @@ -42,10 +42,12 @@ </button> </div> <div class="panel-body"> - <SettingsModalContent v-if="modalOpenedOnce" /> + <SettingsModalUserContent v-if="modalMode === 'user' && modalOpenedOnceUser" /> + <SettingsModalAdminContent v-if="modalMode === 'admin' && modalOpenedOnceAdmin" /> </div> - <div class="panel-footer settings-footer"> + <div class="panel-footer settings-footer -flexible-height"> <Popover + v-if="modalMode === 'user'" class="export" trigger="click" placement="top" @@ -68,7 +70,7 @@ <template #content="{close}"> <div class="dropdown-menu"> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click.prevent="backup" @click="close" > @@ -78,7 +80,7 @@ /><span>{{ $t("settings.file_export_import.backup_settings") }}</span> </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click.prevent="backupWithTheme" @click="close" > @@ -88,7 +90,7 @@ /><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span> </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click.prevent="restore" @click="close" > @@ -107,12 +109,56 @@ > {{ $t("settings.expert_mode") }} </Checkbox> + <span v-if="modalMode === 'admin'"> + <i18n-t keypath="admin_dash.wip_notice"> + <template #adminFeLink> + <a + href="/pleroma/admin/#/login-pleroma" + target="_blank" + > + {{ $t("admin_dash.old_ui_link") }} + </a> + </template> + </i18n-t> + </span> <span id="unscrolled-content" class="extra-content" /> + <span + v-if="modalMode === 'admin'" + class="admin-buttons" + > + <button + class="button-default btn" + :disabled="!adminDraftAny" + @click="resetAdminDraft" + > + {{ $t("admin_dash.reset_all") }} + </button> + {{ ' ' }} + <button + class="button-default btn" + :disabled="!adminDraftAny" + @click="pushAdminDraft" + > + {{ $t("admin_dash.commit_all") }} + </button> + </span> </div> </div> + <teleport to="#modal"> + <ConfirmModal + v-if="$store.state.interface.temporaryChangesTimeoutId" + :title="$t('settings.confirm_new_setting')" + :cancel-text="$t('settings.revert')" + :confirm-text="$t('settings.confirm')" + @cancelled="$store.state.interface.temporaryChangesRevert" + @accepted="$store.state.interface.temporaryChangesConfirm" + > + {{ $t('settings.confirm_new_question') }} + </ConfirmModal> + </teleport> </Modal> </template> diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js new file mode 100644 index 00000000..ce835bf2 --- /dev/null +++ b/src/components/settings_modal/settings_modal_admin_content.js @@ -0,0 +1,95 @@ +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' + +import InstanceTab from './admin_tabs/instance_tab.vue' +import LimitsTab from './admin_tabs/limits_tab.vue' +import FrontendsTab from './admin_tabs/frontends_tab.vue' +import EmojiTab from './admin_tabs/emoji_tab.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faWrench, + faHand, + faLaptopCode, + faPaintBrush, + faBell, + faDownload, + faEyeSlash, + faInfo +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faWrench, + faHand, + faLaptopCode, + faPaintBrush, + faBell, + faDownload, + faEyeSlash, + faInfo +) + +const SettingsModalAdminContent = { + components: { + TabSwitcher, + + InstanceTab, + LimitsTab, + FrontendsTab, + EmojiTab + }, + computed: { + user () { + return this.$store.state.users.currentUser + }, + isLoggedIn () { + return !!this.$store.state.users.currentUser + }, + open () { + return this.$store.state.interface.settingsModalState !== 'hidden' + }, + bodyLock () { + return this.$store.state.interface.settingsModalState === 'visible' + }, + adminDbLoaded () { + return this.$store.state.adminSettings.loaded + }, + adminDescriptionsLoaded () { + return this.$store.state.adminSettings.descriptions !== null + }, + noDb () { + return this.$store.state.adminSettings.dbConfigEnabled === false + } + }, + created () { + if (this.user.rights.admin) { + this.$store.dispatch('loadAdminStuff') + } + }, + methods: { + onOpen () { + const targetTab = this.$store.state.interface.settingsModalTargetTab + // We're being told to open in specific tab + if (targetTab) { + const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => { + return elm.props && elm.props['data-tab-name'] === targetTab + }) + if (tabIndex >= 0) { + this.$refs.tabSwitcher.setTab(tabIndex) + } + } + // Clear the state of target tab, so that next time settings is opened + // it doesn't force it. + this.$store.dispatch('clearSettingsModalTargetTab') + } + }, + mounted () { + this.onOpen() + }, + watch: { + open: function (value) { + if (value) this.onOpen() + } + } +} + +export default SettingsModalAdminContent diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_admin_content.scss index 87df7982..a5314fe1 100644 --- a/src/components/settings_modal/settings_modal_content.scss +++ b/src/components/settings_modal/settings_modal_admin_content.scss @@ -1,10 +1,8 @@ -@import "src/variables"; - .settings_tab-switcher { height: 100%; .setting-item { - border-bottom: 2px solid var(--fg, $fallback--fg); + border-bottom: 2px solid var(--border); margin: 1em 1em 1.4em; padding-bottom: 1.4em; @@ -33,10 +31,6 @@ margin-bottom: 1em; } - select { - min-width: 10em; - } - textarea { width: 100%; max-width: 100%; @@ -45,12 +39,7 @@ .unavailable, .unavailable svg { - color: var(--cRed, $fallback--cRed); - color: $fallback--cRed; - } - - .number-input { - max-width: 6em; + color: var(--cRed); } } } diff --git a/src/components/settings_modal/settings_modal_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue new file mode 100644 index 00000000..65e23b7e --- /dev/null +++ b/src/components/settings_modal/settings_modal_admin_content.vue @@ -0,0 +1,76 @@ +<template> + <tab-switcher + v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)" + ref="tabSwitcher" + class="settings_tab-switcher" + :side-tab-bar="true" + :scrollable-tabs="true" + :render-only-focused="true" + :body-scroll-lock="bodyLock" + > + <div + v-if="noDb" + :label="$t('admin_dash.tabs.nodb')" + icon="exclamation-triangle" + data-tab-name="nodb-notice" + > + <div :label="$t('admin_dash.tabs.nodb')"> + <div class="setting-item"> + <h2>{{ $t('admin_dash.nodb.heading') }}</h2> + <i18n-t keypath="admin_dash.nodb.text"> + <template #documentation> + <a + href="https://docs-develop.pleroma.social/backend/configuration/howto_database_config/" + target="_blank" + > + {{ $t("admin_dash.nodb.documentation") }} + </a> + </template> + <template #property> + <code>config :pleroma, configurable_from_database</code> + </template> + <template #value> + <code>true</code> + </template> + </i18n-t> + <p>{{ $t('admin_dash.nodb.text2') }}</p> + </div> + </div> + </div> + <div + v-if="adminDbLoaded" + :label="$t('admin_dash.tabs.instance')" + icon="wrench" + data-tab-name="general" + > + <InstanceTab /> + </div> + <div + v-if="adminDbLoaded" + :label="$t('admin_dash.tabs.limits')" + icon="hand" + data-tab-name="limits" + > + <LimitsTab /> + </div> + <div + :label="$t('admin_dash.tabs.frontends')" + icon="laptop-code" + data-tab-name="frontends" + > + <FrontendsTab /> + </div> + + <div + :label="$t('admin_dash.tabs.emoji')" + icon="face-smile-beam" + data-tab-name="emoji" + > + <EmojiTab /> + </div> + </tab-switcher> +</template> + +<script src="./settings_modal_admin_content.js"></script> + +<style src="./settings_modal_admin_content.scss" lang="scss"></style> diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_user_content.js index 9ac0301f..ebd5329f 100644 --- a/src/components/settings_modal/settings_modal_content.js +++ b/src/components/settings_modal/settings_modal_user_content.js @@ -7,6 +7,7 @@ import FilteringTab from './tabs/filtering_tab.vue' import SecurityTab from './tabs/security_tab/security_tab.vue' import ProfileTab from './tabs/profile_tab.vue' import GeneralTab from './tabs/general_tab.vue' +import AppearanceTab from './tabs/appearance_tab.vue' import VersionTab from './tabs/version_tab.vue' import ThemeTab from './tabs/theme_tab/theme_tab.vue' @@ -19,7 +20,8 @@ import { faBell, faDownload, faEyeSlash, - faInfo + faInfo, + faWindowRestore } from '@fortawesome/free-solid-svg-icons' library.add( @@ -30,7 +32,8 @@ library.add( faBell, faDownload, faEyeSlash, - faInfo + faInfo, + faWindowRestore ) const SettingsModalContent = { @@ -44,6 +47,7 @@ const SettingsModalContent = { SecurityTab, ProfileTab, GeneralTab, + AppearanceTab, VersionTab, ThemeTab }, diff --git a/src/components/settings_modal/settings_modal_user_content.scss b/src/components/settings_modal/settings_modal_user_content.scss new file mode 100644 index 00000000..a5314fe1 --- /dev/null +++ b/src/components/settings_modal/settings_modal_user_content.scss @@ -0,0 +1,45 @@ +.settings_tab-switcher { + height: 100%; + + .setting-item { + border-bottom: 2px solid var(--border); + margin: 1em 1em 1.4em; + padding-bottom: 1.4em; + + > div, + > label { + display: block; + margin-bottom: 0.5em; + + &:last-child { + margin-bottom: 0; + } + } + + .select-multiple { + display: flex; + + .option-list { + margin: 0; + padding-left: 0.5em; + } + } + + &:last-child { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 1em; + } + + textarea { + width: 100%; + max-width: 100%; + height: 100px; + } + + .unavailable, + .unavailable svg { + color: var(--cRed); + } + } +} diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_user_content.vue index 0be76d22..1441d892 100644 --- a/src/components/settings_modal/settings_modal_content.vue +++ b/src/components/settings_modal/settings_modal_user_content.vue @@ -14,6 +14,20 @@ <GeneralTab /> </div> <div + :label="$t('settings.appearance')" + icon="window-restore" + data-tab-name="appearance" + > + <AppearanceTab /> + </div> + <div + :label="$t('settings.theme')" + icon="paint-brush" + data-tab-name="theme" + > + <ThemeTab /> + </div> + <div v-if="isLoggedIn" :label="$t('settings.profile_tab')" icon="user" @@ -23,6 +37,14 @@ </div> <div v-if="isLoggedIn" + :label="$t('settings.notifications')" + icon="bell" + data-tab-name="notifications" + > + <NotificationsTab /> + </div> + <div + v-if="isLoggedIn" :label="$t('settings.security_tab')" icon="lock" data-tab-name="security" @@ -37,19 +59,13 @@ <FilteringTab /> </div> <div - :label="$t('settings.theme')" - icon="paint-brush" - data-tab-name="theme" - > - <ThemeTab /> - </div> - <div v-if="isLoggedIn" - :label="$t('settings.notifications')" - icon="bell" - data-tab-name="notifications" + :label="$t('settings.mutes_and_blocks')" + :fullHeight="true" + icon="eye-slash" + data-tab-name="mutesAndBlocks" > - <NotificationsTab /> + <MutesAndBlocksTab /> </div> <div v-if="isLoggedIn" @@ -60,15 +76,6 @@ <DataImportExportTab /> </div> <div - v-if="isLoggedIn" - :label="$t('settings.mutes_and_blocks')" - :fullHeight="true" - icon="eye-slash" - data-tab-name="mutesAndBlocks" - > - <MutesAndBlocksTab /> - </div> - <div :label="$t('settings.version.title')" icon="info" data-tab-name="version" @@ -78,6 +85,6 @@ </tab-switcher> </template> -<script src="./settings_modal_content.js"></script> +<script src="./settings_modal_user_content.js"></script> -<style src="./settings_modal_content.scss" lang="scss"></style> +<style src="./settings_modal_user_content.scss" lang="scss"></style> diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js new file mode 100644 index 00000000..b5fd6c4c --- /dev/null +++ b/src/components/settings_modal/tabs/appearance_tab.js @@ -0,0 +1,195 @@ +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' +import IntegerSetting from '../helpers/integer_setting.vue' +import FloatSetting from '../helpers/float_setting.vue' +import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue' + +import FontControl from 'src/components/font_control/font_control.vue' + +import { normalizeThemeData } from 'src/modules/interface' + +import { + getThemes +} from 'src/services/style_setter/style_setter.js' +import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' +import { init } from 'src/services/theme_data/theme_data_3.service.js' +import { + getCssRules, + getScopedVersion +} from 'src/services/theme_data/css_utils.js' + +import SharedComputedObject from '../helpers/shared_computed_object.js' +import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +import Preview from './theme_tab/theme_preview.vue' + +library.add( + faGlobe +) + +const AppearanceTab = { + data () { + return { + availableStyles: [], + intersectionObserver: null, + thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({ + key: mode, + value: mode, + label: this.$t(`settings.third_column_mode_${mode}`) + })), + forcedRoundnessOptions: ['disabled', 'sharp', 'nonsharp', 'round'].map((mode, i) => ({ + key: mode, + value: i - 1, + label: this.$t(`settings.style.themes3.hacks.forced_roundness_mode_${mode}`) + })), + underlayOverrideModes: ['none', 'opaque', 'transparent'].map((mode, i) => ({ + key: mode, + value: mode, + label: this.$t(`settings.style.themes3.hacks.underlay_override_mode_${mode}`) + })) + } + }, + components: { + BooleanSetting, + ChoiceSetting, + IntegerSetting, + FloatSetting, + UnitSetting, + ProfileSettingIndicator, + FontControl, + Preview + }, + mounted () { + getThemes() + .then((promises) => { + return Promise.all( + Object.entries(promises) + .map(([k, v]) => v.then(res => [k, res])) + ) + }) + .then(themes => themes.reduce((acc, [k, v]) => { + if (v) { + return [ + ...acc, + { + name: v.name || v[0], + key: k, + data: v + } + ] + } else { + return acc + } + }, [])) + .then((themesComplete) => { + this.availableStyles = themesComplete + }) + + if (window.IntersectionObserver) { + this.intersectionObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(({ target, isIntersecting }) => { + if (!isIntersecting) return + const theme = this.availableStyles.find(x => x.key === target.dataset.themeKey) + this.$nextTick(() => { + if (theme) theme.ready = true + }) + observer.unobserve(target) + }) + }, { + root: this.$refs.themeList + }) + } + }, + updated () { + this.$nextTick(() => { + this.$refs.themeList.querySelectorAll('.theme-preview').forEach(node => { + this.intersectionObserver.observe(node) + }) + }) + }, + computed: { + noIntersectionObserver () { + return !window.IntersectionObserver + }, + horizontalUnits () { + return defaultHorizontalUnits + }, + fontsOverride () { + return this.$store.getters.mergedConfig.fontsOverride + }, + columns () { + const mode = this.$store.getters.mergedConfig.thirdColumnMode + + const notif = mode === 'none' ? [] : ['notifs'] + + if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') { + return [...notif, 'content', 'sidebar'] + } else { + return ['sidebar', 'content', ...notif] + } + }, + instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, + instanceWallpaperUsed () { + return this.$store.state.instance.background && + !this.$store.state.users.currentUser.background_image + }, + instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable }, + language: { + get: function () { return this.$store.getters.mergedConfig.interfaceLanguage }, + set: function (val) { + this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) + } + }, + isCustomThemeUsed () { + const { theme } = this.mergedConfig + return theme === 'custom' || theme === null + }, + ...SharedComputedObject() + }, + methods: { + updateFont (key, value) { + console.log(key, value) + this.$store.dispatch('setOption', { + name: 'theme3hacks', + value: { + ...this.mergedConfig.theme3hacks, + fonts: { + ...this.mergedConfig.theme3hacks.fonts, + [key]: value + } + } + }) + }, + isThemeActive (key) { + const { theme } = this.mergedConfig + return key === theme + }, + setTheme (name) { + this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true }) + }, + previewTheme (key, input) { + const style = normalizeThemeData(input) + const x = 2 + if (x === 1) return + const theme2 = convertTheme2To3(style) + const theme3 = init({ + inputRuleset: theme2, + ultimateBackgroundColor: '#000000', + liteMode: true, + debug: true, + onlyNormalState: true + }) + + return getScopedVersion( + getCssRules(theme3.eager), + '#theme-preview-' + key + ).join('\n') + } + } +} + +export default AppearanceTab diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue new file mode 100644 index 00000000..de6eb8e7 --- /dev/null +++ b/src/components/settings_modal/tabs/appearance_tab.vue @@ -0,0 +1,313 @@ +<template> + <div class="appearance-tab" :label="$t('settings.general')"> + <div class="setting-item"> + <h2>{{ $t('settings.theme') }}</h2> + <ul + class="theme-list" + ref="themeList" + > + <button + v-if="isCustomThemeUsed" + disabled + class="button-default theme-preview" + > + <preview /> + <h4 class="theme-name">{{ $t('settings.style.custom_theme_used') }}</h4> + </button> + <button + v-for="style in availableStyles" + :data-theme-key="style.key" + :key="style.key" + class="button-default theme-preview" + :class="{ toggled: isThemeActive(style.key) }" + @click="setTheme(style.key)" + > + <!-- eslint-disable vue/no-v-text-v-html-on-component --> + <component + :is="'style'" + v-if="style.ready || noIntersectionObserver" + v-html="previewTheme(style.key, style.data)" + /> + <!-- eslint-enable vue/no-v-text-v-html-on-component --> + <preview :class="{ placeholder: ready }" :id="'theme-preview-' + style.key"/> + <h4 class="theme-name">{{ style.name }}</h4> + </button> + </ul> + </div> + <div class="alert neutral theme-notice"> + {{ $t("settings.style.appearance_tab_note") }} + </div> + <div class="setting-item"> + <h2>{{ $t('settings.scale_and_layout') }}</h2> + <ul class="setting-list"> + <li> + <UnitSetting + path="textSize" + step="0.1" + :units="['px', 'rem']" + :reset-default="{ 'px': 14, 'rem': 1 }" + timed-apply-mode + > + {{ $t('settings.text_size') }} + </UnitSetting> + <div> + <small> + <i18n-t + scope="global" + keypath="settings.text_size_tip" + tag="span" + > + <code>px</code> + <code>rem</code> + </i18n-t> + <br/> + <i18n-t + scope="global" + keypath="settings.text_size_tip2" + tag="span" + > + <code>14px</code> + </i18n-t> + </small> + </div> + </li> + <li> + <h3>{{ $t('settings.style.interface_font_user_override') }}</h3> + <ul class="setting-list"> + <li> + <FontControl + :model-value="mergedConfig.theme3hacks.fonts.interface" + name="ui" + :label="$t('settings.style.fonts.components.interface')" + :fallback="{ family: 'sans-serif' }" + no-inherit="1" + @update:modelValue="v => updateFont('interface', v)" + /> + </li> + <li> + <FontControl + v-if="expertLevel > 0" + :model-value="mergedConfig.theme3hacks.fonts.input" + name="input" + :fallback="{ family: 'inherit' }" + :label="$t('settings.style.fonts.components.input')" + @update:modelValue="v => updateFont('input', v)" + /> + </li> + <li> + <FontControl + v-if="expertLevel > 0" + :model-value="mergedConfig.theme3hacks.fonts.post" + name="post" + :fallback="{ family: 'inherit' }" + :label="$t('settings.style.fonts.components.post')" + @update:modelValue="v => updateFont('post', v)" + /> + </li> + <li> + <FontControl + v-if="expertLevel > 0" + :model-value="mergedConfig.theme3hacks.fonts.monospace" + name="postCode" + :fallback="{ family: 'monospace' }" + :label="$t('settings.style.fonts.components.monospace')" + @update:modelValue="v => updateFont('monospace', v)" + /> + </li> + </ul> + </li> + <li> + <UnitSetting + path="emojiSize" + step="0.1" + :units="['px', 'rem']" + :reset-default="{ 'px': 32, 'rem': 2.2 }" + > + {{ $t('settings.emoji_size') }} + </UnitSetting> + <ul + class="setting-list suboptions" + > + <li> + <FloatSetting + v-if="user" + path="emojiReactionsScale" + expert="1" + > + {{ $t('settings.emoji_reactions_scale') }} + </FloatSetting> + </li> + </ul> + </li> + <li> + <UnitSetting + path="navbarSize" + step="0.1" + :units="['px', 'rem']" + :reset-default="{ 'px': 55, 'rem': 3.5 }" + > + {{ $t('settings.navbar_size') }} + </UnitSetting> + </li> + <h3>{{ $t('settings.columns') }}</h3> + <li> + <UnitSetting + path="panelHeaderSize" + step="0.1" + :units="['px', 'rem']" + :reset-default="{ 'px': 52, 'rem': 3.2 }" + timed-apply-mode + > + {{ $t('settings.panel_header_size') }} + </UnitSetting> + </li> + <li> + <BooleanSetting path="sidebarRight"> + {{ $t('settings.right_sidebar') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="navbarColumnStretch"> + {{ $t('settings.navbar_column_stretch') }} + </BooleanSetting> + </li> + <li> + <ChoiceSetting + v-if="user" + id="thirdColumnMode" + path="thirdColumnMode" + :options="thirdColumnModeOptions" + > + {{ $t('settings.third_column_mode') }} + </ChoiceSetting> + </li> + <li v-if="expertLevel > 0"> + {{ $t('settings.column_sizes') }} + <div class="column-settings"> + <UnitSetting + v-for="column in columns" + :key="column" + :path="column + 'ColumnWidth'" + :units="horizontalUnits" + expert="1" + > + {{ $t('settings.column_sizes_' + column) }} + </UnitSetting> + </div> + </li> + <li> + <BooleanSetting path="disableStickyHeaders"> + {{ $t('settings.disable_sticky_headers') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="showScrollbars"> + {{ $t('settings.show_scrollbars') }} + </BooleanSetting> + </li> + </ul> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.visual_tweaks') }}</h2> + <ul class="setting-list"> + <li> + <ChoiceSetting + id="forcedRoundness" + path="forcedRoundness" + :options="forcedRoundnessOptions" + > + {{ $t('settings.style.themes3.hacks.force_interface_roundness') }} + </ChoiceSetting> + </li> + <li> + <ChoiceSetting + id="underlayOverride" + path="theme3hacks.underlay" + :options="underlayOverrideModes" + > + {{ $t('settings.style.themes3.hacks.underlay_overrides') }} + </ChoiceSetting> + </li> + <li v-if="instanceWallpaperUsed"> + <BooleanSetting path="hideInstanceWallpaper"> + {{ $t('settings.hide_wallpaper') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + path="forceThemeRecompilation" + :expert="1" + > + {{ $t('settings.force_theme_recompilation_debug') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + path="themeDebug" + :expert="1" + > + {{ $t('settings.theme_debug') }} + </BooleanSetting> + </li> + </ul> + </div> + </div> +</template> + +<script src="./appearance_tab.js"></script> + +<style lang="scss"> +.appearance-tab { + .theme-notice { + padding: 0.5em; + margin: 1em; + } + + .column-settings { + display: flex; + justify-content: space-evenly; + flex-wrap: wrap; + } + + .column-settings .size-label { + display: block; + margin-bottom: 0.5em; + margin-top: 0.5em; + } + + .theme-list { + list-style: none; + display: flex; + flex-wrap: wrap; + margin: -0.5em 0; + height: 25em; + overflow-x: hidden; + overflow-y: auto; + scrollbar-gutter: stable; + border-radius: var(--roundness); + border: 1px solid var(--border); + padding: 0; + + .theme-preview { + font-size: 1rem; // fix for firefox + width: 19rem; + display: flex; + flex-direction: column; + align-items: center; + margin: 0.5em; + + &.placeholder { + opacity: 0.2; + } + + .theme-preview-container { + pointer-events: none; + zoom: 0.5; + border: none; + border-radius: var(--roundness); + text-align: left; + } + } + } +} +</style> diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js index 7c37f0bc..fbace15d 100644 --- a/src/components/settings_modal/tabs/filtering_tab.js +++ b/src/components/settings_modal/tabs/filtering_tab.js @@ -1,6 +1,7 @@ import { filter, trim, debounce } from 'lodash' import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' +import UnitSetting from '../helpers/unit_setting.vue' import IntegerSetting from '../helpers/integer_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' @@ -19,6 +20,7 @@ const FilteringTab = { components: { BooleanSetting, ChoiceSetting, + UnitSetting, IntegerSetting }, computed: { diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue index 97046ff0..c86810d5 100644 --- a/src/components/settings_modal/tabs/filtering_tab.vue +++ b/src/components/settings_modal/tabs/filtering_tab.vue @@ -7,13 +7,11 @@ <BooleanSetting path="hideFilteredStatuses"> {{ $t('settings.hide_filtered_statuses') }} </BooleanSetting> - <ul - class="setting-list suboptions" - :class="[{disabled: !streaming}]" - > + <ul class="setting-list suboptions"> <li> <BooleanSetting - :disabled="hideFilteredStatuses" + parent-path="hideFilteredStatuses" + :parent-invert="true" path="hideWordFilteredPosts" > {{ $t('settings.hide_wordfiltered_statuses') }} @@ -22,7 +20,8 @@ <li> <BooleanSetting v-if="user" - :disabled="hideFilteredStatuses" + parent-path="hideFilteredStatuses" + :parent-invert="true" path="hideMutedThreads" > {{ $t('settings.hide_muted_threads') }} @@ -31,7 +30,8 @@ <li> <BooleanSetting v-if="user" - :disabled="hideFilteredStatuses" + parent-path="hideFilteredStatuses" + :parent-invert="true" path="hideMutedPosts" > {{ $t('settings.hide_muted_posts') }} @@ -45,13 +45,18 @@ </BooleanSetting> </li> <li> + <BooleanSetting path="muteSensitiveStatuses"> + {{ $t('settings.mute_sensitive_posts') }} + </BooleanSetting> + </li> + <li> <BooleanSetting path="hidePostStats"> {{ $t('settings.hide_post_stats') }} </BooleanSetting> </li> <li> <BooleanSetting path="hideBotIndication"> - {{ $t('settings.hide_bot_indication') }} + {{ $t('settings.hide_actor_type_indication') }} </BooleanSetting> </li> <ChoiceSetting @@ -67,7 +72,7 @@ <textarea id="muteWords" v-model="muteWordsString" - class="resize-height" + class="input resize-height" /> <div>{{ $t('settings.filtering_explanation') }}</div> </li> @@ -91,6 +96,22 @@ {{ $t('settings.hide_attachments_in_convo') }} </BooleanSetting> </li> + <li> + <BooleanSetting path="hideScrobbles"> + {{ $t('settings.hide_scrobbles') }} + </BooleanSetting> + </li> + <li> + <UnitSetting + key="hideScrobblesAfter" + path="hideScrobblesAfter" + :units="['m', 'h', 'd']" + unitSet="time" + expert="1" + > + {{ $t('settings.hide_scrobbles_after') }} + </UnitSetting> + </li> </ul> </div> <div diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index ea24d6ad..e9e8fa72 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -2,11 +2,12 @@ import BooleanSetting from '../helpers/boolean_setting.vue' import ChoiceSetting from '../helpers/choice_setting.vue' import ScopeSelector from 'src/components/scope_selector/scope_selector.vue' import IntegerSetting from '../helpers/integer_setting.vue' -import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue' +import FloatSetting from '../helpers/float_setting.vue' +import UnitSetting from '../helpers/unit_setting.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' -import ServerSideIndicator from '../helpers/server_side_indicator.vue' +import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faGlobe @@ -39,11 +40,6 @@ const GeneralTab = { value: mode, label: this.$t(`settings.mention_link_display_${mode}`) })), - thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({ - key: mode, - value: mode, - label: this.$t(`settings.third_column_mode_${mode}`) - })), userPopoverAvatarActionOptions: ['close', 'zoom', 'open'].map(mode => ({ key: mode, value: mode, @@ -62,15 +58,13 @@ const GeneralTab = { BooleanSetting, ChoiceSetting, IntegerSetting, - SizeSetting, + FloatSetting, + UnitSetting, InterfaceLanguageSwitcher, ScopeSelector, - ServerSideIndicator + ProfileSettingIndicator }, computed: { - horizontalUnits () { - return defaultHorizontalUnits - }, postFormats () { return this.$store.state.instance.postFormats || [] }, @@ -81,23 +75,6 @@ const GeneralTab = { label: this.$t(`post_status.content_type["${format}"]`) })) }, - columns () { - const mode = this.$store.getters.mergedConfig.thirdColumnMode - - const notif = mode === 'none' ? [] : ['notifs'] - - if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') { - return [...notif, 'content', 'sidebar'] - } else { - return ['sidebar', 'content', ...notif] - } - }, - instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, - instanceWallpaperUsed () { - return this.$store.state.instance.background && - !this.$store.state.users.currentUser.background_image - }, - instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable }, language: { get: function () { return this.$store.getters.mergedConfig.interfaceLanguage }, set: function (val) { @@ -108,7 +85,7 @@ const GeneralTab = { }, methods: { changeDefaultScope (value) { - this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value }) + this.$store.dispatch('setProfileOption', { name: 'defaultScope', value }) } } } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 703e94a0..82d5da89 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -15,11 +15,6 @@ {{ $t('settings.hide_isp') }} </BooleanSetting> </li> - <li v-if="instanceWallpaperUsed"> - <BooleanSetting path="hideInstanceWallpaper"> - {{ $t('settings.hide_wallpaper') }} - </BooleanSetting> - </li> <li> <BooleanSetting path="stopGifs"> {{ $t('settings.stop_gifs') }} @@ -29,14 +24,11 @@ <BooleanSetting path="streaming"> {{ $t('settings.streaming') }} </BooleanSetting> - <ul - class="setting-list suboptions" - :class="[{disabled: !streaming}]" - > + <ul class="setting-list suboptions"> <li> <BooleanSetting path="pauseOnUnfocused" - :disabled="!streaming" + parent-path="streaming" > {{ $t('settings.pause_on_unfocused') }} </BooleanSetting> @@ -101,53 +93,6 @@ {{ $t('settings.hide_shoutbox') }} </BooleanSetting> </li> - <li> - <h3>{{ $t('settings.columns') }}</h3> - </li> - <li> - <BooleanSetting path="disableStickyHeaders"> - {{ $t('settings.disable_sticky_headers') }} - </BooleanSetting> - </li> - <li> - <BooleanSetting path="showScrollbars"> - {{ $t('settings.show_scrollbars') }} - </BooleanSetting> - </li> - <li> - <BooleanSetting path="sidebarRight"> - {{ $t('settings.right_sidebar') }} - </BooleanSetting> - </li> - <li> - <BooleanSetting path="navbarColumnStretch"> - {{ $t('settings.navbar_column_stretch') }} - </BooleanSetting> - </li> - <li> - <ChoiceSetting - v-if="user" - id="thirdColumnMode" - path="thirdColumnMode" - :options="thirdColumnModeOptions" - > - {{ $t('settings.third_column_mode') }} - </ChoiceSetting> - </li> - <li v-if="expertLevel > 0"> - {{ $t('settings.column_sizes') }} - <div class="column-settings"> - <SizeSetting - v-for="column in columns" - :key="column" - :path="column + 'ColumnWidth'" - :units="horizontalUnits" - expert="1" - > - {{ $t('settings.column_sizes_' + column) }} - </SizeSetting> - </div> - </li> <li class="select-multiple"> <span class="label">{{ $t('settings.confirm_dialogs') }}</span> <ul class="option-list"> @@ -213,7 +158,7 @@ </ChoiceSetting> </li> <ul - v-if="conversationDisplay !== 'linear'" + v-if="mergedConfig.conversationDisplay !== 'linear'" class="setting-list suboptions" > <li> @@ -265,12 +210,36 @@ <li> <BooleanSetting v-if="user" - path="serverSide_stripRichContent" + source="profile" + path="stripRichContent" expert="1" > {{ $t('settings.no_rich_text_description') }} </BooleanSetting> </li> + <li> + <BooleanSetting + path="useAbsoluteTimeFormat" + expert="1" + > + {{ $t('settings.absolute_time_format') }} + </BooleanSetting> + </li> + <ul + class="setting-list suboptions" + v-if="mergedConfig.useAbsoluteTimeFormat" + > + <li> + <UnitSetting + path="absoluteTimeFormatMinAge" + unit-set="time" + :units="['s', 'm', 'h', 'd']" + :min="0" + > + {{ $t('settings.absolute_time_format_min_age') }} + </UnitSetting> + </li> + </ul> <h3>{{ $t('settings.attachments') }}</h3> <li> <BooleanSetting @@ -290,7 +259,7 @@ <BooleanSetting path="preloadImage" expert="1" - :disabled="!hideNsfw" + parent-path="hideNsfw" > {{ $t('settings.preload_images') }} </BooleanSetting> @@ -299,7 +268,7 @@ <BooleanSetting path="useOneClickNsfw" expert="1" - :disabled="!hideNsfw" + parent-path="hideNsfw" > {{ $t('settings.use_one_click_nsfw') }} </BooleanSetting> @@ -312,15 +281,13 @@ > {{ $t('settings.loop_video') }} </BooleanSetting> - <ul - class="setting-list suboptions" - :class="[{disabled: !streaming}]" - > + <ul class="setting-list suboptions"> <li> <BooleanSetting path="loopVideoSilentOnly" expert="1" - :disabled="!loopVideo || !loopSilentAvailable" + parent-path="loopVideo" + :disabled="!loopSilentAvailable" > {{ $t('settings.loop_video_silent_only') }} </BooleanSetting> @@ -418,18 +385,18 @@ <ul class="setting-list"> <li> <label for="default-vis"> - {{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" /> + {{ $t('settings.default_vis') }} <ProfileSettingIndicator :is-profile="true" /> <ScopeSelector class="scope-selector" :show-all="true" - :user-default="serverSide_defaultScope" - :initial-scope="serverSide_defaultScope" + :user-default="$store.state.profileConfig.defaultScope" + :initial-scope="$store.state.profileConfig.defaultScope" :on-scope-change="changeDefaultScope" /> </label> </li> <li> - <!-- <BooleanSetting path="serverSide_defaultNSFW"> --> + <!-- <BooleanSetting source="profile" path="defaultNSFW"> --> <BooleanSetting path="sensitiveByDefault"> {{ $t('settings.sensitive_by_default') }} </BooleanSetting> @@ -501,23 +468,17 @@ {{ $t('settings.pad_emoji') }} </BooleanSetting> </li> + <li> + <BooleanSetting + path="autocompleteSelect" + expert="1" + > + {{ $t('settings.autocomplete_select_first') }} + </BooleanSetting> + </li> </ul> </div> </div> </template> <script src="./general_tab.js"></script> - -<style lang="scss"> -.column-settings { - display: flex; - justify-content: space-evenly; - flex-wrap: wrap; -} - -.column-settings .size-label { - display: block; - margin-bottom: 0.5em; - margin-top: 0.5em; -} -</style> diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js index 6cfeea35..51974f9f 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.js +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.js @@ -9,17 +9,20 @@ import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue import SelectableList from 'src/components/selectable_list/selectable_list.vue' import ProgressButton from 'src/components/progress_button/progress_button.vue' import withSubscription from 'src/components/../hocs/with_subscription/with_subscription' +import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more' import Checkbox from 'src/components/checkbox/checkbox.vue' -const BlockList = withSubscription({ +const BlockList = withLoadMore({ fetch: (props, $store) => $store.dispatch('fetchBlocks'), select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []), + destroy: () => {}, childPropName: 'items' })(SelectableList) -const MuteList = withSubscription({ +const MuteList = withLoadMore({ fetch: (props, $store) => $store.dispatch('fetchMutes'), select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []), + destroy: () => {}, childPropName: 'items' })(SelectableList) diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js index 3c6ab87f..c53b5889 100644 --- a/src/components/settings_modal/tabs/notifications_tab.js +++ b/src/components/settings_modal/tabs/notifications_tab.js @@ -16,6 +16,10 @@ const NotificationsTab = { user () { return this.$store.state.users.currentUser }, + canReceiveReports () { + if (!this.user) { return false } + return this.user.privileges.includes('reports_manage_reports') + }, ...SharedComputedObject() }, methods: { diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue index dd3806ed..10228888 100644 --- a/src/components/settings_modal/tabs/notifications_tab.vue +++ b/src/components/settings_modal/tabs/notifications_tab.vue @@ -1,49 +1,239 @@ <template> <div :label="$t('settings.notifications')"> <div class="setting-item"> + <h2>{{ $t('settings.notification_setting_annoyance') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting path="closingDrawerMarksAsSeen"> + {{ $t('settings.notification_setting_drawer_marks_as_seen') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="ignoreInactionableSeen"> + {{ $t('settings.notification_setting_ignore_inactionable_seen') }} + </BooleanSetting> + <div> + <small> + {{ $t('settings.notification_setting_ignore_inactionable_seen_tip') }} + </small> + </div> + </li> + <li> + <BooleanSetting + path="unseenAtTop" + expert="1" + > + {{ $t('settings.notification_setting_unseen_at_top') }} + </BooleanSetting> + </li> + </ul> + </div> + <div class="setting-item"> <h2>{{ $t('settings.notification_setting_filters') }}</h2> <ul class="setting-list"> <li> - <BooleanSetting path="serverSide_blockNotificationsFromStrangers"> + <BooleanSetting + source="profile" + path="blockNotificationsFromStrangers" + > {{ $t('settings.notification_setting_block_from_strangers') }} </BooleanSetting> </li> - <li class="select-multiple"> - <span class="label">{{ $t('settings.notification_visibility') }}</span> - <ul class="option-list"> + <li> + <h3> {{ $t('settings.notification_visibility') }}</h3> + <p v-if="expertLevel > 0"> + {{ $t('settings.notification_setting_filters_chrome_push') }} + </p> + <ul class="setting-list two-column"> <li> - <BooleanSetting path="notificationVisibility.likes"> - {{ $t('settings.notification_visibility_likes') }} - </BooleanSetting> + <h4> {{ $t('settings.notification_visibility_mentions') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.mentions"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.mentions"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> </li> <li> - <BooleanSetting path="notificationVisibility.repeats"> - {{ $t('settings.notification_visibility_repeats') }} - </BooleanSetting> + <h4> {{ $t('settings.notification_visibility_statuses') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.statuses"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.statuses"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> </li> <li> - <BooleanSetting path="notificationVisibility.follows"> - {{ $t('settings.notification_visibility_follows') }} - </BooleanSetting> + <h4> {{ $t('settings.notification_visibility_likes') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.likes"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.likes"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> </li> <li> - <BooleanSetting path="notificationVisibility.mentions"> - {{ $t('settings.notification_visibility_mentions') }} + <h4> {{ $t('settings.notification_visibility_repeats') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.repeats"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.repeats"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <h4> {{ $t('settings.notification_visibility_emoji_reactions') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.emojiReactions"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.emojiReactions"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <h4> {{ $t('settings.notification_visibility_follows') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.follows"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.follows"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <h4> {{ $t('settings.notification_visibility_follow_requests') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.followRequest"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.followRequest"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <h4> {{ $t('settings.notification_visibility_moves') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.moves"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.moves"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <h4> {{ $t('settings.notification_visibility_polls') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.polls"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.polls"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + <li v-if="canReceiveReports"> + <h4> {{ $t('settings.notification_visibility_reports') }}</h4> + <ul class="setting-list"> + <li> + <BooleanSetting path="notificationVisibility.reports"> + {{ $t('settings.notification_visibility_in_column') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationNative.reports"> + {{ $t('settings.notification_visibility_native_notifications') }} + </BooleanSetting> + </li> + </ul> + </li> + </ul> + </li> + <li> + <BooleanSetting path="showExtraNotifications"> + {{ $t('settings.notification_show_extra') }} + </BooleanSetting> + </li> + <li> + <ul class="setting-list suboptions"> + <li> + <BooleanSetting + path="showChatsInExtraNotifications" + :disabled="!mergedConfig.showExtraNotifications" + > + {{ $t('settings.notification_extra_chats') }} </BooleanSetting> </li> <li> - <BooleanSetting path="notificationVisibility.moves"> - {{ $t('settings.notification_visibility_moves') }} + <BooleanSetting + path="showAnnouncementsInExtraNotifications" + :disabled="!mergedConfig.showExtraNotifications" + > + {{ $t('settings.notification_extra_announcements') }} </BooleanSetting> </li> <li> - <BooleanSetting path="notificationVisibility.emojiReactions"> - {{ $t('settings.notification_visibility_emoji_reactions') }} + <BooleanSetting + path="showFollowRequestsInExtraNotifications" + :disabled="!mergedConfig.showExtraNotifications" + > + {{ $t('settings.notification_extra_follow_requests') }} </BooleanSetting> </li> <li> - <BooleanSetting path="notificationVisibility.polls"> - {{ $t('settings.notification_visibility_polls') }} + <BooleanSetting + path="showExtraNotificationsTip" + :disabled="!mergedConfig.showExtraNotifications" + > + {{ $t('settings.notification_extra_tip') }} </BooleanSetting> </li> </ul> @@ -64,10 +254,26 @@ > {{ $t('settings.enable_web_push_notifications') }} </BooleanSetting> + <ul class="setting-list suboptions"> + <li> + <BooleanSetting + path="webPushAlwaysShowNotifications" + :disabled="!mergedConfig.webPushNotifications" + > + {{ $t('settings.enable_web_push_always_show') }} + </BooleanSetting> + <div :class="{ faint: !mergedConfig.webPushNotifications }"> + <small> + {{ $t('settings.enable_web_push_always_show_tip') }} + </small> + </div> + </li> + </ul> </li> <li> <BooleanSetting - path="serverSide_webPushHideContents" + source="profile" + path="webPushHideContents" expert="1" > {{ $t('settings.notification_setting_hide_notification_contents') }} diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index eeacad48..dee17450 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -9,6 +9,7 @@ import suggestor from 'src/components/emoji_input/suggestor.js' import Autosuggest from 'src/components/autosuggest/autosuggest.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' +import Select from 'src/components/select/select.vue' import BooleanSetting from '../helpers/boolean_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' import localeService from 'src/services/locale/locale.service.js' @@ -39,6 +40,7 @@ const ProfileTab = { showRole: this.$store.state.users.currentUser.show_role, role: this.$store.state.users.currentUser.role, bot: this.$store.state.users.currentUser.bot, + actorType: this.$store.state.users.currentUser.actor_type, pickAvatarBtnVisible: true, bannerUploading: false, backgroundUploading: false, @@ -57,7 +59,8 @@ const ProfileTab = { ProgressButton, Checkbox, BooleanSetting, - InterfaceLanguageSwitcher + InterfaceLanguageSwitcher, + Select }, computed: { user () { @@ -116,6 +119,12 @@ const ProfileTab = { bannerImgSrc () { const src = this.$store.state.users.currentUser.cover_photo return (!src) ? this.defaultBanner : src + }, + groupActorAvailable () { + return this.$store.state.instance.groupActorAvailable + }, + availableActorTypes () { + return this.groupActorAvailable ? ['Person', 'Service', 'Group'] : ['Person', 'Service'] } }, methods: { @@ -127,7 +136,7 @@ const ProfileTab = { /* eslint-disable camelcase */ display_name: this.newName, fields_attributes: this.newFields.filter(el => el != null), - bot: this.bot, + actor_type: this.actorType, show_role: this.showRole, birthday: this.newBirthday || '', show_birthday: this.showBirthday diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss index ee253ffe..7eda943b 100644 --- a/src/components/settings_modal/tabs/profile_tab.scss +++ b/src/components/settings_modal/tabs/profile_tab.scss @@ -1,5 +1,3 @@ -@import "../../../variables"; - .profile-tab { .bio { margin: 0; @@ -43,16 +41,14 @@ display: block; width: 100%; height: 100%; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); } .reset-button { position: absolute; top: 0.2em; right: 0.2em; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); background-color: rgb(0 0 0 / 60%); opacity: 0.7; width: 1.5em; diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue index 6a5b478a..034034a1 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -12,7 +12,7 @@ <input id="username" v-model="newName" - class="name-changer" + class="input name-changer" v-bind="propsToNative(inputProps)" > </template> @@ -26,7 +26,7 @@ <template #default="inputProps"> <textarea v-model="newBio" - class="bio resize-height" + class="input bio resize-height" v-bind="propsToNative(inputProps)" /> </template> @@ -47,7 +47,7 @@ id="birthday" v-model="newBirthday" type="date" - class="birthday-input" + class="input birthday-input" > <Checkbox v-model="showBirthday"> {{ $t('settings.birthday.show_birthday') }} @@ -71,6 +71,7 @@ v-model="newFields[i].name" :placeholder="$t('settings.profile_fields.name')" v-bind="propsToNative(inputProps)" + class="input" > </template> </EmojiInput> @@ -85,6 +86,7 @@ v-model="newFields[i].value" :placeholder="$t('settings.profile_fields.value')" v-bind="propsToNative(inputProps)" + class="input" > </template> </EmojiInput> @@ -109,10 +111,24 @@ </button> </div> <p> - <Checkbox v-model="bot"> - {{ $t('settings.bot') }} - </Checkbox> + <label> + {{ $t('settings.actor_type') }} + <Select v-model="actorType"> + <option + v-for="option in availableActorTypes" + :key="option" + :value="option" + > + {{ $t('settings.actor_type_' + option) }} + </option> + </Select> + </label> </p> + <div v-if="groupActorAvailable"> + <small> + {{ $t('settings.actor_type_description') }} + </small> + </div> <p> <interface-language-switcher :prompt-text="$t('settings.email_language')" @@ -191,6 +207,7 @@ <div> <input type="file" + class="input" @change="uploadFile('banner', $event)" > </div> @@ -233,6 +250,7 @@ <div> <input type="file" + class="input" @change="uploadFile('background', $event)" > </div> @@ -254,37 +272,50 @@ <h2>{{ $t('settings.account_privacy') }}</h2> <ul class="setting-list"> <li> - <BooleanSetting path="serverSide_locked"> + <BooleanSetting + source="profile" + path="locked" + > {{ $t('settings.lock_account_description') }} </BooleanSetting> </li> <li> - <BooleanSetting path="serverSide_discoverable"> + <BooleanSetting + source="profile" + path="discoverable" + > {{ $t('settings.discoverable') }} </BooleanSetting> </li> <li> - <BooleanSetting path="serverSide_allowFollowingMove"> + <BooleanSetting + source="profile" + path="allowFollowingMove" + > {{ $t('settings.allow_following_move') }} </BooleanSetting> </li> <li> - <BooleanSetting path="serverSide_hideFavorites"> + <BooleanSetting + source="profile" + path="hideFavorites" + > {{ $t('settings.hide_favorites_description') }} </BooleanSetting> </li> <li> - <BooleanSetting path="serverSide_hideFollowers"> + <BooleanSetting + source="profile" + path="hideFollowers" + > {{ $t('settings.hide_followers_description') }} </BooleanSetting> - <ul - class="setting-list suboptions" - :class="[{disabled: !serverSide_hideFollowers}]" - > + <ul class="setting-list suboptions"> <li> <BooleanSetting - path="serverSide_hideFollowersCount" - :disabled="!serverSide_hideFollowers" + source="profile" + path="hideFollowersCount" + parent-path="hideFollowers" > {{ $t('settings.hide_followers_count_description') }} </BooleanSetting> @@ -292,17 +323,18 @@ </ul> </li> <li> - <BooleanSetting path="serverSide_hideFollows"> + <BooleanSetting + source="profile" + path="hideFollows" + > {{ $t('settings.hide_follows_description') }} </BooleanSetting> - <ul - class="setting-list suboptions" - :class="[{disabled: !serverSide_hideFollows}]" - > + <ul class="setting-list suboptions"> <li> <BooleanSetting - path="serverSide_hideFollowsCount" - :disabled="!serverSide_hideFollows" + source="profile" + path="hideFollowsCount" + parent-path="hideFollows" > {{ $t('settings.hide_follows_count_description') }} </BooleanSetting> diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue index ee5b6b13..9421b16e 100644 --- a/src/components/settings_modal/tabs/security_tab/mfa.vue +++ b/src/components/settings_modal/tabs/security_tab/mfa.vue @@ -99,12 +99,14 @@ <input v-model="otpConfirmToken" type="text" + class="input" > <p>{{ $t('settings.enter_current_password_to_confirm') }}:</p> <input v-model="currentPassword" type="password" + class="input" > <div class="confirm-otp-actions"> <button @@ -137,8 +139,6 @@ <script src="./mfa.js"></script> <style lang="scss"> -@import "../../../../variables"; - .mfa-settings { .mfa-heading, .method-item { @@ -149,8 +149,7 @@ } .warning { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } .setup-otp { diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue index 923161b2..32a8a759 100644 --- a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue +++ b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue @@ -21,16 +21,13 @@ </template> <script src="./mfa_backup_codes.js"></script> <style lang="scss"> -@import "../../../../variables"; - .mfa-backup-codes { .warning { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } .backup-codes { - font-family: var(--postCodeFont, monospace); + font-family: var(--monoFont); } } </style> diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue index 8e767bd0..99b66818 100644 --- a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue +++ b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue @@ -30,6 +30,7 @@ <input v-model="currentPassword" type="password" + class="input" > </confirm> <div diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue index 6e03bef4..74103f6f 100644 --- a/src/components/settings_modal/tabs/security_tab/security_tab.vue +++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue @@ -8,6 +8,7 @@ v-model="newEmail" type="email" autocomplete="email" + class="input" > </div> <div> @@ -16,6 +17,7 @@ v-model="changeEmailPassword" type="password" autocomplete="current-password" + class="input" > </div> <button @@ -40,6 +42,7 @@ <input v-model="changePasswordInputs[0]" type="password" + class="input" > </div> <div> @@ -47,6 +50,7 @@ <input v-model="changePasswordInputs[1]" type="password" + class="input" > </div> <div> @@ -54,6 +58,7 @@ <input v-model="changePasswordInputs[2]" type="password" + class="input" > </div> <button @@ -143,8 +148,8 @@ /> </div> <div> - <i18n - path="settings.new_alias_target" + <i18n-t + keypath="settings.new_alias_target" tag="p" > <code @@ -152,9 +157,10 @@ > foo@example.org </code> - </i18n> + </i18n-t> <input v-model="addAliasTarget" + class="input" > </div> <button @@ -175,18 +181,19 @@ <h2>{{ $t('settings.move_account') }}</h2> <p>{{ $t('settings.move_account_notes') }}</p> <div> - <i18n - path="settings.move_account_target" + <i18n-t + keypath="settings.move_account_target" tag="p" > - <code - place="example" - > - foo@example.org - </code> - </i18n> + <template #example> + <code> + foo@example.org + </code> + </template> + </i18n-t> <input v-model="moveAccountTarget" + class="input" > </div> <div> @@ -195,6 +202,7 @@ v-model="moveAccountPassword" type="password" autocomplete="current-password" + class="input" > </div> <button @@ -222,6 +230,7 @@ <input v-model="deleteAccountConfirmPasswordInput" type="password" + class="input" > <button class="btn button-default" diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/theme_preview.vue index d755279a..dbaecee4 100644 --- a/src/components/settings_modal/tabs/theme_tab/preview.vue +++ b/src/components/settings_modal/tabs/theme_tab/theme_preview.vue @@ -1,11 +1,11 @@ <template> - <div class="preview-container"> + <div class="theme-preview-container"> <div class="underlay underlay-preview" /> <div class="panel dummy"> <div class="panel-heading"> <div class="title"> {{ $t('settings.style.preview.header') }} - <span class="badge badge-notification"> + <span class="badge -notification"> 99 </span> </div> @@ -81,7 +81,7 @@ class="faint" scope="global" > - <a style="color: var(--faintLink);"> + <a style="color: var(--linkFaint);"> {{ $t('settings.style.preview.faint_link') }} </a> </i18n-t> @@ -95,17 +95,13 @@ <input :value="$t('settings.style.preview.input')" type="text" + class="input" > <div class="actions"> - <span class="checkbox"> - <input - id="preview_checkbox" - checked="very yes" - type="checkbox" - > - <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label> - </span> + <Checkbox> + {{ $t('settings.style.preview.checkbox') }} + </Checkbox> <button class="btn button-default"> {{ $t('settings.style.preview.button') }} </button> @@ -116,6 +112,7 @@ </template> <script> +import Checkbox from 'src/components/checkbox/checkbox.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes, @@ -131,19 +128,123 @@ library.add( faReply ) -export default {} +export default { + components: { + Checkbox + } +} </script> <style lang="scss"> -.preview-container { +.theme-preview-container { position: relative; -} + border-top: 1px dashed; + border-bottom: 1px dashed; + border-color: var(--border); + margin: 1em 0; + padding: 1em; + background-color: var(--wallpaper); + background-image: var(--body-background-image); + background-size: cover; + background-position: 50% 50%; + + .theme-preview-content { + padding: 20px; + } + + .dummy { + .post { + font-family: var(--postFont); + display: flex; + + .content { + flex: 1; + + h4 { + margin-bottom: 0.25em; + } + + .icons { + margin-top: 0.5em; + display: flex; + + i { + margin-right: 1em; + } + } + } + } + + .after-post { + margin-top: 1em; + display: flex; + align-items: center; + } + + .avatar, + .avatar-alt { + background: + linear-gradient( + 135deg, + #b8e1fc 0%, + #a9d2f3 10%, + #90bae4 25%, + #90bcea 37%, + #90bff0 50%, + #6ba8e5 51%, + #a2daf5 83%, + #bdf3fd 100% + ); + color: black; + font-family: sans-serif; + text-align: center; + margin-right: 1em; + } + + .avatar-alt { + flex: 0 auto; + margin-left: 28px; + font-size: 12px; + min-width: 20px; + min-height: 20px; + line-height: 20px; + } + + .avatar { + flex: 0 auto; + width: 48px; + height: 48px; + font-size: 14px; + line-height: 48px; + } + + .actions { + display: flex; + align-items: baseline; + + .checkbox { + margin-right: 1em; + flex: 1; + } + } + + .separator { + margin: 1em; + border-bottom: 1px solid; + border-color: var(--border); + } + + .btn { + min-width: 3em; + } + } -.underlay-preview { - position: absolute; - top: 0; - bottom: 0; - left: 10px; - right: 10px; + .underlay-preview { + position: absolute; + top: 0; + bottom: 0; + left: 10px; + right: 10px; + } } -</style> + </style> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js index 4a739f73..25836559 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -1,18 +1,11 @@ import { rgb2hex, hex2rgb, - getContrastRatioLayers + getContrastRatioLayers, + relativeLuminance } from 'src/services/color_convert/color_convert.js' import { - DEFAULT_SHADOWS, - generateColors, - generateShadows, - generateRadii, - generateFonts, - composePreset, - getThemes, - shadows2to3, - colors2to3 + getThemes } from 'src/services/style_setter/style_setter.js' import { newImporter, @@ -25,8 +18,23 @@ import { CURRENT_VERSION, OPACITIES, getLayers, - getOpacitySlot + getOpacitySlot, + DEFAULT_SHADOWS, + generateColors, + generateShadows, + generateRadii, + generateFonts, + shadows2to3, + colors2to3 } from 'src/services/theme_data/theme_data.service.js' + +import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' +import { init } from 'src/services/theme_data/theme_data_3.service.js' +import { + getCssRules, + getScopedVersion +} from 'src/services/theme_data/css_utils.js' + import ColorInput from 'src/components/color_input/color_input.vue' import RangeInput from 'src/components/range_input/range_input.vue' import OpacityInput from 'src/components/opacity_input/opacity_input.vue' @@ -37,7 +45,7 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import Checkbox from 'src/components/checkbox/checkbox.vue' import Select from 'src/components/select/select.vue' -import Preview from './preview.vue' +import Preview from './theme_preview.vue' // List of color values used in v1 const v1OnlyNames = [ @@ -62,6 +70,7 @@ const colorConvert = (color) => { export default { data () { return { + themeV3Preview: [], themeImporter: newImporter({ validator: this.importValidator, onImport: this.onImport, @@ -78,10 +87,7 @@ export default { tempImportFile: undefined, engineVersion: 0, - previewShadows: {}, - previewColors: {}, - previewRadii: {}, - previewFonts: {}, + previewTheme: {}, shadowsInvalid: true, colorsInvalid: true, @@ -232,13 +238,6 @@ export default { chatMessage: this.chatMessageRadiusLocal } }, - preview () { - return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts) - }, - previewTheme () { - if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} } - return this.preview.theme - }, // This needs optimization maybe previewContrast () { try { @@ -306,14 +305,6 @@ export default { return {} } }, - previewRules () { - if (!this.preview.rules) return '' - return [ - ...Object.values(this.preview.rules), - 'color: var(--text)', - 'font-family: var(--interfaceFont, sans-serif)' - ].join(';') - }, shadowsAvailable () { return Object.keys(DEFAULT_SHADOWS).sort() }, @@ -511,16 +502,15 @@ export default { } }, setCustomTheme () { - this.$store.dispatch('setOption', { - name: 'customTheme', - value: { + this.$store.dispatch('setThemeV2', { + customTheme: { + ignore: true, + themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, ...this.previewTheme - } - }) - this.$store.dispatch('setOption', { - name: 'customThemeSource', - value: { + }, + customThemeSource: { + themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, shadows: this.shadowsLocal, fonts: this.fontsLocal, @@ -530,16 +520,24 @@ export default { } }) }, - updatePreviewColorsAndShadows () { - this.previewColors = generateColors({ + updatePreviewColors () { + const result = generateColors({ opacity: this.currentOpacity, colors: this.currentColors }) - this.previewShadows = generateShadows( - { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion }, - this.previewColors.theme.colors, - this.previewColors.mod - ) + this.previewTheme.colors = result.theme.colors + this.previewTheme.opacity = result.theme.opacity + }, + updatePreviewShadows () { + this.previewTheme.shadows = generateShadows( + { + shadows: this.shadowsLocal, + opacity: this.previewTheme.opacity, + themeEngineVersion: this.engineVersion + }, + this.previewTheme.colors, + relativeLuminance(this.previewTheme.colors.bg) < 0.5 ? 1 : -1 + ).theme.shadows }, importTheme () { this.themeImporter.importData() }, exportTheme () { this.themeExporter.exportData() }, @@ -608,7 +606,7 @@ export default { normalizeLocalState (theme, version = 0, source, forceSource = false) { let input if (typeof source !== 'undefined') { - if (forceSource || source.themeEngineVersion === CURRENT_VERSION) { + if (forceSource || source?.themeEngineVersion === CURRENT_VERSION) { input = source version = source.themeEngineVersion } else { @@ -690,6 +688,8 @@ export default { } else { this.shadowsLocal = shadows } + this.updatePreviewColors() + this.updatePreviewShadows() this.shadowSelected = this.shadowsAvailable[0] } @@ -697,12 +697,25 @@ export default { this.clearFonts() this.fontsLocal = fonts } + }, + updateTheme3Preview () { + const theme2 = convertTheme2To3(this.previewTheme) + const theme3 = init({ + inputRuleset: theme2, + ultimateBackgroundColor: '#000000', + liteMode: true + }) + + this.themeV3Preview = getScopedVersion( + getCssRules(theme3.eager), + '#theme-preview' + ).join('\n') } }, watch: { currentRadii () { try { - this.previewRadii = generateRadii({ radii: this.currentRadii }) + this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme.radii this.radiiInvalid = false } catch (e) { this.radiiInvalid = true @@ -711,9 +724,8 @@ export default { }, shadowsLocal: { handler () { - if (Object.getOwnPropertyNames(this.previewColors).length === 1) return try { - this.updatePreviewColorsAndShadows() + this.updatePreviewShadows() this.shadowsInvalid = false } catch (e) { this.shadowsInvalid = true @@ -725,7 +737,7 @@ export default { fontsLocal: { handler () { try { - this.previewFonts = generateFonts({ fonts: this.fontsLocal }) + this.previewTheme.fonts = generateFonts({ fonts: this.fontsLocal }).theme.fonts this.fontsInvalid = false } catch (e) { this.fontsInvalid = true @@ -736,18 +748,16 @@ export default { }, currentColors () { try { - this.updatePreviewColorsAndShadows() + this.updatePreviewColors() this.colorsInvalid = false - this.shadowsInvalid = false } catch (e) { this.colorsInvalid = true - this.shadowsInvalid = true console.warn(e) } }, currentOpacity () { try { - this.updatePreviewColorsAndShadows() + this.updatePreviewColors() } catch (e) { console.warn(e) } @@ -755,7 +765,6 @@ export default { selected () { this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => { if (Array.isArray(s)) { - console.log(s[0] === this.selected, this.selected) return s[0] === this.selected } else { return s.name === this.selected diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss index 9935c2e7..84933fb8 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -1,6 +1,9 @@ -@import "src/variables"; - .theme-tab { + .deprecation-warning { + padding: 0.5em; + margin: 2em; + } + padding-bottom: 2em; .preset-switcher { @@ -12,6 +15,10 @@ margin-right: 0.25em; } + .btn-group .btn { + margin: 0; + } + .style-control { display: flex; align-items: baseline; @@ -159,111 +166,6 @@ } } - .preview-container { - border-top: 1px dashed; - border-bottom: 1px dashed; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - margin: 1em 0; - padding: 1em; - background-color: var(--wallpaper); - background-image: var(--body-background-image); - background-size: cover; - background-position: 50% 50%; - - .dummy { - .post { - font-family: var(--postFont); - display: flex; - - .content { - flex: 1; - - h4 { - margin-bottom: 0.25em; - } - - .icons { - margin-top: 0.5em; - display: flex; - - i { - margin-right: 1em; - } - } - } - } - - .after-post { - margin-top: 1em; - display: flex; - align-items: center; - } - - .avatar, - .avatar-alt { - background: - linear-gradient( - 135deg, - #b8e1fc 0%, - #a9d2f3 10%, - #90bae4 25%, - #90bcea 37%, - #90bff0 50%, - #6ba8e5 51%, - #a2daf5 83%, - #bdf3fd 100% - ); - color: black; - font-family: sans-serif; - text-align: center; - margin-right: 1em; - } - - .avatar-alt { - flex: 0 auto; - margin-left: 28px; - font-size: 12px; - min-width: 20px; - min-height: 20px; - line-height: 20px; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); - } - - .avatar { - flex: 0 auto; - width: 48px; - height: 48px; - font-size: 14px; - line-height: 48px; - } - - .actions { - display: flex; - align-items: baseline; - - .checkbox { - display: inline-flex; - align-items: baseline; - margin-right: 1em; - flex: 1; - } - } - - .separator { - margin: 1em; - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - } - - .btn { - min-width: 3em; - } - } - } - .radius-item { flex-basis: auto; } @@ -296,7 +198,7 @@ border: 0; box-shadow: none; background: transparent; - color: var(--faint, $fallback--faint); + color: var(--textFaint); align-self: stretch; } @@ -316,10 +218,6 @@ max-width: 50em; } - .theme-preview-content { - padding: 20px; - } - .theme-warning { display: flex; align-items: baseline; diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue index ff2fece9..4498c143 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -1,5 +1,8 @@ <template> <div class="theme-tab"> + <div class="alert warning deprecation-warning"> + {{ $t("settings.style.themes2_outdated") }} + </div> <div class="presets-container"> <div class="save-load"> <div @@ -120,7 +123,19 @@ </div> </div> - <preview :style="previewRules" /> + <!-- eslint-disable vue/no-v-text-v-html-on-component --> + <component :is="'style'" v-html="themeV3Preview"/> + <!-- eslint-enable vue/no-v-text-v-html-on-component --> + <preview id="theme-preview"/> + + <div> + <button + class="btn button-default" + @click="updateTheme3Preview" + > + {{ $t("settings.style.update_preview") }} + </button> + </div> <keep-alive> <tab-switcher key="style-tweak"> @@ -156,7 +171,7 @@ <OpacityInput v-model="bgOpacityLocal" name="bgOpacity" - :fallback="previewTheme.opacity.bg" + :fallback="previewTheme.opacity?.bg" /> <ColorInput v-model="textColorLocal" @@ -167,14 +182,14 @@ <ColorInput v-model="accentColorLocal" name="accentColor" - :fallback="previewTheme.colors.link" + :fallback="previewTheme.colors?.link" :label="$t('settings.accent')" :show-optional-tickbox="typeof linkColorLocal !== 'undefined'" /> <ColorInput v-model="linkColorLocal" name="linkColor" - :fallback="previewTheme.colors.accent" + :fallback="previewTheme.colors?.accent" :label="$t('settings.links')" :show-optional-tickbox="typeof accentColorLocal !== 'undefined'" /> @@ -190,13 +205,13 @@ v-model="fgTextColorLocal" name="fgTextColor" :label="$t('settings.text')" - :fallback="previewTheme.colors.fgText" + :fallback="previewTheme.colors?.fgText" /> <ColorInput v-model="fgLinkColorLocal" name="fgLinkColor" :label="$t('settings.links')" - :fallback="previewTheme.colors.fgLink" + :fallback="previewTheme.colors?.fgLink" /> <p>{{ $t('settings.style.common_colors.foreground_hint') }}</p> </div> @@ -256,14 +271,14 @@ <ColorInput v-model="postLinkColorLocal" name="postLinkColor" - :fallback="previewTheme.colors.accent" + :fallback="previewTheme.colors?.accent" :label="$t('settings.links')" /> <ContrastRatio :contrast="previewContrast.postLink" /> <ColorInput v-model="postGreentextColorLocal" name="postGreentextColor" - :fallback="previewTheme.colors.cGreen" + :fallback="previewTheme.colors?.cGreen" :label="$t('settings.greentext')" /> <ContrastRatio :contrast="previewContrast.postGreentext" /> @@ -272,13 +287,13 @@ v-model="alertErrorColorLocal" name="alertError" :label="$t('settings.style.advanced_colors.alert_error')" - :fallback="previewTheme.colors.alertError" + :fallback="previewTheme.colors?.alertError" /> <ColorInput v-model="alertErrorTextColorLocal" name="alertErrorText" :label="$t('settings.text')" - :fallback="previewTheme.colors.alertErrorText" + :fallback="previewTheme.colors?.alertErrorText" /> <ContrastRatio :contrast="previewContrast.alertErrorText" @@ -288,13 +303,13 @@ v-model="alertWarningColorLocal" name="alertWarning" :label="$t('settings.style.advanced_colors.alert_warning')" - :fallback="previewTheme.colors.alertWarning" + :fallback="previewTheme.colors?.alertWarning" /> <ColorInput v-model="alertWarningTextColorLocal" name="alertWarningText" :label="$t('settings.text')" - :fallback="previewTheme.colors.alertWarningText" + :fallback="previewTheme.colors?.alertWarningText" /> <ContrastRatio :contrast="previewContrast.alertWarningText" @@ -304,13 +319,13 @@ v-model="alertNeutralColorLocal" name="alertNeutral" :label="$t('settings.style.advanced_colors.alert_neutral')" - :fallback="previewTheme.colors.alertNeutral" + :fallback="previewTheme.colors?.alertNeutral" /> <ColorInput v-model="alertNeutralTextColorLocal" name="alertNeutralText" :label="$t('settings.text')" - :fallback="previewTheme.colors.alertNeutralText" + :fallback="previewTheme.colors?.alertNeutralText" /> <ContrastRatio :contrast="previewContrast.alertNeutralText" @@ -319,7 +334,7 @@ <OpacityInput v-model="alertOpacityLocal" name="alertOpacity" - :fallback="previewTheme.opacity.alert" + :fallback="previewTheme.opacity?.alert" /> </div> <div class="color-item"> @@ -328,13 +343,13 @@ v-model="badgeNotificationColorLocal" name="badgeNotification" :label="$t('settings.style.advanced_colors.badge_notification')" - :fallback="previewTheme.colors.badgeNotification" + :fallback="previewTheme.colors?.badgeNotification" /> <ColorInput v-model="badgeNotificationTextColorLocal" name="badgeNotificationText" :label="$t('settings.text')" - :fallback="previewTheme.colors.badgeNotificationText" + :fallback="previewTheme.colors?.badgeNotificationText" /> <ContrastRatio :contrast="previewContrast.badgeNotificationText" @@ -346,19 +361,19 @@ <ColorInput v-model="panelColorLocal" name="panelColor" - :fallback="previewTheme.colors.panel" + :fallback="previewTheme.colors?.panel" :label="$t('settings.background')" /> <OpacityInput v-model="panelOpacityLocal" name="panelOpacity" - :fallback="previewTheme.opacity.panel" + :fallback="previewTheme.opacity?.panel" :disabled="panelColorLocal === 'transparent'" /> <ColorInput v-model="panelTextColorLocal" name="panelTextColor" - :fallback="previewTheme.colors.panelText" + :fallback="previewTheme.colors?.panelText" :label="$t('settings.text')" /> <ContrastRatio @@ -368,7 +383,7 @@ <ColorInput v-model="panelLinkColorLocal" name="panelLinkColor" - :fallback="previewTheme.colors.panelLink" + :fallback="previewTheme.colors?.panelLink" :label="$t('settings.links')" /> <ContrastRatio @@ -381,20 +396,20 @@ <ColorInput v-model="topBarColorLocal" name="topBarColor" - :fallback="previewTheme.colors.topBar" + :fallback="previewTheme.colors?.topBar" :label="$t('settings.background')" /> <ColorInput v-model="topBarTextColorLocal" name="topBarTextColor" - :fallback="previewTheme.colors.topBarText" + :fallback="previewTheme.colors?.topBarText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.topBarText" /> <ColorInput v-model="topBarLinkColorLocal" name="topBarLinkColor" - :fallback="previewTheme.colors.topBarLink" + :fallback="previewTheme.colors?.topBarLink" :label="$t('settings.links')" /> <ContrastRatio :contrast="previewContrast.topBarLink" /> @@ -404,19 +419,19 @@ <ColorInput v-model="inputColorLocal" name="inputColor" - :fallback="previewTheme.colors.input" + :fallback="previewTheme.colors?.input" :label="$t('settings.background')" /> <OpacityInput v-model="inputOpacityLocal" name="inputOpacity" - :fallback="previewTheme.opacity.input" + :fallback="previewTheme.opacity?.input" :disabled="inputColorLocal === 'transparent'" /> <ColorInput v-model="inputTextColorLocal" name="inputTextColor" - :fallback="previewTheme.colors.inputText" + :fallback="previewTheme.colors?.inputText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.inputText" /> @@ -426,33 +441,33 @@ <ColorInput v-model="btnColorLocal" name="btnColor" - :fallback="previewTheme.colors.btn" + :fallback="previewTheme.colors?.btn" :label="$t('settings.background')" /> <OpacityInput v-model="btnOpacityLocal" name="btnOpacity" - :fallback="previewTheme.opacity.btn" + :fallback="previewTheme.opacity?.btn" :disabled="btnColorLocal === 'transparent'" /> <ColorInput v-model="btnTextColorLocal" name="btnTextColor" - :fallback="previewTheme.colors.btnText" + :fallback="previewTheme.colors?.btnText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.btnText" /> <ColorInput v-model="btnPanelTextColorLocal" name="btnPanelTextColor" - :fallback="previewTheme.colors.btnPanelText" + :fallback="previewTheme.colors?.btnPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ContrastRatio :contrast="previewContrast.btnPanelText" /> <ColorInput v-model="btnTopBarTextColorLocal" name="btnTopBarTextColor" - :fallback="previewTheme.colors.btnTopBarText" + :fallback="previewTheme.colors?.btnTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <ContrastRatio :contrast="previewContrast.btnTopBarText" /> @@ -460,27 +475,27 @@ <ColorInput v-model="btnPressedColorLocal" name="btnPressedColor" - :fallback="previewTheme.colors.btnPressed" + :fallback="previewTheme.colors?.btnPressed" :label="$t('settings.background')" /> <ColorInput v-model="btnPressedTextColorLocal" name="btnPressedTextColor" - :fallback="previewTheme.colors.btnPressedText" + :fallback="previewTheme.colors?.btnPressedText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.btnPressedText" /> <ColorInput v-model="btnPressedPanelTextColorLocal" name="btnPressedPanelTextColor" - :fallback="previewTheme.colors.btnPressedPanelText" + :fallback="previewTheme.colors?.btnPressedPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ContrastRatio :contrast="previewContrast.btnPressedPanelText" /> <ColorInput v-model="btnPressedTopBarTextColorLocal" name="btnPressedTopBarTextColor" - :fallback="previewTheme.colors.btnPressedTopBarText" + :fallback="previewTheme.colors?.btnPressedTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <ContrastRatio :contrast="previewContrast.btnPressedTopBarText" /> @@ -488,52 +503,52 @@ <ColorInput v-model="btnDisabledColorLocal" name="btnDisabledColor" - :fallback="previewTheme.colors.btnDisabled" + :fallback="previewTheme.colors?.btnDisabled" :label="$t('settings.background')" /> <ColorInput v-model="btnDisabledTextColorLocal" name="btnDisabledTextColor" - :fallback="previewTheme.colors.btnDisabledText" + :fallback="previewTheme.colors?.btnDisabledText" :label="$t('settings.text')" /> <ColorInput v-model="btnDisabledPanelTextColorLocal" name="btnDisabledPanelTextColor" - :fallback="previewTheme.colors.btnDisabledPanelText" + :fallback="previewTheme.colors?.btnDisabledPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ColorInput v-model="btnDisabledTopBarTextColorLocal" name="btnDisabledTopBarTextColor" - :fallback="previewTheme.colors.btnDisabledTopBarText" + :fallback="previewTheme.colors?.btnDisabledTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <h5>{{ $t('settings.style.advanced_colors.toggled') }}</h5> <ColorInput v-model="btnToggledColorLocal" name="btnToggledColor" - :fallback="previewTheme.colors.btnToggled" + :fallback="previewTheme.colors?.btnToggled" :label="$t('settings.background')" /> <ColorInput v-model="btnToggledTextColorLocal" name="btnToggledTextColor" - :fallback="previewTheme.colors.btnToggledText" + :fallback="previewTheme.colors?.btnToggledText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.btnToggledText" /> <ColorInput v-model="btnToggledPanelTextColorLocal" name="btnToggledPanelTextColor" - :fallback="previewTheme.colors.btnToggledPanelText" + :fallback="previewTheme.colors?.btnToggledPanelText" :label="$t('settings.style.advanced_colors.panel_header')" /> <ContrastRatio :contrast="previewContrast.btnToggledPanelText" /> <ColorInput v-model="btnToggledTopBarTextColorLocal" name="btnToggledTopBarTextColor" - :fallback="previewTheme.colors.btnToggledTopBarText" + :fallback="previewTheme.colors?.btnToggledTopBarText" :label="$t('settings.style.advanced_colors.top_bar')" /> <ContrastRatio :contrast="previewContrast.btnToggledTopBarText" /> @@ -543,20 +558,20 @@ <ColorInput v-model="tabColorLocal" name="tabColor" - :fallback="previewTheme.colors.tab" + :fallback="previewTheme.colors?.tab" :label="$t('settings.background')" /> <ColorInput v-model="tabTextColorLocal" name="tabTextColor" - :fallback="previewTheme.colors.tabText" + :fallback="previewTheme.colors?.tabText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.tabText" /> <ColorInput v-model="tabActiveTextColorLocal" name="tabActiveTextColor" - :fallback="previewTheme.colors.tabActiveText" + :fallback="previewTheme.colors?.tabActiveText" :label="$t('settings.text')" /> <ContrastRatio :contrast="previewContrast.tabActiveText" /> @@ -566,13 +581,13 @@ <ColorInput v-model="borderColorLocal" name="borderColor" - :fallback="previewTheme.colors.border" + :fallback="previewTheme.colors?.border" :label="$t('settings.style.common.color')" /> <OpacityInput v-model="borderOpacityLocal" name="borderOpacity" - :fallback="previewTheme.opacity.border" + :fallback="previewTheme.opacity?.border" :disabled="borderColorLocal === 'transparent'" /> </div> @@ -581,25 +596,25 @@ <ColorInput v-model="faintColorLocal" name="faintColor" - :fallback="previewTheme.colors.faint" + :fallback="previewTheme.colors?.faint" :label="$t('settings.text')" /> <ColorInput v-model="faintLinkColorLocal" name="faintLinkColor" - :fallback="previewTheme.colors.faintLink" + :fallback="previewTheme.colors?.faintLink" :label="$t('settings.links')" /> <ColorInput v-model="panelFaintColorLocal" name="panelFaintColor" - :fallback="previewTheme.colors.panelFaint" + :fallback="previewTheme.colors?.panelFaint" :label="$t('settings.style.advanced_colors.panel_header')" /> <OpacityInput v-model="faintOpacityLocal" name="faintOpacity" - :fallback="previewTheme.opacity.faint" + :fallback="previewTheme.opacity?.faint" /> </div> <div class="color-item"> @@ -608,12 +623,12 @@ v-model="underlayColorLocal" name="underlay" :label="$t('settings.style.advanced_colors.underlay')" - :fallback="previewTheme.colors.underlay" + :fallback="previewTheme.colors?.underlay" /> <OpacityInput v-model="underlayOpacityLocal" name="underlayOpacity" - :fallback="previewTheme.opacity.underlay" + :fallback="previewTheme.opacity?.underlay" :disabled="underlayOpacityLocal === 'transparent'" /> </div> @@ -623,7 +638,7 @@ v-model="wallpaperColorLocal" name="wallpaper" :label="$t('settings.style.advanced_colors.wallpaper')" - :fallback="previewTheme.colors.wallpaper" + :fallback="previewTheme.colors?.wallpaper" /> </div> <div class="color-item"> @@ -632,13 +647,13 @@ v-model="pollColorLocal" name="poll" :label="$t('settings.background')" - :fallback="previewTheme.colors.poll" + :fallback="previewTheme.colors?.poll" /> <ColorInput v-model="pollTextColorLocal" name="pollText" :label="$t('settings.text')" - :fallback="previewTheme.colors.pollText" + :fallback="previewTheme.colors?.pollText" /> </div> <div class="color-item"> @@ -647,7 +662,7 @@ v-model="iconColorLocal" name="icon" :label="$t('settings.style.advanced_colors.icons')" - :fallback="previewTheme.colors.icon" + :fallback="previewTheme.colors?.icon" /> </div> <div class="color-item"> @@ -656,20 +671,20 @@ v-model="highlightColorLocal" name="highlight" :label="$t('settings.background')" - :fallback="previewTheme.colors.highlight" + :fallback="previewTheme.colors?.highlight" /> <ColorInput v-model="highlightTextColorLocal" name="highlightText" :label="$t('settings.text')" - :fallback="previewTheme.colors.highlightText" + :fallback="previewTheme.colors?.highlightText" /> <ContrastRatio :contrast="previewContrast.highlightText" /> <ColorInput v-model="highlightLinkColorLocal" name="highlightLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.highlightLink" + :fallback="previewTheme.colors?.highlightLink" /> <ContrastRatio :contrast="previewContrast.highlightLink" /> </div> @@ -679,26 +694,26 @@ v-model="popoverColorLocal" name="popover" :label="$t('settings.background')" - :fallback="previewTheme.colors.popover" + :fallback="previewTheme.colors?.popover" /> <OpacityInput v-model="popoverOpacityLocal" name="popoverOpacity" - :fallback="previewTheme.opacity.popover" + :fallback="previewTheme.opacity?.popover" :disabled="popoverOpacityLocal === 'transparent'" /> <ColorInput v-model="popoverTextColorLocal" name="popoverText" :label="$t('settings.text')" - :fallback="previewTheme.colors.popoverText" + :fallback="previewTheme.colors?.popoverText" /> <ContrastRatio :contrast="previewContrast.popoverText" /> <ColorInput v-model="popoverLinkColorLocal" name="popoverLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.popoverLink" + :fallback="previewTheme.colors?.popoverLink" /> <ContrastRatio :contrast="previewContrast.popoverLink" /> </div> @@ -708,20 +723,20 @@ v-model="selectedPostColorLocal" name="selectedPost" :label="$t('settings.background')" - :fallback="previewTheme.colors.selectedPost" + :fallback="previewTheme.colors?.selectedPost" /> <ColorInput v-model="selectedPostTextColorLocal" name="selectedPostText" :label="$t('settings.text')" - :fallback="previewTheme.colors.selectedPostText" + :fallback="previewTheme.colors?.selectedPostText" /> <ContrastRatio :contrast="previewContrast.selectedPostText" /> <ColorInput v-model="selectedPostLinkColorLocal" name="selectedPostLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.selectedPostLink" + :fallback="previewTheme.colors?.selectedPostLink" /> <ContrastRatio :contrast="previewContrast.selectedPostLink" /> </div> @@ -731,20 +746,20 @@ v-model="selectedMenuColorLocal" name="selectedMenu" :label="$t('settings.background')" - :fallback="previewTheme.colors.selectedMenu" + :fallback="previewTheme.colors?.selectedMenu" /> <ColorInput v-model="selectedMenuTextColorLocal" name="selectedMenuText" :label="$t('settings.text')" - :fallback="previewTheme.colors.selectedMenuText" + :fallback="previewTheme.colors?.selectedMenuText" /> <ContrastRatio :contrast="previewContrast.selectedMenuText" /> <ColorInput v-model="selectedMenuLinkColorLocal" name="selectedMenuLink" :label="$t('settings.links')" - :fallback="previewTheme.colors.selectedMenuLink" + :fallback="previewTheme.colors?.selectedMenuLink" /> <ContrastRatio :contrast="previewContrast.selectedMenuLink" /> </div> @@ -753,57 +768,57 @@ <ColorInput v-model="chatBgColorLocal" name="chatBgColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.background')" /> <h5>{{ $t('settings.style.advanced_colors.chat.incoming') }}</h5> <ColorInput v-model="chatMessageIncomingBgColorLocal" name="chatMessageIncomingBgColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.background')" /> <ColorInput v-model="chatMessageIncomingTextColorLocal" name="chatMessageIncomingTextColor" - :fallback="previewTheme.colors.text" + :fallback="previewTheme.colors?.text" :label="$t('settings.text')" /> <ColorInput v-model="chatMessageIncomingLinkColorLocal" name="chatMessageIncomingLinkColor" - :fallback="previewTheme.colors.link" + :fallback="previewTheme.colors?.link" :label="$t('settings.links')" /> <ColorInput v-model="chatMessageIncomingBorderColorLocal" name="chatMessageIncomingBorderLinkColor" - :fallback="previewTheme.colors.fg" + :fallback="previewTheme.colors?.fg" :label="$t('settings.style.advanced_colors.chat.border')" /> <h5>{{ $t('settings.style.advanced_colors.chat.outgoing') }}</h5> <ColorInput v-model="chatMessageOutgoingBgColorLocal" name="chatMessageOutgoingBgColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.background')" /> <ColorInput v-model="chatMessageOutgoingTextColorLocal" name="chatMessageOutgoingTextColor" - :fallback="previewTheme.colors.text" + :fallback="previewTheme.colors?.text" :label="$t('settings.text')" /> <ColorInput v-model="chatMessageOutgoingLinkColorLocal" name="chatMessageOutgoingLinkColor" - :fallback="previewTheme.colors.link" + :fallback="previewTheme.colors?.link" :label="$t('settings.links')" /> <ColorInput v-model="chatMessageOutgoingBorderColorLocal" name="chatMessageOutgoingBorderLinkColor" - :fallback="previewTheme.colors.bg" + :fallback="previewTheme.colors?.bg" :label="$t('settings.style.advanced_colors.chat.border')" /> </div> @@ -826,7 +841,7 @@ v-model="btnRadiusLocal" name="btnRadius" :label="$t('settings.btnRadius')" - :fallback="previewTheme.radii.btn" + :fallback="previewTheme.radii?.btn" max="16" hard-min="0" /> @@ -834,7 +849,7 @@ v-model="inputRadiusLocal" name="inputRadius" :label="$t('settings.inputRadius')" - :fallback="previewTheme.radii.input" + :fallback="previewTheme.radii?.input" max="9" hard-min="0" /> @@ -842,7 +857,7 @@ v-model="checkboxRadiusLocal" name="checkboxRadius" :label="$t('settings.checkboxRadius')" - :fallback="previewTheme.radii.checkbox" + :fallback="previewTheme.radii?.checkbox" max="16" hard-min="0" /> @@ -850,7 +865,7 @@ v-model="panelRadiusLocal" name="panelRadius" :label="$t('settings.panelRadius')" - :fallback="previewTheme.radii.panel" + :fallback="previewTheme.radii?.panel" max="50" hard-min="0" /> @@ -858,7 +873,7 @@ v-model="avatarRadiusLocal" name="avatarRadius" :label="$t('settings.avatarRadius')" - :fallback="previewTheme.radii.avatar" + :fallback="previewTheme.radii?.avatar" max="28" hard-min="0" /> @@ -866,7 +881,7 @@ v-model="avatarAltRadiusLocal" name="avatarAltRadius" :label="$t('settings.avatarAltRadius')" - :fallback="previewTheme.radii.avatarAlt" + :fallback="previewTheme.radii?.avatarAlt" max="28" hard-min="0" /> @@ -874,7 +889,7 @@ v-model="attachmentRadiusLocal" name="attachmentRadius" :label="$t('settings.attachmentRadius')" - :fallback="previewTheme.radii.attachment" + :fallback="previewTheme.radii?.attachment" max="50" hard-min="0" /> @@ -882,7 +897,7 @@ v-model="tooltipRadiusLocal" name="tooltipRadius" :label="$t('settings.tooltipRadius')" - :fallback="previewTheme.radii.tooltip" + :fallback="previewTheme.radii?.tooltip" max="50" hard-min="0" /> @@ -890,7 +905,7 @@ v-model="chatMessageRadiusLocal" name="chatMessageRadius" :label="$t('settings.chatMessageRadius')" - :fallback="previewTheme.radii.chatMessage || 2" + :fallback="previewTheme.radii?.chatMessage || 2" max="50" hard-min="0" /> @@ -996,26 +1011,26 @@ v-model="fontsLocal.interface" name="ui" :label="$t('settings.style.fonts.components.interface')" - :fallback="previewTheme.fonts.interface" + :fallback="previewTheme.fonts?.interface" no-inherit="1" /> <FontControl v-model="fontsLocal.input" name="input" :label="$t('settings.style.fonts.components.input')" - :fallback="previewTheme.fonts.input" + :fallback="previewTheme.fonts?.input" /> <FontControl v-model="fontsLocal.post" name="post" :label="$t('settings.style.fonts.components.post')" - :fallback="previewTheme.fonts.post" + :fallback="previewTheme.fonts?.post" /> <FontControl v-model="fontsLocal.postCode" name="postCode" :label="$t('settings.style.fonts.components.postCode')" - :fallback="previewTheme.fonts.postCode" + :fallback="previewTheme.fonts?.postCode" /> </div> </tab-switcher> diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js index a1d1012b..f8e12dbf 100644 --- a/src/components/shadow_control/shadow_control.js +++ b/src/components/shadow_control/shadow_control.js @@ -1,7 +1,7 @@ import ColorInput from '../color_input/color_input.vue' import OpacityInput from '../opacity_input/opacity_input.vue' import Select from '../select/select.vue' -import { getCssShadow } from '../../services/style_setter/style_setter.js' +import { getCssShadow } from '../../services/theme_data/theme_data.service.js' import { hex2rgb } from '../../services/color_convert/color_convert.js' import { library } from '@fortawesome/fontawesome-svg-core' import { diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue index 7546535d..c3b956cd 100644 --- a/src/components/shadow_control/shadow_control.vue +++ b/src/components/shadow_control/shadow_control.vue @@ -11,14 +11,14 @@ <input v-model="selected.y" :disabled="!present" - class="input-number" + class="input input-number" type="number" > <div class="wrap"> <input v-model="selected.y" :disabled="!present" - class="input-range" + class="input input-range" type="range" max="20" min="-20" @@ -38,14 +38,14 @@ <input v-model="selected.x" :disabled="!present" - class="input-number" + class="input input-number" type="number" > <div class="wrap"> <input v-model="selected.x" :disabled="!present" - class="input-range" + class="input input-range" type="range" max="20" min="-20" @@ -129,12 +129,13 @@ v-model="selected.inset" :disabled="!present" name="inset" - class="input-inset" + class="input -checkbox input-inset visible-for-screenreader-only" type="checkbox" > <label class="checkbox-label" for="inset" + :aria-hidden="true" /> </div> <div @@ -152,7 +153,7 @@ v-model="selected.blur" :disabled="!present" name="blur" - class="input-range" + class="input input-range" type="range" max="20" min="0" @@ -160,7 +161,7 @@ <input v-model="selected.blur" :disabled="!present" - class="input-number" + class="input input-number" type="number" min="0" > @@ -180,7 +181,7 @@ v-model="selected.spread" :disabled="!present" name="spread" - class="input-range" + class="input input-range" type="range" max="20" min="-20" @@ -188,7 +189,7 @@ <input v-model="selected.spread" :disabled="!present" - class="input-number" + class="input input-number" type="number" > </div> @@ -218,8 +219,6 @@ <script src="./shadow_control.js"></script> <style lang="scss"> -@import "../../variables"; - .shadow-control { display: flex; flex-wrap: wrap; @@ -236,8 +235,6 @@ display: flex; flex-wrap: wrap; - $side: 15em; - input[type="number"] { width: 5em; min-width: 2em; @@ -260,7 +257,7 @@ .x-shift-control .wrap, input[type="range"] { margin: 0; - width: $side; + width: 15em; height: 2em; } @@ -270,7 +267,7 @@ .wrap { width: 2em; - height: $side; + height: 15em; } input[type="range"] { @@ -292,16 +289,12 @@ linear-gradient(-45deg, transparent 75%, #666 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0; - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border-radius: var(--roundness); .preview-block { width: 33%; height: 33%; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); } } } diff --git a/src/components/shout_panel/shout_panel.vue b/src/components/shout_panel/shout_panel.vue index a7013469..9113211e 100644 --- a/src/components/shout_panel/shout_panel.vue +++ b/src/components/shout_panel/shout_panel.vue @@ -5,7 +5,7 @@ > <div class="panel panel-default"> <div - class="panel-heading timeline-heading" + class="panel-heading" :class="{ 'shout-heading': floating }" @click.stop.prevent="togglePanel" > @@ -18,7 +18,7 @@ /> </div> </div> - <div class="shout-window"> + <div class="panel-body shout-window"> <div v-for="message in messages" :key="message.id" @@ -41,10 +41,10 @@ </div> </div> </div> - <div class="shout-input"> + <div class="panel-body shout-input"> <textarea v-model="currentMessage" - class="shout-input-textarea" + class="shout-input-textarea input" rows="1" @keyup.enter="submit(currentMessage)" /> @@ -75,8 +75,6 @@ <script src="./shout_panel.js"></script> <style lang="scss"> -@import "../../variables"; - .floating-shout { position: fixed; bottom: 0.5em; @@ -97,8 +95,7 @@ cursor: pointer; .icon { - color: $fallback--text; - color: var(--panelText, $fallback--text); + color: var(--text); margin-right: 0.5em; } @@ -128,8 +125,7 @@ img { height: 24px; width: 24px; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); margin-right: 0.5em; margin-top: 0.25em; } diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 27019577..81c5a612 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -115,7 +115,10 @@ const SideDrawer = { GestureService.updateSwipe(e, this.closeGesture) }, openSettingsModal () { - this.$store.dispatch('openSettingsModal') + this.$store.dispatch('openSettingsModal', 'user') + }, + openAdminModal () { + this.$store.dispatch('openSettingsModal', 'admin') } } } diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 994ac953..608b5608 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -1,6 +1,6 @@ <template> <div - class="side-drawer-container" + class="side-drawer-container mobile-drawer" :class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }" > <div @@ -35,7 +35,10 @@ v-if="!currentUser" @click="toggleDrawer" > - <router-link :to="{ name: 'login' }"> + <router-link + :to="{ name: 'login' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -47,7 +50,10 @@ v-if="currentUser || !privateMode" @click="toggleDrawer" > - <router-link :to="timelinesRoute"> + <router-link + :to="timelinesRoute" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -59,7 +65,10 @@ v-if="currentUser" @click="toggleDrawer" > - <router-link :to="{ name: 'lists' }"> + <router-link + :to="{ name: 'lists' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -74,6 +83,7 @@ <router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }" style="position: relative;" + class="menu-item" > <FAIcon fixed-width @@ -82,7 +92,7 @@ /> {{ $t("nav.chats") }} <span v-if="unreadChatCount" - class="badge badge-notification" + class="badge -notification" > {{ unreadChatCount }} </span> @@ -91,7 +101,10 @@ </ul> <ul v-if="currentUser"> <li @click="toggleDrawer"> - <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> + <router-link + :to="{ name: 'interactions', params: { username: currentUser.screen_name } }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -103,7 +116,10 @@ v-if="currentUser.locked" @click="toggleDrawer" > - <router-link to="/friend-requests"> + <router-link + to="/friend-requests" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -111,7 +127,7 @@ /> {{ $t("nav.friend_requests") }} <span v-if="followRequestCount > 0" - class="badge badge-notification" + class="badge -notification" > {{ followRequestCount }} </span> @@ -121,7 +137,10 @@ v-if="shout" @click="toggleDrawer" > - <router-link :to="{ name: 'shout-panel' }"> + <router-link + :to="{ name: 'shout-panel' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -135,7 +154,10 @@ v-if="currentUser || !privateMode" @click="toggleDrawer" > - <router-link :to="{ name: 'search' }"> + <router-link + :to="{ name: 'search' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -147,7 +169,10 @@ v-if="currentUser && suggestionsEnabled" @click="toggleDrawer" > - <router-link :to="{ name: 'who-to-follow' }"> + <router-link + :to="{ name: 'who-to-follow' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -157,7 +182,7 @@ </li> <li @click="toggleDrawer"> <button - class="button-unstyled -link -fullwidth" + class="menu-item" @click="openSettingsModal" > <FAIcon @@ -168,7 +193,10 @@ </button> </li> <li @click="toggleDrawer"> - <router-link :to="{ name: 'about'}"> + <router-link + :to="{ name: 'about'}" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -180,16 +208,16 @@ v-if="currentUser && currentUser.role === 'admin'" @click="toggleDrawer" > - <a - href="/pleroma/admin/#/login-pleroma" - target="_blank" + <button + class="menu-item" + @click.stop="openAdminModal" > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" icon="tachometer-alt" /> {{ $t("nav.administration") }} - </a> + </button> </li> <li v-if="currentUser && supportsAnnouncements" @@ -197,6 +225,7 @@ > <router-link :to="{ name: 'announcements' }" + class="menu-item" > <FAIcon fixed-width @@ -205,7 +234,7 @@ /> {{ $t("nav.announcements") }} <span v-if="unreadAnnouncementCount" - class="badge badge-notification" + class="badge -notification" > {{ unreadAnnouncementCount }} </span> @@ -215,7 +244,10 @@ v-if="currentUser" @click="toggleDrawer" > - <router-link :to="{ name: 'edit-navigation' }"> + <router-link + :to="{ name: 'edit-navigation' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -228,7 +260,7 @@ @click="toggleDrawer" > <button - class="button-unstyled -link -fullwidth" + class="menu-item" @click="doLogout" > <FAIcon @@ -251,8 +283,6 @@ <script src="./side_drawer.js"></script> <style lang="scss"> -@import "../../variables"; - .side-drawer-container { position: fixed; z-index: var(--ZI_navbar); @@ -305,17 +335,8 @@ width: 80%; max-width: 20em; flex: 0 0 80%; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); - background-color: $fallback--bg; - background-color: var(--popover, $fallback--bg); - color: $fallback--link; - color: var(--popoverText, $fallback--link); - - --faint: var(--popoverFaintText, $fallback--faint); - --faintLink: var(--popoverFaintLink, $fallback--faint); - --lightText: var(--popoverLightText, $fallback--lightText); - --icon: var(--popoverIcon, $fallback--icon); + box-shadow: var(--shadow); + background-color: var(--background); .badge { margin-left: 10px; @@ -362,8 +383,7 @@ margin: 0; padding: 0; border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); } .side-drawer ul:last-child { @@ -380,18 +400,6 @@ height: 3em; line-height: 3em; padding: 0 0.7em; - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenuPopover, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuPopoverText, $fallback--text); - - --faint: var(--selectedMenuPopoverFaintText, $fallback--faint); - --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); - --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - } } } </style> diff --git a/src/components/status/post.style.js b/src/components/status/post.style.js new file mode 100644 index 00000000..d0038424 --- /dev/null +++ b/src/components/status/post.style.js @@ -0,0 +1,42 @@ +export default { + name: 'Post', + selector: '.Status', + states: { + selected: '.-focused' + }, + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'RichContent', + 'Input', + 'Avatar', + 'Attachment', + 'PollGraph' + ], + validInnerComponentsLite: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'ButtonUnstyled', + 'RichContent', + 'Avatar' + ], + defaultRules: [ + { + directives: { + background: '--bg' + } + }, + { + state: ['selected'], + directives: { + background: '--inheritedBackground, 10' + } + } + ] +} diff --git a/src/components/status/status.js b/src/components/status/status.js index 9a9bca7a..bf4e4275 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -39,7 +39,8 @@ import { faThumbtack, faChevronUp, faChevronDown, - faAngleDoubleRight + faAngleDoubleRight, + faPlay } from '@fortawesome/free-solid-svg-icons' library.add( @@ -59,7 +60,8 @@ library.add( faThumbtack, faChevronUp, faChevronDown, - faAngleDoubleRight + faAngleDoubleRight, + faPlay ) const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1) @@ -133,6 +135,7 @@ const Status = { 'showPinned', 'inProfile', 'profileUserId', + 'inQuote', 'simpleTree', 'controlledThreadDisplayStatus', @@ -151,6 +154,7 @@ const Status = { 'controlledSetMediaPlaying', 'dive' ], + emits: ['interacted'], data () { return { uncontrolledReplying: false, @@ -159,7 +163,8 @@ const Status = { uncontrolledMediaPlaying: [], suspendable: true, error: null, - headTailLinks: null + headTailLinks: null, + displayQuote: !this.inQuote } }, computed: { @@ -227,17 +232,14 @@ const Status = { muteWordHits () { return muteWordHits(this.status, this.muteWords) }, - rtBotStatus () { - return this.statusoid.user.bot - }, botStatus () { - return this.status.user.bot + return this.status.user.actor_type === 'Service' }, - botIndicator () { - return this.botStatus && !this.hideBotIndication + showActorTypeIndicator () { + return !this.hideBotIndication }, - rtBotIndicator () { - return this.rtBotStatus && !this.hideBotIndication + sensitiveStatus () { + return this.status.nsfw }, mentionsLine () { if (!this.headTailLinks) return [] @@ -266,7 +268,9 @@ const Status = { // Wordfiltered this.muteWordHits.length > 0 || // bot status - (this.muteBotStatuses && this.botStatus && !this.compact) + (this.muteBotStatuses && this.botStatus && !this.compact) || + // sensitive status + (this.muteSensitiveStatuses && this.sensitiveStatus && !this.compact) return !this.unmuted && !this.shouldNotMute && reasonsToMute }, userIsMuted () { @@ -369,9 +373,15 @@ const Status = { hidePostStats () { return this.mergedConfig.hidePostStats }, + shouldDisplayFavsAndRepeats () { + return !this.hidePostStats && this.isFocused && (this.combinedFavsAndRepeatsUsers.length > 0 || this.statusFromGlobalRepository.quotes_count) + }, muteBotStatuses () { return this.mergedConfig.muteBotStatuses }, + muteSensitiveStatuses () { + return this.mergedConfig.muteSensitiveStatuses + }, hideBotIndication () { return this.mergedConfig.hideBotIndication }, @@ -401,6 +411,44 @@ const Status = { }, editingAvailable () { return this.$store.state.instance.editingAvailable + }, + hasVisibleQuote () { + return this.status.quote_url && this.status.quote_visible + }, + hasInvisibleQuote () { + return this.status.quote_url && !this.status.quote_visible + }, + quotedStatus () { + return this.status.quote_id ? this.$store.state.statuses.allStatusesObject[this.status.quote_id] : undefined + }, + shouldDisplayQuote () { + return this.quotedStatus && this.displayQuote + }, + scrobblePresent () { + if (this.mergedConfig.hideScrobbles) return false + if (!this.status.user.latestScrobble) return false + const value = this.mergedConfig.hideScrobblesAfter.match(/\d+/gs)[0] + const unit = this.mergedConfig.hideScrobblesAfter.match(/\D+/gs)[0] + let multiplier = 60 * 1000 // minutes is smallest unit + switch (unit) { + case 'm': + break + case 'h': + multiplier *= 60 // hour + break + case 'd': + multiplier *= 60 // hour + multiplier *= 24 // day + break + } + const maxAge = Number(value) * multiplier + const createdAt = Date.parse(this.status.user.latestScrobble.created_at) + const age = Date.now() - createdAt + if (age > maxAge) return false + return this.status.user.latestScrobble.artist + }, + scrobble () { + return this.status.user.latestScrobble } }, methods: { @@ -420,9 +468,11 @@ const Status = { this.error = error }, clearError () { + this.$emit('interacted') this.error = undefined }, toggleReplying () { + this.$emit('interacted') controlledOrUncontrolledToggle(this, 'replying') }, gotoOriginal (id) { @@ -469,6 +519,18 @@ const Status = { window.scrollBy(0, rect.bottom - window.innerHeight + 50) } } + }, + toggleDisplayQuote () { + if (this.shouldDisplayQuote) { + this.displayQuote = false + } else if (!this.quotedStatus) { + this.$store.dispatch('fetchStatus', this.status.quote_id) + .then(() => { + this.displayQuote = true + }) + } else { + this.displayQuote = true + } } }, watch: { diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 44812867..63809ff2 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Status { min-width: 0; white-space: normal; @@ -12,24 +10,8 @@ --_still-image-label-visibility: hidden; } - &.-focused { - background-color: $fallback--lightBg; - background-color: var(--selectedPost, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedPostText, $fallback--text); - - --lightText: var(--selectedPostLightText, $fallback--light); - --faint: var(--selectedPostFaintText, $fallback--faint); - --faintLink: var(--selectedPostFaintLink, $fallback--faint); - --postLink: var(--selectedPostPostLink, $fallback--faint); - --postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint); - --icon: var(--selectedPostIcon, $fallback--icon); - } - .gravestone { - padding: var(--status-margin, $status-margin); - color: $fallback--faint; - color: var(--faint, $fallback--faint); + padding: var(--status-margin); display: flex; .deleted-text { @@ -40,7 +22,7 @@ .status-container { display: flex; - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); > * { min-width: 0; @@ -52,7 +34,7 @@ } .pin { - padding: var(--status-margin, $status-margin) var(--status-margin, $status-margin) 0; + padding: var(--status-margin) var(--status-margin) 0; display: flex; align-items: center; justify-content: flex-end; @@ -68,7 +50,7 @@ } .left-side { - margin-right: var(--status-margin, $status-margin); + margin-right: var(--status-margin); } .right-side { @@ -77,7 +59,7 @@ } .usercard { - margin-bottom: var(--status-margin, $status-margin); + margin-bottom: var(--status-margin); } .status-username { @@ -135,11 +117,6 @@ .button-unstyled { padding: 5px; margin: -5px; - - &:hover svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } } .svg-inline--fa { @@ -243,16 +220,15 @@ } .repeat-info { - padding: 0.4em var(--status-margin, $status-margin); + padding: 0.4em var(--status-margin); .repeat-icon { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--cGreen); } } .repeater-avatar { - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); margin-left: 28px; width: 20px; height: 20px; @@ -289,7 +265,7 @@ position: relative; width: 100%; display: flex; - margin-top: var(--status-margin, $status-margin); + margin-top: var(--status-margin); > * { max-width: 4em; @@ -357,7 +333,7 @@ } .favs-repeated-users { - margin-top: var(--status-margin, $status-margin); + margin-top: var(--status-margin); } .stats { @@ -368,10 +344,10 @@ .avatar-row { flex: 1; - overflow: hidden; position: relative; display: flex; align-items: center; + overflow: hidden; &::before { content: ""; @@ -379,16 +355,16 @@ height: 100%; width: 1px; left: 0; - background-color: var(--faint, $fallback--faint); + background-color: var(--textFaint); } } .stat-count { - margin-right: var(--status-margin, $status-margin); + margin-right: var(--status-margin); user-select: none; .stat-title { - color: var(--faint, $fallback--faint); + color: var(--textFaint); font-size: 0.85em; text-transform: uppercase; position: relative; @@ -398,6 +374,7 @@ font-weight: bolder; font-size: 1.1em; line-height: 1em; + color: var(--text); } &:hover .stat-title { @@ -422,4 +399,22 @@ } } } + + .quoted-status { + margin-top: 0.5em; + border: 1px solid var(--border); + border-radius: var(--roundness); + + &.-unavailable-prompt { + padding: 0.5em; + } + } + + .display-quoted-status-button { + margin: 0.5em; + + &-icon { + color: inherit; + } + } } diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 35b15362..61a58cda 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -31,6 +31,12 @@ /> </small> <small + v-if="muteSensitiveStatuses && status.nsfw" + class="mute-thread" + > + {{ $t('status.sensitive_muted') }} + </small> + <small v-if="showReasonMutedThread" class="mute-thread" > @@ -79,7 +85,7 @@ <UserAvatar v-if="retweet" class="left-side repeater-avatar" - :bot="rtBotIndicator" + :show-actor-type-indicator="showActorTypeIndicator" :better-shadow="betterShadow" :user="statusoid.user" /> @@ -133,7 +139,7 @@ > <UserAvatar class="post-avatar" - :bot="botIndicator" + :show-actor-type-indicator="showActorTypeIndicator" :compact="compact" :better-shadow="betterShadow" :user="status.user" @@ -180,7 +186,7 @@ <span class="heading-right"> <router-link - class="timeago faint-link" + class="timeago faint" :to="{ name: 'conversation', params: { id: status.id } }" > <Timeago @@ -250,6 +256,47 @@ </span> </div> <div + v-if="scrobblePresent" + class="status-rich-presence" + > + <a + v-if="scrobble.externalLink" + :href="scrobble.externalLink" + target="_blank" + > + {{ scrobble.artist }} — {{ scrobble.title }} + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="play" + /> + <span class="status-rich-presence-time"> + <Timeago + template-key="time.in_past" + :time="scrobble.created_at" + :auto-update="60" + /> + </span> + </a> + <span v-if="!scrobble.externalLink"> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="music" + /> + {{ scrobble.artist }} — {{ scrobble.title }} + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="play" + /> + <span class="status-rich-presence-time"> + <Timeago + template-key="time.in_past" + :time="scrobble.created_at" + :auto-update="60" + /> + </span> + </span> + </div> + <div v-if="isReply || hasMentionsLine" class="heading-reply-row" > @@ -364,13 +411,52 @@ @parseReady="setHeadTailLinks" /> + <article + v-if="hasVisibleQuote" + class="quoted-status" + > + <button + class="button-unstyled -link display-quoted-status-button" + :aria-expanded="shouldDisplayQuote" + @click="toggleDisplayQuote" + > + {{ shouldDisplayQuote ? $t('status.hide_quote') : $t('status.display_quote') }} + <FAIcon + class="display-quoted-status-button-icon" + :icon="shouldDisplayQuote ? 'chevron-up' : 'chevron-down'" + /> + </button> + <Status + v-if="shouldDisplayQuote" + :statusoid="quotedStatus" + :in-quote="true" + /> + </article> + <p + v-else-if="hasInvisibleQuote" + class="quoted-status -unavailable-prompt" + > + <i18n-t keypath="status.invisible_quote"> + <template #link> + <bdi> + <a + :href="status.quote_url" + target="_blank" + > + {{ status.quote_url }} + </a> + </bdi> + </template> + </i18n-t> + </p> + <div v-if="inConversation && !isPreview && replies && replies.length" class="replies" > <button v-if="showOtherRepliesAsButton && replies.length > 1" - class="button-unstyled -link faint" + class="button-unstyled -link" :title="$tc('status.ancestor_follow', replies.length - 1, { numReplies: replies.length - 1 })" @click.prevent="dive" > @@ -398,7 +484,7 @@ <transition name="fade"> <div - v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0" + v-if="shouldDisplayFavsAndRepeats" class="favs-repeated-users" > <div class="stats"> @@ -426,6 +512,19 @@ </div> </div> </UserListPopover> + <router-link + v-if="statusFromGlobalRepository.quotes_count > 0" + :to="{ name: 'quotes', params: { id: status.id } }" + > + <div + class="stat-count" + > + <a class="stat-title">{{ $t('status.quotes') }}</a> + <div class="stat-number"> + {{ statusFromGlobalRepository.quotes_count }} + </div> + </div> + </router-link> <div class="avatar-row"> <AvatarList :users="combinedFavsAndRepeatsUsers" /> </div> @@ -451,14 +550,17 @@ :visibility="status.visibility" :logged-in="loggedIn" :status="status" + @click="$emit('interacted')" /> <favorite-button :logged-in="loggedIn" :status="status" + @click="$emit('interacted')" /> <ReactButton v-if="loggedIn" :status="status" + @click="$emit('interacted')" /> <extra-buttons :status="status" @@ -476,7 +578,7 @@ <UserAvatar class="post-avatar" :compact="compact" - :bot="botIndicator" + :show-actor-type-indicator="showActorTypeIndicator" /> </div> <div class="right-side"> diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss index 8fd60afc..0a467b4f 100644 --- a/src/components/status_body/status_body.scss +++ b/src/components/status_body/status_body.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .StatusBody { display: flex; flex-direction: column; @@ -14,7 +12,6 @@ & .text, & .summary { - font-family: var(--postFont, sans-serif); white-space: pre-wrap; overflow-wrap: break-word; word-wrap: break-word; @@ -41,7 +38,7 @@ margin-bottom: 0.5em; border-style: solid; border-width: 0 0 1px; - border-color: var(--border, $fallback--border); + border-color: var(--border); flex-grow: 0; &.-tall { @@ -112,15 +109,6 @@ } } - .greentext { - color: $fallback--cGreen; - color: var(--postGreentext, $fallback--cGreen); - } - - .cyantext { - color: var(--postCyantext, $fallback--cBlue); - } - &.-compact { align-items: top; flex-direction: row; diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue index fb356360..f21bcc80 100644 --- a/src/components/status_body/status_body.vue +++ b/src/components/status_body/status_body.vue @@ -11,6 +11,7 @@ > <RichContent class="media-body summary" + :faint="compact" :html="status.summary_raw_html" :emoji="status.emojis" /> @@ -48,6 +49,7 @@ :html="status.raw_html" :emoji="status.emojis" :handle-links="true" + :faint="compact" :greentext="mergedConfig.greentext" :attentions="status.attentions" @parseReady="onParseReady" diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js index 89f0aa51..8d8a91dc 100644 --- a/src/components/status_content/status_content.js +++ b/src/components/status_content/status_content.js @@ -73,6 +73,10 @@ const StatusContent = { }, computed: { ...controlledOrUncontrolledGetters(['showingTall', 'expandingSubject', 'showingLongSubject']), + statusCard () { + if (!this.status.card) return null + return this.status.card.url === this.status.quote_url ? null : this.status.card + }, hideAttachments () { return (this.mergedConfig.hideAttachments && !this.inConversation) || (this.mergedConfig.hideAttachmentsInConv && this.inConversation) diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index c0e5c0b9..e977d489 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -43,7 +43,7 @@ /> <div - v-if="status.card && !noHeading && !compact" + v-if="statusCard && !noHeading && !compact" class="link-preview media-body" > <link-preview diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue index 311ca099..839f82e9 100644 --- a/src/components/status_popover/status_popover.vue +++ b/src/components/status_popover/status_popover.vue @@ -40,19 +40,14 @@ <script src="./status_popover.js"></script> <style lang="scss"> -@import "../../variables"; - /* popover styles load on-demand, so we need to override */ .status-popover.popover { font-size: 1rem; min-width: 15em; max-width: 95%; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); border-style: solid; border-width: 1px; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); /* TODO cleanup this */ .Status.Status { diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue index 904853c0..6678132e 100644 --- a/src/components/sticker_picker/sticker_picker.vue +++ b/src/components/sticker_picker/sticker_picker.vue @@ -32,8 +32,6 @@ <script src="./sticker_picker.js"></script> <style lang="scss"> -@import "../../variables"; - .sticker-picker { width: 100%; @@ -56,7 +54,7 @@ height: 100%; &:hover { - filter: drop-shadow(0 0 5px var(--accent, $fallback--link)); + filter: drop-shadow(0 0 5px var(--accent)); } } } diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue index fc46fbe6..57633827 100644 --- a/src/components/still-image/still-image.vue +++ b/src/components/still-image/still-image.vue @@ -28,8 +28,6 @@ <script src="./still-image.js"></script> <style lang="scss"> -@import "../../variables"; - .still-image { position: relative; line-height: 0; @@ -68,8 +66,7 @@ color: #fff; display: block; padding: 2px 4px; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); z-index: 2; visibility: var(--_still-image-label-visibility, visible); } diff --git a/src/components/tab_switcher/tab.style.js b/src/components/tab_switcher/tab.style.js new file mode 100644 index 00000000..eac8aaeb --- /dev/null +++ b/src/components/tab_switcher/tab.style.js @@ -0,0 +1,78 @@ +export default { + name: 'Tab', // Name of the component + selector: '.tab', // CSS selector/prefix + states: { + active: '.active', + hover: ':hover:not(.disabled)', + disabled: '.disabled' + }, + validInnerComponents: [ + 'Text', + 'Icon' + ], + defaultRules: [ + { + directives: { + background: '--fg', + shadow: ['--defaultButtonShadow', '--defaultButtonBevel'], + roundness: 3 + } + }, + { + state: ['hover'], + directives: { + shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel'] + } + }, + { + state: ['active'], + directives: { + opacity: 0 + } + }, + { + state: ['hover', 'active'], + directives: { + shadow: ['--defaultButtonShadow', '--defaultButtonBevel'] + } + }, + { + state: ['disabled'], + directives: { + background: '$blend(--inheritedBackground, 0.25, --parent)', + shadow: ['--defaultButtonBevel'] + } + }, + { + component: 'Text', + parent: { + component: 'Tab', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + }, + { + component: 'Icon', + parent: { + component: 'Tab', + state: ['active'] + }, + directives: { + textColor: '--text' + } + }, + { + component: 'Icon', + parent: { + component: 'Tab', + state: ['active', 'hover'] + }, + directives: { + textColor: '--text' + } + } + ] +} diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx index c8d390bc..027a380a 100644 --- a/src/components/tab_switcher/tab_switcher.jsx +++ b/src/components/tab_switcher/tab_switcher.jsx @@ -60,13 +60,7 @@ export default { const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName return this.$slots.default().findIndex(isWanted) === this.activeIndex } - }, - settingsModalVisible () { - return this.settingsModalState === 'visible' - }, - ...mapState({ - settingsModalState: state => state.interface.settingsModalState - }) + } }, beforeUpdate () { const currentSlot = this.slots()[this.active] @@ -103,7 +97,7 @@ export default { .map((slot, index) => { const props = slot.props if (!props) return - const classesTab = ['tab', 'button-default'] + const classesTab = ['tab'] const classesWrapper = ['tab-wrapper'] if (this.activeIndex === index) { classesTab.push('active') @@ -117,6 +111,7 @@ export default { onClick={this.clickTab(index)} class={classesTab.join(' ')} type="button" + role="tab" > <img src={props.image} title={props['image-tooltip']}/> {props.label ? '' : props.label} @@ -131,6 +126,7 @@ export default { 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"> @@ -167,11 +163,15 @@ export default { return ( <div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}> - <div class="tabs"> + <div + class="tabs" + role="tablist" + > {tabs} </div> <div ref="contents" + role="tabpanel" class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')} v-body-scroll-lock={this.bodyScrollLock} > diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss index 705925c8..4e242b91 100644 --- a/src/components/tab_switcher/tab_switcher.scss +++ b/src/components/tab_switcher/tab_switcher.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - /* stylelint-disable no-descending-specificity */ .tab-switcher { display: flex; @@ -25,8 +23,7 @@ content: ""; flex: 1 1 auto; border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + border-bottom-color: var(--border); } .tab-wrapper { @@ -37,8 +34,7 @@ right: 0; bottom: 0; border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + border-bottom-color: var(--border); } } @@ -80,8 +76,7 @@ flex-basis: 0.5em; content: ""; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &::after { @@ -106,16 +101,14 @@ right: 0; bottom: 0; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &::before { flex: 0 0 6px; content: ""; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &:last-child .tab { @@ -173,6 +166,15 @@ } .tab { + user-select: none; + color: var(--text); + border: none; + cursor: pointer; + box-shadow: var(--shadow); + font-size: 1em; + font-family: var(--font); + border-radius: var(--roundness); + background-color: var(--background); position: relative; white-space: nowrap; padding: 6px 1em; @@ -188,8 +190,6 @@ &.active { background: transparent; z-index: 5; - color: $fallback--text; - color: var(--tabActiveText, $fallback--text); } img { @@ -231,7 +231,7 @@ margin-top: 0.5em; margin-left: 0.2em; margin-bottom: 0.25em; - border-bottom: 1px solid var(--border, $fallback--border); + border-bottom: 1px solid var(--border); @media all and (min-width: 800px) { display: none; diff --git a/src/components/text.style.js b/src/components/text.style.js new file mode 100644 index 00000000..a254ceb4 --- /dev/null +++ b/src/components/text.style.js @@ -0,0 +1,22 @@ +export default { + name: 'Text', + selector: '/*text*/', + virtual: true, + states: { + faint: '.faint' + }, + defaultRules: [ + { + directives: { + textColor: '--text', + textAuto: 'no-preserve' + } + }, + { + state: ['faint'], + directives: { + textOpacity: 0.5 + } + } + ] +} diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue index 04727278..971b7001 100644 --- a/src/components/thread_tree/thread_tree.vue +++ b/src/components/thread_tree/thread_tree.vue @@ -119,15 +119,13 @@ <script src="./thread_tree.js"></script> <style lang="scss"> -@import "../../variables"; - .thread-tree-replies { - margin-left: var(--status-margin, $status-margin); - border-left: 2px solid var(--border, $fallback--border); + margin-left: var(--status-margin); + border-left: 2px solid var(--border); } .thread-tree-replies-hidden { - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); /* Make the button stretch along the whole row */ display: flex; diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue index b5f49515..bf918441 100644 --- a/src/components/timeago/timeago.vue +++ b/src/components/timeago/timeago.vue @@ -3,7 +3,7 @@ :datetime="time" :title="localeDateString" > - {{ relativeTimeString }} + {{ relativeOrAbsoluteTimeString }} </time> </template> @@ -16,16 +16,28 @@ export default { props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold', 'templateKey'], data () { return { + relativeTimeMs: 0, relativeTime: { key: 'time.now', num: 0 }, interval: null } }, computed: { - localeDateString () { - const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale) + shouldUseAbsoluteTimeFormat () { + if (!this.$store.getters.mergedConfig.useAbsoluteTimeFormat) { + return false + } + return DateUtils.durationStrToMs(this.$store.getters.mergedConfig.absoluteTimeFormatMinAge) <= this.relativeTimeMs + }, + browserLocale () { + return localeService.internalToBrowserLocale(this.$i18n.locale) + }, + timeAsDate () { return typeof this.time === 'string' - ? new Date(Date.parse(this.time)).toLocaleString(browserLocale) - : this.time.toLocaleString(browserLocale) + ? new Date(Date.parse(this.time)) + : this.time + }, + localeDateString () { + return this.timeAsDate.toLocaleString(this.browserLocale) }, relativeTimeString () { const timeString = this.$i18n.tc(this.relativeTime.key, this.relativeTime.num, [this.relativeTime.num]) @@ -35,6 +47,40 @@ export default { } return timeString + }, + absoluteTimeString () { + if (this.longFormat) { + return this.localeDateString + } + const now = new Date() + const formatter = (() => { + if (DateUtils.isSameDay(this.timeAsDate, now)) { + return new Intl.DateTimeFormat(this.browserLocale, { + minute: 'numeric', + hour: 'numeric' + }) + } else if (DateUtils.isSameMonth(this.timeAsDate, now)) { + return new Intl.DateTimeFormat(this.browserLocale, { + hour: 'numeric', + day: 'numeric' + }) + } else if (DateUtils.isSameYear(this.timeAsDate, now)) { + return new Intl.DateTimeFormat(this.browserLocale, { + month: 'short', + day: 'numeric' + }) + } else { + return new Intl.DateTimeFormat(this.browserLocale, { + year: 'numeric', + month: 'short' + }) + } + })() + + return formatter.format(this.timeAsDate) + }, + relativeOrAbsoluteTimeString () { + return this.shouldUseAbsoluteTimeFormat ? this.absoluteTimeString : this.relativeTimeString } }, watch: { @@ -54,6 +100,7 @@ export default { methods: { refreshRelativeTimeObject () { const nowThreshold = typeof this.nowThreshold === 'number' ? this.nowThreshold : 1 + this.relativeTimeMs = DateUtils.relativeTimeMs(this.time) this.relativeTime = this.longFormat ? DateUtils.relativeTime(this.time, nowThreshold) : DateUtils.relativeTimeShort(this.time, nowThreshold) diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index b7414610..59170f49 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -25,6 +25,7 @@ const Timeline = { 'title', 'userId', 'listId', + 'statusId', 'tag', 'embedded', 'count', @@ -77,13 +78,13 @@ const Timeline = { } }, classes () { - let rootClasses = !this.embedded ? ['panel', 'panel-default'] : ['-nonpanel'] + let rootClasses = !this.embedded ? ['panel', 'panel-default'] : ['-embedded'] if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention']) return { root: rootClasses, - header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading', '-sticky'] : []), - body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []), - footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : []) + header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading', '-sticky'] : ['panel-body']), + body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : ['panel-body']), + footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : ['panel-body']) } }, // id map of statuses which need to be hidden in the main list due to pinning logic @@ -121,6 +122,7 @@ const Timeline = { showImmediately, userId: this.userId, listId: this.listId, + statusId: this.statusId, tag: this.tag }) }, @@ -160,6 +162,9 @@ const Timeline = { if (this.timeline.flushMarker !== 0) { this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true }) this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 }) + if (this.timelineName === 'user') { + this.$store.dispatch('fetchPinnedStatuses', this.userId) + } this.fetchOlderStatuses() } else { this.blockClicksTemporarily() @@ -180,6 +185,7 @@ const Timeline = { showImmediately: true, userId: this.userId, listId: this.listId, + statusId: this.statusId, tag: this.tag }).then(({ statuses }) => { if (statuses && statuses.length === 0) { diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss index 4371947d..2dd66328 100644 --- a/src/components/timeline/timeline.scss +++ b/src/components/timeline/timeline.scss @@ -1,31 +1,20 @@ -@import "../../variables"; - .Timeline { - .alert-dot { - border-radius: 100%; - height: 8px; - width: 8px; - position: absolute; - left: calc(50% - 4px); - top: calc(50% - 4px); - margin-left: 6px; - margin-top: -6px; - background-color: var(--badgeNeutral); + .timeline-body { + background: none; + backdrop-filter: none; } .alert-badge { font-size: 0.75em; line-height: 1; text-align: right; - border-radius: var(--tooltipRadius); + border-radius: var(--roundness); position: absolute; left: calc(50% - 0.5em); top: calc(50% - 0.4em); padding: 0.2em; margin-left: 0.7em; margin-top: -1em; - background-color: var(--badgeNeutral); - color: var(--badgeNeutralText); } .loadmore-button { @@ -37,16 +26,21 @@ } .conversation-heading { - top: calc(var(--__panel-heading-height) * var(--currentPanelStack, 2)); + top: calc(var(--__panel-heading-height) * var(--currentPanelStack, 1) + var(--navbar-height)); z-index: 2; } - &.-nonpanel { + &.-embedded { .timeline-heading { text-align: center; line-height: 2.75em; padding: 0 0.5em; + // Override the shrug empty filler + &:empty::before { + content: initial; + } + .button-default, .alert { line-height: 2em; diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 2279f21a..862a1972 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -38,7 +38,7 @@ fixed-width icon="circle-plus" /> - <div class="alert-badge"> + <div class="badge -counter"> {{ mobileLoadButtonString }} </div> </button> diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index 5a2a86c2..c4586b32 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -19,7 +19,8 @@ export const timelineNames = () => { bookmarks: 'nav.bookmarks', dms: 'nav.dms', 'public-timeline': 'nav.public_tl', - 'public-external-timeline': 'nav.twkn' + 'public-external-timeline': 'nav.twkn', + quotes: 'nav.quotes' } } diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue index 5f1da1f7..d755b9dd 100644 --- a/src/components/timeline_menu/timeline_menu.vue +++ b/src/components/timeline_menu/timeline_menu.vue @@ -45,8 +45,6 @@ <script src="./timeline_menu.js"></script> <style lang="scss"> -@import "../../variables"; - .timeline-menu-popover { min-width: 24rem; max-width: 100vw; @@ -60,65 +58,6 @@ margin: 0; padding: 0; } - - a { - display: block; - padding: 0 0.65em; - height: 3.5em; - line-height: 3.5em; - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - } - - &.router-link-active { - font-weight: bolder; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - - &:hover { - text-decoration: underline; - } - } - - svg { - margin-right: 0.4em; - margin-left: -0.2em; - } - } - - li { - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - padding: 0; - - &:last-child a { - border-bottom-right-radius: $fallback--panelRadius; - border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); - border-bottom-left-radius: $fallback--panelRadius; - border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius); - } - - &:last-child { - border: none; - } - } } .TimelineMenu { @@ -159,8 +98,6 @@ } &.open .timeline-menu-title svg { - color: $fallback--text; - color: var(--panelText, $fallback--text); transform: rotate(180deg); } diff --git a/src/components/top_bar.style.js b/src/components/top_bar.style.js new file mode 100644 index 00000000..46b3fb56 --- /dev/null +++ b/src/components/top_bar.style.js @@ -0,0 +1,28 @@ +export default { + name: 'TopBar', + selector: 'nav', + validInnerComponents: [ + 'Link', + 'Text', + 'Icon', + 'Button', + 'ButtonUnstyled', + 'Input', + 'Badge' + ], + defaultRules: [ + { + directives: { + background: '--fg', + shadow: [{ + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }] + } + } + ] +} diff --git a/src/components/underlay.style.js b/src/components/underlay.style.js new file mode 100644 index 00000000..3e0e1bf1 --- /dev/null +++ b/src/components/underlay.style.js @@ -0,0 +1,19 @@ +export default { + name: 'Underlay', + selector: '#content', + // Out of tree selector: Most components are laid over underlay, but underlay itself is not part of the DOM tree, + // i.e. it's a separate absolutely-positioned component, so we need to treat it differently depending on whether + // we are searching for underlay specifically or for whatever is laid on top of it. + outOfTreeSelector: '.underlay', + validInnerComponents: [ + 'Panel' + ], + defaultRules: [ + { + directives: { + background: '#000000', + opacity: 0.2 + } + } + ] +} diff --git a/src/components/update_notification/update_notification.scss b/src/components/update_notification/update_notification.scss index 4337acc4..fb2edaa4 100644 --- a/src/components/update_notification/update_notification.scss +++ b/src/components/update_notification/update_notification.scss @@ -1,5 +1,3 @@ -@import "src/variables"; - .UpdateNotification { overflow: hidden; } @@ -48,7 +46,7 @@ .panel-body { border-width: 0 0 1px; border-style: solid; - border-color: var(--border, $fallback--border); + border-color: var(--border); } .panel-footer { diff --git a/src/components/user_avatar/avatar.style.js b/src/components/user_avatar/avatar.style.js new file mode 100644 index 00000000..812d45a4 --- /dev/null +++ b/src/components/user_avatar/avatar.style.js @@ -0,0 +1,22 @@ +export default { + name: 'Avatar', + selector: '.Avatar', + variants: { + compact: '.-compact' + }, + defaultRules: [ + { + directives: { + roundness: 3, + shadow: [{ + x: 0, + y: 1, + blur: 8, + spread: 0, + color: '#000000', + alpha: 0.7 + }] + } + } + ] +} diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js index 33d9a258..ffd81f87 100644 --- a/src/components/user_avatar/user_avatar.js +++ b/src/components/user_avatar/user_avatar.js @@ -3,11 +3,13 @@ import StillImage from '../still-image/still-image.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { - faRobot + faRobot, + faPeopleGroup } from '@fortawesome/free-solid-svg-icons' library.add( - faRobot + faRobot, + faPeopleGroup ) const UserAvatar = { @@ -15,7 +17,7 @@ const UserAvatar = { 'user', 'betterShadow', 'compact', - 'bot' + 'showActorTypeIndicator' ], data () { return { diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue index 91c17611..83608c50 100644 --- a/src/components/user_avatar/user_avatar.vue +++ b/src/components/user_avatar/user_avatar.vue @@ -18,21 +18,24 @@ :class="{ '-compact': compact }" /> <FAIcon - v-if="bot" + v-if="showActorTypeIndicator && user?.actor_type === 'Service'" icon="robot" - class="bot-indicator" + class="actor-type-indicator" + /> + <FAIcon + v-if="showActorTypeIndicator && user?.actor_type === 'Group'" + icon="people-group" + class="actor-type-indicator" /> </span> </template> <script src="./user_avatar.js"></script> <style lang="scss"> -@import "../../variables"; - .Avatar { - --_avatarShadowBox: var(--avatarStatusShadow); - --_avatarShadowFilter: var(--avatarStatusShadowFilter); - --_avatarShadowInset: var(--avatarStatusShadowInset); + --_avatarShadowBox: var(--shadow); + --_avatarShadowFilter: var(--shadowFilter); + --_avatarShadowInset: var(--shadowInset); --_still-image-label-visibility: hidden; display: inline-block; @@ -43,16 +46,14 @@ &.-compact { width: 32px; height: 32px; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); } .avatar { width: 100%; height: 100%; box-shadow: var(--_avatarShadowBox); - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); &.-better-shadow { box-shadow: var(--_avatarShadowInset); @@ -64,13 +65,11 @@ } &.-compact { - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); } &.-placeholder { - background-color: $fallback--fg; - background-color: var(--fg, $fallback--fg); + background-color: var(--background); } } @@ -79,7 +78,7 @@ height: 100%; } - .bot-indicator { + .actor-type-indicator { position: absolute; bottom: 0; right: 0; @@ -87,7 +86,7 @@ padding: 0.2em; background: rgb(127 127 127 / 50%); color: #fff; - border-radius: var(--tooltipRadius); + border-radius: var(--roundness); } } </style> diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index e17bf8eb..b1fe2e8f 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -225,7 +225,7 @@ export default { this.$store.dispatch('setCurrentMedia', attachment) }, mentionUser () { - this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) + this.$store.dispatch('openPostStatusModal', { profileMention: true, repliedUser: this.user }) }, onAvatarClickHandler (e) { if (this.onAvatarClick) { diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss index 4ab93a8a..70a76d54 100644 --- a/src/components/user_card/user_card.scss +++ b/src/components/user_card/user_card.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .user-card { position: relative; z-index: 1; @@ -21,14 +19,6 @@ position: relative; } - .panel-body { - word-wrap: break-word; - border-bottom-right-radius: inherit; - border-bottom-left-radius: inherit; - // create new stacking context - position: relative; - } - .background-image { position: absolute; top: 0; @@ -62,11 +52,6 @@ padding: 1em; margin: 0; - a { - color: $fallback--link; - color: var(--postLink, $fallback--link); - } - img { object-fit: contain; vertical-align: middle; @@ -76,53 +61,37 @@ } &.-rounded-t { - border-top-left-radius: $fallback--panelRadius; - border-top-left-radius: var(--panelRadius, $fallback--panelRadius); - border-top-right-radius: $fallback--panelRadius; - border-top-right-radius: var(--panelRadius, $fallback--panelRadius); + border-top-left-radius: var(--roundness); + border-top-right-radius: var(--roundness); - --__roundnessTop: var(--panelRadius); + --__roundnessTop: var(--roundness); --__roundnessBottom: 0; } &.-rounded { - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); - --__roundnessTop: var(--panelRadius); - --__roundnessBottom: var(--panelRadius); + --__roundnessTop: var(--roundness); + --__roundnessBottom: var(--roundness); } &.-popover { - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); - --__roundnessTop: var(--tooltipRadius); - --__roundnessBottom: var(--tooltipRadius); + --__roundnessTop: var(--roundness); + --__roundnessBottom: var(--roundness); } &.-bordered { border-width: 1px; border-style: solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); } } .user-info { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); padding: 0 26px; - a { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - - &:hover { - color: var(--icon); - } - } - .container { min-width: 0; padding: 16px 0 6px; @@ -164,8 +133,7 @@ display: flex; justify-content: center; align-items: center; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); opacity: 0; transition: opacity 0.2s ease; @@ -188,8 +156,7 @@ padding: 0.5em 0; &:not(:hover) .icon { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); + color: var(--lightText); } } @@ -203,6 +170,7 @@ } .user-screen-name { + color: var(--text); min-width: 1px; flex: 0 1 auto; text-overflow: ellipsis; @@ -214,16 +182,11 @@ flex: 0 0 auto; margin-left: 1em; font-size: 0.7em; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } .user-role { flex: none; - color: $fallback--text; - color: var(--alertNeutralText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--alertNeutral, $fallback--fg); } } @@ -241,6 +204,11 @@ --emoji-size: 1.7em; + .RichContent { + /* stylelint-disable-next-line declaration-no-important */ + --link: var(--text) !important; + } + .top-line, .bottom-line { display: flex; @@ -334,8 +302,6 @@ padding: 0.5em 1.5em 0; text-align: center; justify-content: space-between; - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); flex-wrap: wrap; } diff --git a/src/components/user_card/user_card.style.js b/src/components/user_card/user_card.style.js new file mode 100644 index 00000000..34eaa176 --- /dev/null +++ b/src/components/user_card/user_card.style.js @@ -0,0 +1,41 @@ +export default { + name: 'UserCard', + selector: '.user-card', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Button', + 'ButtonUnstyled', + 'Input', + 'RichContent', + 'Alert' + ], + defaultRules: [ + { + directives: { + background: '--bg', + opacity: 0, + roundness: 3, + shadow: [{ + x: 1, + y: 1, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }], + '--profileTint': 'color | $alpha(--background, 0.5)' + } + }, + { + parent: { + component: 'UserCard' + }, + component: 'RichContent', + directives: { + opacity: 0 + } + } + ] +} diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 2de14063..70c4f67d 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -113,22 +113,28 @@ <template v-if="!hideBio"> <span v-if="user.deactivated" - class="alert user-role" + class="alert neutral user-role" > {{ $t('user_card.deactivated') }} </span> <span v-if="!!visibleRole" - class="alert user-role" + class="alert neutral user-role" > {{ $t(`general.role.${visibleRole}`) }} </span> <span - v-if="user.bot" - class="alert user-role" + v-if="user.actor_type === 'Service'" + class="alert neutral user-role" > {{ $t('user_card.bot') }} </span> + <span + v-if="user.actor_type === 'Group'" + class="alert user-role" + > + {{ $t('user_card.group') }} + </span> </template> <span v-if="user.locked"> <FAIcon @@ -160,14 +166,14 @@ v-if="userHighlightType !== 'disabled'" :id="'userHighlightColorTx'+user.id" v-model="userHighlightColor" - class="userHighlightText" + class="input userHighlightText" type="text" > <input v-if="userHighlightType !== 'disabled'" :id="'userHighlightColor'+user.id" v-model="userHighlightColor" - class="userHighlightCl" + class="input userHighlightCl" type="color" > {{ ' ' }} @@ -276,10 +282,7 @@ /> </div> </div> - <div - v-if="!hideBio" - class="panel-body" - > + <div v-if="!hideBio"> <div v-if="!mergedConfig.hideUserStats && switcher" class="user-counts" diff --git a/src/components/user_list_menu/user_list_menu.vue b/src/components/user_list_menu/user_list_menu.vue index 06947ab7..39ee2451 100644 --- a/src/components/user_list_menu/user_list_menu.vue +++ b/src/components/user_list_menu/user_list_menu.vue @@ -10,11 +10,11 @@ <button v-for="list in lists" :key="list.id" - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleList(list.id)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': list.inList }" /> {{ list.title }} @@ -22,7 +22,7 @@ </div> </template> <template #trigger> - <button class="btn button-default dropdown-item -has-submenu"> + <button class="menu-item dropdown-item -has-submenu"> {{ $t('lists.manage_lists') }} <FAIcon class="chevron-icon" diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue index 8307cc8a..cd134453 100644 --- a/src/components/user_list_popover/user_list_popover.vue +++ b/src/components/user_list_popover/user_list_popover.vue @@ -48,8 +48,6 @@ <script src="./user_list_popover.js"></script> <style lang="scss"> -@import "../../variables"; - .user-list-popover { padding: 0.5em; diff --git a/src/components/user_note/user_note.vue b/src/components/user_note/user_note.vue index 4e05951f..e175ced0 100644 --- a/src/components/user_note/user_note.vue +++ b/src/components/user_note/user_note.vue @@ -33,7 +33,7 @@ <textarea v-show="editing" v-model="localNote" - class="note-text" + class="input note-text" /> <span v-show="!editing" @@ -48,8 +48,6 @@ <script src="./user_note.js"></script> <style lang="scss"> -@import "../../variables"; - .user-note { display: flex; flex-direction: column; @@ -82,7 +80,7 @@ .note-text.-blank { font-style: italic; - color: var(--faint, $fallback--faint); + color: var(--textFaint); } } </style> diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue index 95ec97af..9323b65e 100644 --- a/src/components/user_panel/user_panel.vue +++ b/src/components/user_panel/user_panel.vue @@ -22,8 +22,15 @@ <script src="./user_panel.js"></script> <style lang="scss"> -.user-panel .signed-in { - overflow: visible; - z-index: 10; +.user-panel { + .panel { + background: var(--background); + backdrop-filter: var(--backdrop-filter); + } + + .signed-in { + overflow: visible; + z-index: 10; + } } </style> diff --git a/src/components/user_popover/user_popover.vue b/src/components/user_popover/user_popover.vue index 3b2bbc45..413b254c 100644 --- a/src/components/user_popover/user_popover.vue +++ b/src/components/user_popover/user_popover.vue @@ -24,8 +24,6 @@ <script src="./user_popover.js"></script> <style lang="scss"> -@import "../../variables"; - /* popover styles load on-demand, so we need to override */ /* stylelint-disable block-no-empty */ .user-popover.popover { diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index acb612ed..751bfd5a 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -80,6 +80,9 @@ const UserProfile = { followersTabVisible () { return this.isUs || !this.user.hide_followers }, + favoritesTabVisible () { + return this.isUs || !this.user.hide_favorites + }, formattedBirthday () { const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale) return this.user.birthday && new Date(Date.parse(this.user.birthday)).toLocaleDateString(browserLocale, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' }) @@ -103,6 +106,8 @@ const UserProfile = { startFetchingTimeline('user', userId) startFetchingTimeline('media', userId) if (this.isUs) { + startFetchingTimeline('favorites') + } else if (!this.user.hide_favorites) { startFetchingTimeline('favorites', userId) } // Fetch all pinned statuses immediately diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index c63a303c..1ec12d0c 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -4,52 +4,54 @@ v-if="user" class="user-profile panel panel-default" > - <UserCard - :user-id="userId" - :switcher="true" - :selected="timeline.viewing" - avatar-action="zoom" - rounded="top" - :has-note-editor="true" - /> - <span - v-if="!!user.birthday" - class="user-birthday" - > - <FAIcon - class="fa-old-padding" - icon="birthday-cake" + <div class="panel-body"> + <UserCard + :user-id="userId" + :switcher="true" + :selected="timeline.viewing" + avatar-action="zoom" + rounded="top" + :has-note-editor="true" /> - {{ $t('user_card.birthday', { birthday: formattedBirthday }) }} - </span> - <div - v-if="user.fields_html && user.fields_html.length > 0" - class="user-profile-fields" - > - <dl - v-for="(field, index) in user.fields_html" - :key="index" - class="user-profile-field" + <span + v-if="!!user.birthday" + class="user-birthday" > - <dt - :title="user.fields_text[index].name" - class="user-profile-field-name" - > - <RichContent - :html="field.name" - :emoji="user.emoji" - /> - </dt> - <dd - :title="user.fields_text[index].value" - class="user-profile-field-value" + <FAIcon + class="fa-old-padding" + icon="birthday-cake" + /> + {{ $t('user_card.birthday', { birthday: formattedBirthday }) }} + </span> + <div + v-if="user.fields_html && user.fields_html.length > 0" + class="user-profile-fields" + > + <dl + v-for="(field, index) in user.fields_html" + :key="index" + class="user-profile-field" > - <RichContent - :html="field.value" - :emoji="user.emoji" - /> - </dd> - </dl> + <dt + :title="user.fields_text[index].name" + class="user-profile-field-name" + > + <RichContent + :html="field.name" + :emoji="user.emoji" + /> + </dt> + <dd + :title="user.fields_text[index].value" + class="user-profile-field-value" + > + <RichContent + :html="field.value" + :emoji="user.emoji" + /> + </dd> + </dl> + </div> </div> <tab-switcher :active-tab="tab" @@ -72,10 +74,14 @@ <div v-if="followsTabVisible" key="followees" + class="panel-body" :label="$t('user_card.followees')" :disabled="!user.friends_count" > - <FriendList :user-id="userId"> + <FriendList + :user-id="userId" + :non-interactive="true" + > <template #item="{item}"> <FollowCard :user="item" /> </template> @@ -84,10 +90,14 @@ <div v-if="followersTabVisible" key="followers" + class="panel-body" :label="$t('user_card.followers')" :disabled="!user.followers_count" > - <FollowerList :user-id="userId"> + <FollowerList + :user-id="userId" + :non-interactive="true" + > <template #item="{item}"> <FollowCard :user="item" @@ -109,7 +119,7 @@ :footer-slipgate="footerRef" /> <Timeline - v-if="isUs" + v-if="favoritesTabVisible" key="favorites" :label="$t('user_card.favorites')" :disabled="!favorites.visibleStatuses.length" @@ -117,6 +127,7 @@ :title="$t('user_card.favorites')" timeline-name="favorites" :timeline="favorites" + :user-id="isUs ? undefined : userId" :in-profile="true" :footer-slipgate="footerRef" /> @@ -135,7 +146,7 @@ {{ $t('settings.profile_tab') }} </div> </div> - <div class="panel-body"> + <div> <span v-if="error">{{ error }}</span> <FAIcon v-else @@ -150,8 +161,6 @@ <script src="./user_profile.js"></script> <style lang="scss"> -@import "../../variables"; - .user-profile { flex: 2; flex-basis: 500px; @@ -181,9 +190,8 @@ .user-profile-field { display: flex; margin: 0.25em; - border: 1px solid var(--border, $fallback--border); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border: 1px solid var(--border); + border-radius: var(--roundness); .user-profile-field-name { flex: 0 1 30%; @@ -191,7 +199,7 @@ text-align: right; color: var(--lightText); min-width: 120px; - border-right: 1px solid var(--border, $fallback--border); + border-right: 1px solid var(--border); } .user-profile-field-value { @@ -228,4 +236,5 @@ padding: 7em; } } + </style> diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue index 092c514e..2bda509a 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.vue +++ b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -19,7 +19,7 @@ <p>{{ $t('user_reporting.add_comment_description') }}</p> <textarea v-model="comment" - class="form-control" + class="input form-control" :placeholder="$t('user_reporting.additional_comments')" rows="1" @input="resize" @@ -72,8 +72,6 @@ <script src="./user_reporting_modal.js"></script> <style lang="scss"> -@import "../../variables"; - .user-reporting-panel { width: 90vw; max-width: 700px; @@ -84,8 +82,7 @@ display: flex; flex-direction: column-reverse; border-top: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); overflow: hidden; } @@ -155,8 +152,7 @@ width: 50%; max-width: 320px; border-right: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); padding: 1.1em; > div { diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue index 8a3ea1e3..df763143 100644 --- a/src/components/video_attachment/video_attachment.vue +++ b/src/components/video_attachment/video_attachment.vue @@ -2,7 +2,7 @@ <video class="video" preload="metadata" - :src="attachment.url" + :src="attachment.url + '#t=0.00000000000001'" :loop="loopVideo" :controls="controls" :alt="attachment.description" |
