diff --git a/README.md b/README.md index b291e3ec..4e879456 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Pleroma-FE +![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet) + This is a fork of Pleroma-FE from the Pleroma project, with support for new Akkoma features such as: - MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm) - Custom emoji reactions diff --git a/index.html b/index.html index 1f4e798c..d81d2ed1 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,7 @@
+ diff --git a/package.json b/package.json index 1445f12c..dcde025a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@babel/runtime": "7.17.8", "@chenfengyuan/vue-qrcode": "2.0.0", "@fortawesome/fontawesome-svg-core": "1.3.0", - "@fortawesome/free-regular-svg-icons": "5.15.4", + "@fortawesome/free-regular-svg-icons": "^6.1.2", "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/vue-fontawesome": "3.0.1", "@kazvmoe-infra/pinch-zoom-element": "1.2.0", diff --git a/src/App.vue b/src/App.vue index c3cf33f8..ed4f318e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -59,7 +59,7 @@ - diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 698a3f6e..32f8da78 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -1,3 +1,4 @@ +import Cookies from 'js-cookie' import { createApp } from 'vue' import { createRouter, createWebHistory } from 'vue-router' import vClickOutside from 'click-outside-vue3' @@ -50,6 +51,20 @@ const preloadFetch = async (request) => { } } +const resolveLanguage = (instanceLanguages) => { + // First language in navigator.languages that is listed as an instance language + // falls back to first instance language + const navigatorLanguages = navigator.languages.map((x) => x.split('-')[0]) + + for (const navLanguage of navigatorLanguages) { + if (instanceLanguages.includes(navLanguage)) { + return navLanguage + } + } + + return instanceLanguages[0] +} + const getInstanceConfig = async ({ store }) => { try { const res = await preloadFetch('/api/v1/instance') @@ -60,6 +75,10 @@ const getInstanceConfig = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) + // don't override cookie if set + if (!Cookies.get('userLanguage')) { + store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) }) + } if (vapidPublicKey) { store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) @@ -126,6 +145,11 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('hideBotIndication') copyInstanceOption('hideUserStats') copyInstanceOption('hideFilteredStatuses') + copyInstanceOption('hideSiteName') + copyInstanceOption('hideSiteFavicon') + copyInstanceOption('showWiderShortcuts') + copyInstanceOption('showNavShortcuts') + copyInstanceOption('showPanelNavShortcuts') copyInstanceOption('logo') store.dispatch('setInstanceOption', { @@ -156,6 +180,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('alwaysShowSubjectInput') copyInstanceOption('showFeaturesPanel') copyInstanceOption('hideSitename') + copyInstanceOption('renderMisskeyMarkdown') copyInstanceOption('sidebarRight') return store.dispatch('setTheme', config['theme']) @@ -287,6 +312,7 @@ const getNodeInfo = async ({ store }) => { }) store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation }) + store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances }) store.dispatch('setInstanceOption', { name: 'federating', value: typeof federation.enabled === 'undefined' @@ -378,8 +404,9 @@ const afterStoreSetup = async ({ store, i18n }) => { routes: routes(store), scrollBehavior: (to, _from, savedPosition) => { if (to.matched.some(m => m.meta.dontScroll)) { - return false + return {} } + return savedPosition || { left: 0, top: 0 } } }) diff --git a/src/boot/routes.js b/src/boot/routes.js index cb2534c1..d762f057 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -58,7 +58,7 @@ export default (store) => { component: RemoteUserResolver, beforeEnter: validateAuthenticatedRoute }, - { name: 'external-user-profile', path: '/users/:id', component: UserProfile }, + { name: 'external-user-profile', path: '/users/:id', component: UserProfile, meta: { dontScroll: true } }, { name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute }, { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'registration', path: '/registration', component: Registration }, @@ -75,7 +75,7 @@ export default (store) => { { name: 'list-timeline', path: '/lists/:id', component: ListTimeline }, { name: 'list-edit', path: '/lists/:id/edit', component: ListEdit }, { name: 'announcements', path: '/announcements', component: AnnouncementsPage }, - { name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile } + { name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile, meta: { dontScroll: true } } ] return routes diff --git a/src/components/about/about.js b/src/components/about/about.js index 1df25845..e69bf8f9 100644 --- a/src/components/about/about.js +++ b/src/components/about/about.js @@ -3,6 +3,7 @@ import FeaturesPanel from '../features_panel/features_panel.vue' import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue' import StaffPanel from '../staff_panel/staff_panel.vue' import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue' +import LocalBubblePanel from '../local_bubble_panel/local_bubble_panel.vue' const About = { components: { @@ -10,7 +11,8 @@ const About = { FeaturesPanel, TermsOfServicePanel, StaffPanel, - MRFTransparencyPanel + MRFTransparencyPanel, + LocalBubblePanel }, computed: { showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, diff --git a/src/components/about/about.vue b/src/components/about/about.vue index 5d5d6479..221fc381 100644 --- a/src/components/about/about.vue +++ b/src/components/about/about.vue @@ -3,6 +3,7 @@ + diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index 8fe0fe5e..c227c409 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -1,6 +1,8 @@ import ProgressButton from '../progress_button/progress_button.vue' import Popover from '../popover/popover.vue' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' +import { mapState } from 'vuex' import { faEllipsisV } from '@fortawesome/free-solid-svg-icons' @@ -14,13 +16,22 @@ const AccountActions = { 'user', 'relationship' ], data () { - return { } + return { + showingConfirmBlock: false + } }, components: { ProgressButton, - Popover + Popover, + ConfirmModal }, methods: { + showConfirmBlock () { + this.showingConfirmBlock = true + }, + hideConfirmBlock () { + this.showingConfirmBlock = false + }, showRepeats () { this.$store.dispatch('showReblogs', this.user.id) }, @@ -28,7 +39,15 @@ const AccountActions = { this.$store.dispatch('hideReblogs', this.user.id) }, blockUser () { + if (!this.shouldConfirmBlock) { + this.doBlockUser() + } else { + this.showConfirmBlock() + } + }, + doBlockUser () { this.$store.dispatch('blockUser', this.user.id) + this.hideConfirmBlock() }, unblockUser () { this.$store.dispatch('unblockUser', this.user.id) @@ -36,6 +55,14 @@ const AccountActions = { reportUser () { this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) } + }, + computed: { + shouldConfirmBlock () { + return this.$store.getters.mergedConfig.modalOnBlock + }, + ...mapState({ + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable + }) } } diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index afd8bb1b..3e65aa62 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -59,6 +59,27 @@ + + + + + + + diff --git a/src/components/confirm_modal/confirm_modal.js b/src/components/confirm_modal/confirm_modal.js new file mode 100644 index 00000000..96ddc118 --- /dev/null +++ b/src/components/confirm_modal/confirm_modal.js @@ -0,0 +1,37 @@ +import DialogModal from '../dialog_modal/dialog_modal.vue' + +/** + * This component emits the following events: + * cancelled, emitted when the action should not be performed; + * accepted, emitted when the action should be performed; + * + * The caller should close this dialog after receiving any of the two events. + */ +const ConfirmModal = { + components: { + DialogModal + }, + props: { + title: { + type: String + }, + cancelText: { + type: String + }, + confirmText: { + type: String + } + }, + computed: { + }, + methods: { + onCancel () { + this.$emit('cancelled') + }, + onAccept () { + this.$emit('accepted') + } + } +} + +export default ConfirmModal diff --git a/src/components/confirm_modal/confirm_modal.vue b/src/components/confirm_modal/confirm_modal.vue new file mode 100644 index 00000000..5783c794 --- /dev/null +++ b/src/components/confirm_modal/confirm_modal.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js index e048f53d..9ba5abc4 100644 --- a/src/components/desktop_nav/desktop_nav.js +++ b/src/components/desktop_nav/desktop_nav.js @@ -1,16 +1,21 @@ import SearchBar from 'components/search_bar/search_bar.vue' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faSignInAlt, faSignOutAlt, faHome, faComments, - faBell, faUserPlus, faBullhorn, faSearch, faTachometerAlt, faCog, + faGlobe, + faBolt, + faUsers, + faCommentMedical, + faBookmark, faInfoCircle } from '@fortawesome/free-solid-svg-icons' @@ -19,18 +24,23 @@ library.add( faSignOutAlt, faHome, faComments, - faBell, faUserPlus, faBullhorn, faSearch, faTachometerAlt, faCog, + faGlobe, + faBolt, + faUsers, + faCommentMedical, + faBookmark, faInfoCircle ) export default { components: { - SearchBar + SearchBar, + ConfirmModal }, data: () => ({ searchBarHidden: true, @@ -40,7 +50,8 @@ export default { window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') || window.CSS.supports('-o-mask-size', 'contain') - ) + ), + showingConfirmLogout: false }), computed: { enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, @@ -65,20 +76,34 @@ export default { }) }, logo () { return this.$store.state.instance.logo }, + mergedConfig () { + return this.$store.getters.mergedConfig + }, sitename () { return this.$store.state.instance.name }, + showNavShortcuts () { + return this.mergedConfig.showNavShortcuts + }, + showWiderShortcuts () { + return this.mergedConfig.showWiderShortcuts + }, + hideSiteFavicon () { + return this.mergedConfig.hideSiteFavicon + }, + hideSiteName () { + return this.mergedConfig.hideSiteName + }, hideSitename () { return this.$store.state.instance.hideSitename }, logoLeft () { return this.$store.state.instance.logoLeft }, currentUser () { return this.$store.state.users.currentUser }, - privateMode () { return this.$store.state.instance.private } + privateMode () { return this.$store.state.instance.private }, + shouldConfirmLogout () { + return this.$store.getters.mergedConfig.modalOnLogout + } }, methods: { scrollToTop () { window.scrollTo(0, 0) }, - logout () { - this.$router.replace('/main/public') - this.$store.dispatch('logout') - }, onSearchBarToggled (hidden) { this.searchBarHidden = hidden }, diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss index 7fd2b26f..4d8c40e3 100644 --- a/src/components/desktop_nav/desktop_nav.scss +++ b/src/components/desktop_nav/desktop_nav.scss @@ -15,16 +15,16 @@ display: grid; grid-template-rows: var(--navbar-height); grid-template-columns: 2fr auto 2fr; - grid-template-areas: "sitename logo actions"; + grid-template-areas: "nav-left logo actions"; box-sizing: border-box; padding: 0 1.2em; margin: auto; - max-width: 980px; + max-width: 1110px; } &.-logoLeft .inner-nav { grid-template-columns: auto 2fr 2fr; - grid-template-areas: "logo sitename actions"; + grid-template-areas: "logo nav-left actions"; } .button-default { @@ -84,14 +84,21 @@ } .nav-icon { - margin-left: 1em; + margin-left: 0.2em; width: 2em; height: 100%; - font-size: 130%; text-align: center; - &-logout { - margin-left: 2em; + &.router-link-active { + font-size: 1.2em; + margin-top: 0.05em; + + .svg-inline--fa { + font-weight: bolder; + color: $fallback--text; + color: var(--selectedMenuText, $fallback--text); + --lightText: var(--selectedMenuLightText, $fallback--lightText); + } } .svg-inline--fa { @@ -100,8 +107,25 @@ } } - .sitename { - grid-area: sitename; + .-wide { + .nav-icon { + margin-left: 0.7em; + } + } + + .left { + padding-left: 5px; + display: flex; + } + + .nav-left-wrapper { + grid-area: nav-left; + + .favicon { + height: 28px; + vertical-align: middle; + padding-right: 5px; + } } .actions { diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index fe9af544..32e3c73f 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -5,16 +5,79 @@ :class="{ '-logoLeft': logoLeft }" @click="scrollToTop()" > -
-
+
+
+ + + {{ $t('login.logout_confirm') }} + + diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue index 06b270c3..1a42b0e5 100644 --- a/src/components/dialog_modal/dialog_modal.vue +++ b/src/components/dialog_modal/dialog_modal.vue @@ -39,7 +39,7 @@ right: 0; top: 0; background: rgba(27,31,35,.5); - z-index: 99; + z-index: 2000; } } @@ -51,7 +51,7 @@ margin: 15vh auto; position: fixed; transform: translateX(-50%); - z-index: 999; + z-index: 2001; cursor: default; display: block; background-color: $fallback--bg; diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index 7ca4e8a8..cbfa09fc 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -491,11 +491,10 @@ const EmojiInput = { }, setPlacement (container, target, offsetBottom) { if (!container || !target) return - - target.style.top = offsetBottom + 'px' - target.style.bottom = 'auto' - - if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) { + if (this.placement === 'bottom' || (this.placement === 'auto' && !this.overflowsBottom(container))) { + target.style.top = offsetBottom + 'px' + target.style.bottom = 'auto' + } else { target.style.top = 'auto' target.style.bottom = this.input.offsetHeight + 'px' } diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js index bb11b840..549b9517 100644 --- a/src/components/emoji_reactions/emoji_reactions.js +++ b/src/components/emoji_reactions/emoji_reactions.js @@ -27,7 +27,11 @@ const EmojiReactions = { }, accountsForEmoji () { return this.status.emoji_reactions.reduce((acc, reaction) => { - acc[reaction.name] = reaction.accounts || [] + if (reaction.url) { + acc[reaction.url] = reaction.accounts || [] + } else { + acc[reaction.name] = reaction.accounts || [] + } return acc }, {}) }, @@ -42,6 +46,14 @@ const EmojiReactions = { reactedWith (emoji) { return this.status.emoji_reactions.find(r => r.name === emoji).me }, + isLocalReaction (emojiUrl) { + if (!emojiUrl) return true + const reacted = this.accountsForEmoji[emojiUrl] + if (reacted.length === 0) { + return true + } + return reacted[0].is_local + }, fetchEmojiReactionsByIfMissing () { const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts) if (hasNoAccounts) { diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue index e76560bc..edc57d89 100644 --- a/src/components/emoji_reactions/emoji_reactions.vue +++ b/src/components/emoji_reactions/emoji_reactions.vue @@ -2,12 +2,13 @@
+ + + {{ $t('status.delete_confirm') }} + + diff --git a/src/components/follow_button/follow_button.js b/src/components/follow_button/follow_button.js index 3edbcb86..443aa9bc 100644 --- a/src/components/follow_button/follow_button.js +++ b/src/components/follow_button/follow_button.js @@ -1,12 +1,20 @@ +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' export default { props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], + components: { + ConfirmModal + }, data () { return { - inProgress: false + inProgress: false, + showingConfirmUnfollow: false } }, computed: { + shouldConfirmUnfollow () { + return this.$store.getters.mergedConfig.modalOnUnfollow + }, isPressed () { return this.inProgress || this.relationship.following }, @@ -35,6 +43,12 @@ export default { } }, methods: { + showConfirmUnfollow () { + this.showingConfirmUnfollow = true + }, + hideConfirmUnfollow () { + this.showingConfirmUnfollow = false + }, onClick () { this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() }, @@ -45,12 +59,21 @@ export default { }) }, unfollow () { + if (this.shouldConfirmUnfollow) { + this.showConfirmUnfollow() + } else { + this.doUnfollow() + } + }, + doUnfollow () { const store = this.$store this.inProgress = true requestUnfollow(this.relationship.id, store).then(() => { this.inProgress = false store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id }) }) + + this.hideConfirmUnfollow() } } } diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue index 965d5256..e421c15b 100644 --- a/src/components/follow_button/follow_button.vue +++ b/src/components/follow_button/follow_button.vue @@ -7,6 +7,27 @@ @click="onClick" > {{ label }} + + + + + + + diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js index cbd75311..b0873bb1 100644 --- a/src/components/follow_request_card/follow_request_card.js +++ b/src/components/follow_request_card/follow_request_card.js @@ -1,10 +1,18 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js' const FollowRequestCard = { props: ['user'], components: { - BasicUserCard + BasicUserCard, + ConfirmModal + }, + data () { + return { + showingApproveConfirmDialog: false, + showingDenyConfirmDialog: false + } }, methods: { findFollowRequestNotificationId () { @@ -13,7 +21,26 @@ const FollowRequestCard = { ) return notif && notif.id }, + showApproveConfirmDialog () { + this.showingApproveConfirmDialog = true + }, + hideApproveConfirmDialog () { + this.showingApproveConfirmDialog = false + }, + showDenyConfirmDialog () { + this.showingDenyConfirmDialog = true + }, + hideDenyConfirmDialog () { + this.showingDenyConfirmDialog = false + }, approveUser () { + if (this.shouldConfirmApprove) { + this.showApproveConfirmDialog() + } else { + this.doApprove() + } + }, + doApprove () { this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.dispatch('removeFollowRequest', this.user) @@ -25,14 +52,34 @@ const FollowRequestCard = { notification.type = 'follow' } }) + this.hideApproveConfirmDialog() }, denyUser () { + if (this.shouldConfirmDeny) { + this.showDenyConfirmDialog() + } else { + this.doDeny() + } + }, + doDeny () { const notifId = this.findFollowRequestNotificationId() this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) .then(() => { this.$store.dispatch('dismissNotificationLocal', { id: notifId }) this.$store.dispatch('removeFollowRequest', this.user) }) + this.hideDenyConfirmDialog() + } + }, + computed: { + mergedConfig () { + return this.$store.getters.mergedConfig + }, + shouldConfirmApprove () { + return this.mergedConfig.modalOnApproveFollow + }, + shouldConfirmDeny () { + return this.mergedConfig.modalOnDenyFollow } } } diff --git a/src/components/follow_request_card/follow_request_card.vue b/src/components/follow_request_card/follow_request_card.vue index 1b12ba4b..835471e7 100644 --- a/src/components/follow_request_card/follow_request_card.vue +++ b/src/components/follow_request_card/follow_request_card.vue @@ -14,6 +14,28 @@ {{ $t('user_card.deny') }}
+ + + {{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }} + + + {{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }} + + diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue index 7ad1fe2e..e46bcf71 100644 --- a/src/components/interface_language_switcher/interface_language_switcher.vue +++ b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -1,5 +1,6 @@ @@ -206,6 +218,14 @@ } } } + .confirm-modal.dark-overlay { + &::before { + z-index: 3000; + } + .dialog-modal.panel { + z-index: 3001; + } + } } diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index 96b8c3a3..75ea08cc 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -13,13 +13,13 @@ @@ -167,6 +167,7 @@ .moderation-tools-popover { height: 100%; + z-index: 999; .trigger { display: flex !important; height: 100%; diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index b9337cf1..b0a08a2f 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -10,7 +10,7 @@ import { faChevronDown, faChevronUp, faComments, - faBell, + faBolt, faInfoCircle, faStream, faList, @@ -25,7 +25,7 @@ library.add( faChevronDown, faChevronUp, faComments, - faBell, + faBolt, faInfoCircle, faStream, faList, diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 6860011e..c7d62356 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -48,7 +48,7 @@ {{ $t("nav.interactions") }} diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index fc8affd7..6a30115b 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -5,6 +5,7 @@ import UserCard from '../user_card/user_card.vue' import Timeago from '../timeago/timeago.vue' import StatusContent from '../status_content/status_content.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -36,7 +37,9 @@ const Notification = { return { userExpanded: false, betterShadow: this.$store.state.interface.browserSupport.cssFilter, - unmuted: false + unmuted: false, + showingApproveConfirmDialog: false, + showingDenyConfirmDialog: false } }, props: [ 'notification' ], @@ -46,7 +49,8 @@ const Notification = { UserCard, Timeago, Status, - RichContent + RichContent, + ConfirmModal }, methods: { toggleUserExpanded () { @@ -61,7 +65,26 @@ const Notification = { toggleMute () { this.unmuted = !this.unmuted }, + showApproveConfirmDialog () { + this.showingApproveConfirmDialog = true + }, + hideApproveConfirmDialog () { + this.showingApproveConfirmDialog = false + }, + showDenyConfirmDialog () { + this.showingDenyConfirmDialog = true + }, + hideDenyConfirmDialog () { + this.showingDenyConfirmDialog = false + }, approveUser () { + if (this.shouldConfirmApprove) { + this.showApproveConfirmDialog() + } else { + this.doApprove() + } + }, + doApprove () { this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id }) @@ -71,13 +94,22 @@ const Notification = { notification.type = 'follow' } }) + this.hideApproveConfirmDialog() }, denyUser () { + if (this.shouldConfirmDeny) { + this.showDenyConfirmDialog() + } else { + this.doDeny() + } + }, + doDeny () { this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) .then(() => { this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id }) this.$store.dispatch('removeFollowRequest', this.user) }) + this.hideDenyConfirmDialog() } }, computed: { @@ -107,6 +139,15 @@ const Notification = { isStatusNotification () { return isStatusNotification(this.notification.type) }, + mergedConfig () { + return this.$store.getters.mergedConfig + }, + shouldConfirmApprove () { + return this.mergedConfig.modalOnApproveFollow + }, + shouldConfirmDeny () { + return this.mergedConfig.modalOnDenyFollow + }, ...mapState({ currentUser: state => state.users.currentUser }) diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 72e31a0c..76101865 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -231,6 +231,28 @@
+ + + {{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }} + + + {{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }} + + diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js index 4f71af0a..9dc4d091 100644 --- a/src/components/retweet_button/retweet_button.js +++ b/src/components/retweet_button/retweet_button.js @@ -1,3 +1,4 @@ +import ConfirmModal from '../confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faRetweet } from '@fortawesome/free-solid-svg-icons' @@ -5,13 +6,24 @@ library.add(faRetweet) const RetweetButton = { props: ['status', 'loggedIn', 'visibility'], + components: { + ConfirmModal + }, data () { return { - animated: false + animated: false, + showingConfirmDialog: false } }, methods: { retweet () { + if (!this.status.repeated && this.shouldConfirmRepeat) { + this.showConfirmDialog() + } else { + this.doRetweet() + } + }, + doRetweet () { if (!this.status.repeated) { this.$store.dispatch('retweet', { id: this.status.id }) } else { @@ -21,6 +33,13 @@ const RetweetButton = { setTimeout(() => { this.animated = false }, 500) + this.hideConfirmDialog() + }, + showConfirmDialog () { + this.showingConfirmDialog = true + }, + hideConfirmDialog () { + this.showingConfirmDialog = false } }, computed: { @@ -29,6 +48,9 @@ const RetweetButton = { }, mergedConfig () { return this.$store.getters.mergedConfig + }, + shouldConfirmRepeat () { + return this.mergedConfig.modalOnRepeat } } } diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index e8a77e10..6bb7a283 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -33,6 +33,18 @@ > {{ status.repeat_num }} + + + {{ $t('status.repeat_confirm') }} + + diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index 0d553485..0d4127b6 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -148,20 +148,22 @@ export default { mfmHtml.innerHTML = marked.parse(content) // Add options with set values to CSS - Array.from(mfmHtml.content.firstChild.getElementsByClassName('mfm')).map((el) => { - if (el.dataset.speed) { - el.style.animationDuration = el.dataset.speed - } - if (el.dataset.deg) { - el.style.transform = `rotate(${el.dataset.deg}deg)` - } - if (Array.from(el.classList).includes('_mfm_font_')) { - const font = Object.keys(el.dataset)[0] - if (['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'].includes(font)) { - el.style.fontFamily = font + if (mfmHtml.content.firstChild) { + Array.from(mfmHtml.content.firstChild.getElementsByClassName('mfm')).map((el) => { + if (el.dataset.speed) { + el.style.animationDuration = el.dataset.speed } - } - }) + if (el.dataset.deg) { + el.style.transform = `rotate(${el.dataset.deg}deg)` + } + if (Array.from(el.classList).includes('_mfm_font_')) { + const font = Object.keys(el.dataset)[0] + if (['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'].includes(font)) { + el.style.fontFamily = font + } + } + }) + } return mfmHtml.innerHTML } diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js index 0a72dca1..b1bb4eab 100644 --- a/src/components/settings_modal/settings_modal.js +++ b/src/components/settings_modal/settings_modal.js @@ -14,6 +14,7 @@ import { faTimes, faFileUpload, faFileDownload, + faSignOutAlt, faChevronDown } from '@fortawesome/free-solid-svg-icons' import { @@ -28,6 +29,7 @@ library.add( faWindowMinimize, faFileUpload, faFileDownload, + faSignOutAlt, faChevronDown ) @@ -66,6 +68,11 @@ const SettingsModal = { closeModal () { this.$store.dispatch('closeSettingsModal') }, + logout () { + this.$router.replace('/main/public') + this.$store.dispatch('closeSettingsModal') + this.$store.dispatch('logout') + }, peekModal () { this.$store.dispatch('togglePeekSettingsModal') }, @@ -150,6 +157,7 @@ const SettingsModal = { } }, computed: { + currentUser () { return this.$store.state.users.currentUser }, currentSaveStateNotice () { return this.$store.state.interface.settings.currentSaveStateNotice }, diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss index 13cb0e65..70b63360 100644 --- a/src/components/settings_modal/settings_modal.scss +++ b/src/components/settings_modal/settings_modal.scss @@ -71,5 +71,11 @@ display: flex; flex-grow: 1; } + + .logout-button { + position: absolute; + right: 20px; + padding-right: 10px; + } } } diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue index d3bed061..03c9978e 100644 --- a/src/components/settings_modal/settings_modal.vue +++ b/src/components/settings_modal/settings_modal.vue @@ -111,6 +111,20 @@ id="unscrolled-content" class="extra-content" /> + diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 37ab3d9c..bb738f43 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -50,6 +50,46 @@

{{ $t('nav.timeline') }}

    +
  • + + {{ $t('settings.hide_site_favicon') }} + +
  • +
  • + + {{ $t('settings.hide_site_name') }} + +
  • +
  • + + {{ $t('settings.show_nav_shortcuts') }} + +
  • +
  • + + {{ $t('settings.show_panel_nav_shortcuts') }} + +
  • +
  • + + {{ $t('settings.show_wider_shortcuts') }} + +
  • {{ $t('settings.stop_gifs') }} @@ -128,6 +168,18 @@ {{ $t('settings.render_mfm') }} +
      +
    • + + {{ $t('settings.render_mfm_on_hover') }} + +
    • +
  • +
  • +

    {{ $t('settings.columns') }}

    +
  • +
  • + + {{ $t('settings.disable_sticky_headers') }} + +
  • +
  • + + {{ $t('settings.show_scrollbars') }} + +
  • +
  • + + {{ $t('settings.right_sidebar') }} + +
  • +
  • + + {{ $t('settings.third_column_mode') }} + +
  • +
  • +

    {{ $t('settings.confirmation_dialogs') }}

    +
  • +
  • + {{ $t('settings.confirm_dialogs') }} +
      +
    • + + {{ $t('settings.confirm_dialogs_repeat') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_unfollow') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_block') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_mute') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_delete') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_approve_follow') }} + +
    • +
    • + + {{ $t('settings.confirm_dialogs_deny_follow') }} + +
    • +
    +
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 9bfee6bd..1c8d5865 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -8,7 +8,7 @@ import { faSignOutAlt, faHome, faComments, - faBell, + faBolt, faUserPlus, faBullhorn, faSearch, @@ -23,7 +23,7 @@ library.add( faSignOutAlt, faHome, faComments, - faBell, + faBolt, faUserPlus, faBullhorn, faSearch, diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 4bbc43bb..6d78fa85 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -74,7 +74,7 @@ {{ $t("nav.interactions") }} diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss index b1d9ecc9..0f946f79 100644 --- a/src/components/status_body/status_body.scss +++ b/src/components/status_body/status_body.scss @@ -204,3 +204,4 @@ } } } + diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js index 3cd50622..81e40b0b 100644 --- a/src/components/status_content/status_content.js +++ b/src/components/status_content/status_content.js @@ -100,6 +100,12 @@ const StatusContent = { maxThumbnails () { return this.mergedConfig.maxThumbnails }, + renderMfmOnHover () { + return this.mergedConfig.renderMfmOnHover + }, + renderMisskeyMarkdown () { + return this.mergedConfig.renderMisskeyMarkdown + }, ...mapGetters(['mergedConfig']), ...mapState({ currentUser: state => state.users.currentUser diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index 4f9c85bb..b8735f2c 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -1,7 +1,7 @@