diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f666a4ef..bfc41ac4 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -10,3 +10,5 @@ Contributors of this project. - shpuld (shpuld@shitposter.club): CSS and styling - Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images. - hj (hj@shigusegubu.club): Code +- Sean King (seanking@freespeechextremist.com): Code +- Tusooa Zhu (tusooa@kazv.moe): Code diff --git a/README.md b/README.md index 06b1dfb7..9be1c3bd 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/config/ihba.json b/config/ihba.json new file mode 100644 index 00000000..4b0560fa --- /dev/null +++ b/config/ihba.json @@ -0,0 +1,4 @@ +{ + "target": "https://ihatebeinga.live", + "staticConfigPreference": false +} diff --git a/index.html b/index.html index 40db0bbe..1b1385de 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.js b/src/App.js index 4304787f..3690b944 100644 --- a/src/App.js +++ b/src/App.js @@ -10,7 +10,9 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil import MobileNav from './components/mobile_nav/mobile_nav.vue' import DesktopNav from './components/desktop_nav/desktop_nav.vue' import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' +import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue' import PostStatusModal from './components/post_status_modal/post_status_modal.vue' +import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue' import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' import { windowWidth, windowHeight } from './services/window_utils/window_utils' import { mapGetters } from 'vuex' @@ -33,6 +35,8 @@ export default { SettingsModal, UserReportingModal, PostStatusModal, + EditStatusModal, + StatusHistoryModal, GlobalNoticeList }, data: () => ({ @@ -83,6 +87,7 @@ export default { return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, + editingAvailable () { return this.$store.state.instance.editingAvailable }, layoutType () { return this.$store.state.interface.layoutType }, privateMode () { return this.$store.state.instance.private }, reverseLayout () { diff --git a/src/App.vue b/src/App.vue index c3cf33f8..e1c0f5dc 100644 --- a/src/App.vue +++ b/src/App.vue @@ -58,8 +58,10 @@ + + - diff --git a/src/boot/after_store.js b/src/boot/after_store.js index c0a4cff9..c12c70f1 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' @@ -48,6 +49,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') @@ -58,6 +73,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 }) @@ -124,6 +143,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', { @@ -154,6 +178,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('alwaysShowSubjectInput') copyInstanceOption('showFeaturesPanel') copyInstanceOption('hideSitename') + copyInstanceOption('renderMisskeyMarkdown') copyInstanceOption('sidebarRight') return store.dispatch('setTheme', config['theme']) @@ -248,8 +273,10 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) + store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) + store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') }) const uploadLimits = metadata.uploadLimits store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) @@ -371,6 +398,7 @@ const afterStoreSetup = async ({ store, i18n }) => { store.dispatch('startFetchingAnnouncements') getTOS({ store }) getStickers({ store }) + store.dispatch('getSupportedTranslationlanguages') const router = createRouter({ history: createWebHistory(), 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/announcements_page/announcements_page.vue b/src/components/announcements_page/announcements_page.vue index b1489dec..a548a8a0 100644 --- a/src/components/announcements_page/announcements_page.vue +++ b/src/components/announcements_page/announcements_page.vue @@ -1,9 +1,9 @@ + + + + diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 2ef2977a..f8df9eb5 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,6 +1,8 @@ import { reduce, filter, findIndex, clone, get } from 'lodash' import Status from '../status/status.vue' import ThreadTree from '../thread_tree/thread_tree.vue' +import { WSConnectionStatus } from '../../services/api/api.service.js' +import { mapGetters, mapState } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -77,6 +79,9 @@ const conversation = { const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2 return maxDepth >= 1 ? maxDepth : 1 }, + streamingEnabled () { + return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED + }, displayStyle () { return this.$store.getters.mergedConfig.conversationDisplay }, @@ -339,7 +344,11 @@ const conversation = { }, maybeHighlight () { return this.isExpanded ? this.highlight : null - } + }, + ...mapGetters(['mergedConfig']), + ...mapState({ + mastoUserSocketStatus: state => state.api.mastoUserSocketStatus + }) }, components: { Status, @@ -395,6 +404,11 @@ const conversation = { setHighlight (id) { if (!id) return this.highlight = id + + if (!this.streamingEnabled) { + this.$store.dispatch('fetchStatus', id) + } + this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchEmojiReactionsBy', id) }, 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..9dc02e68 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..0feef27b 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,9 +51,10 @@ margin: 15vh auto; position: fixed; transform: translateX(-50%); - z-index: 999; + z-index: 2001; cursor: default; display: block; + width: max-content; background-color: $fallback--bg; background-color: var(--bg, $fallback--bg); diff --git a/src/components/edit_status_modal/edit_status_modal.js b/src/components/edit_status_modal/edit_status_modal.js new file mode 100644 index 00000000..75adfea7 --- /dev/null +++ b/src/components/edit_status_modal/edit_status_modal.js @@ -0,0 +1,75 @@ +import PostStatusForm from '../post_status_form/post_status_form.vue' +import Modal from '../modal/modal.vue' +import statusPosterService from '../../services/status_poster/status_poster.service.js' +import get from 'lodash/get' + +const EditStatusModal = { + components: { + PostStatusForm, + Modal + }, + data () { + return { + resettingForm: false + } + }, + computed: { + isLoggedIn () { + return !!this.$store.state.users.currentUser + }, + modalActivated () { + return this.$store.state.editStatus.modalActivated + }, + isFormVisible () { + return this.isLoggedIn && !this.resettingForm && this.modalActivated + }, + params () { + return this.$store.state.editStatus.params || {} + } + }, + watch: { + params (newVal, oldVal) { + if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) { + this.resettingForm = true + this.$nextTick(() => { + this.resettingForm = false + }) + } + }, + isFormVisible (val) { + if (val) { + this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus()) + } + } + }, + methods: { + doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) { + const params = { + store: this.$store, + statusId: this.$store.state.editStatus.params.statusId, + status, + spoilerText, + sensitive, + poll, + media, + contentType + } + + return statusPosterService.editStatus(params) + .then((data) => { + return data + }) + .catch((err) => { + console.error('Error editing status', err) + return { + error: err.message + } + }) + }, + closeModal () { + this.$store.dispatch('closeEditStatusModal') + } + } +} + +export default EditStatusModal diff --git a/src/components/edit_status_modal/edit_status_modal.vue b/src/components/edit_status_modal/edit_status_modal.vue new file mode 100644 index 00000000..00dde7de --- /dev/null +++ b/src/components/edit_status_modal/edit_status_modal.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js index 549b9517..06e21f6e 100644 --- a/src/components/emoji_reactions/emoji_reactions.js +++ b/src/components/emoji_reactions/emoji_reactions.js @@ -46,14 +46,6 @@ 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 edc57d89..6d9713ff 100644 --- a/src/components/emoji_reactions/emoji_reactions.vue +++ b/src/components/emoji_reactions/emoji_reactions.vue @@ -8,7 +8,6 @@ + + +
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..e9ebb97a 100644 --- a/src/components/interface_language_switcher/interface_language_switcher.vue +++ b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -1,5 +1,10 @@ @@ -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/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue index 1787fa07..b05bd298 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue @@ -1,6 +1,6 @@ + + + {{ $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/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 4e59e430..f023397e 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -55,6 +55,14 @@ const pxStringToNumber = (str) => { const PostStatusForm = { props: [ + 'statusId', + 'statusText', + 'statusIsSensitive', + 'statusPoll', + 'statusFiles', + 'statusMediaDescriptions', + 'statusScope', + 'statusContentType', 'replyTo', 'quoteId', 'repliedUser', @@ -63,6 +71,7 @@ const PostStatusForm = { 'subject', 'disableSubject', 'disableScopeSelector', + 'disableVisibilitySelector', 'disableNotice', 'disableLockWarning', 'disablePolls', @@ -120,23 +129,40 @@ const PostStatusForm = { const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject } = this.$store.getters.mergedConfig + let statusParams = { + spoilerText: this.subject || '', + status: statusText, + sensitiveByDefault, + nsfw: !!sensitiveByDefault, + files: [], + poll: {}, + mediaDescriptions: {}, + visibility: this.suggestedVisibility(), + contentType + } + + if (this.statusId) { + const statusContentType = this.statusContentType || contentType + statusParams = { + spoilerText: this.subject || '', + status: this.statusText || '', + sensitiveIfSubject, + nsfw: this.statusIsSensitive || !!sensitiveByDefault, + files: this.statusFiles || [], + poll: this.statusPoll || {}, + mediaDescriptions: this.statusMediaDescriptions || {}, + visibility: this.statusScope || this.suggestedVisibility(), + contentType: statusContentType + } + } + return { dropFiles: [], uploadingFiles: false, error: null, posting: false, highlighted: 0, - newStatus: { - spoilerText: this.subject || '', - status: statusText, - sensitiveIfSubject, - nsfw: !!sensitiveByDefault, - files: [], - poll: {}, - mediaDescriptions: {}, - visibility: this.suggestedVisibility(), - contentType - }, + newStatus: statusParams, caret: 0, pollFormVisible: false, showDropIcon: 'hide', @@ -232,6 +258,9 @@ const PostStatusForm = { uploadFileLimitReached () { return this.newStatus.files.length >= this.fileLimit }, + isEdit () { + return typeof this.statusId !== 'undefined' && this.statusId.trim() !== '' + }, ...mapGetters(['mergedConfig']), ...mapState({ mobileLayout: state => state.interface.mobileLayout diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 4824b723..6c505ddc 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -66,6 +66,13 @@ {{ $t('post_status.direct_warning_to_first_only') }} {{ $t('post_status.direct_warning_to_all') }}

+
+

{{ $t('post_status.edit_remote_warning') }}

+

{{ $t('post_status.edit_unsupported_warning') }}

+
:first-child { + margin-top: 0; + } + + > :last-child { + margin-bottom: 0; + } + } + .media-upload-icon, .poll-icon, .emoji-icon { font-size: 1.85em; line-height: 1.1; 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/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.js b/src/components/settings_modal/tabs/general_tab.js index 981da55b..9fe47eaf 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -43,6 +43,11 @@ const GeneralTab = { value: mode, label: this.$t(`settings.third_column_mode_${mode}`) })), + userProfileDefaultTabOptions: ['statuses', 'replies'].map(tab => ({ + key: tab, + value: tab, + label: this.$t(`user_card.${tab}`) + })), loopSilentAvailable: // Firefox Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || @@ -50,6 +55,7 @@ const GeneralTab = { Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') || // Future spec, still not supported in Nightly 63 as of 08/2018 Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks') + } }, components: { @@ -82,11 +88,23 @@ const GeneralTab = { this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) } }, + translationLanguages () { + return (this.$store.getters.mergedConfig.supportedTranslationLanguages.target || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name })) + }, + translationLanguage: { + get: function () { return this.$store.getters.mergedConfig.translationLanguage }, + set: function (val) { + this.$store.dispatch('setOption', { name: 'translationLanguage', value: val }) + } + }, ...SharedComputedObject() }, methods: { changeDefaultScope (value) { this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value }) + }, + setTranslationLanguage (value) { + this.$store.dispatch('setOption', { name: 'translationLanguage', value }) } } } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index b2d06eef..608c73af 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -15,16 +15,51 @@ {{ $t('settings.hide_isp') }} -
  • - - {{ $t('settings.right_sidebar') }} - -
  • {{ $t('settings.hide_wallpaper') }}
  • +
  • + + {{ $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') }} @@ -64,41 +99,7 @@ {{ $t('settings.virtual_scrolling') }}
  • -
  • - - {{ $t('settings.disable_sticky_headers') }} - -
  • -
  • - - {{ $t('settings.show_scrollbars') }} - -
  • -
  • - - {{ $t('settings.third_column_mode') }} - -
  • -
  • - - {{ $t('settings.minimal_scopes_mode') }} - -
  • -
  • - - {{ $t('settings.sensitive_by_default') }} - -
  • -
  • - - {{ $t('settings.sensitive_if_subject') }} - -
  • +
  • {{ $t('settings.render_mfm') }} @@ -116,6 +117,25 @@
  • +
  • + + {{ $t('settings.user_profile_default_tab') }} + +
  • +
  • + + {{ $t('settings.translation_language') }} + +
  • +
  • +

    {{ $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') }} + +
    • +
    +
  • @@ -366,12 +457,22 @@ /> +
  • + + {{ $t('settings.minimal_scopes_mode') }} + +
  • {{ $t('settings.sensitive_by_default') }}
  • +
  • + + {{ $t('settings.sensitive_if_subject') }} + +
  • {{ $t("nav.interactions") }}
  • diff --git a/src/components/status/status.js b/src/components/status/status.js index 2959c3fd..a794b284 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -437,6 +437,12 @@ const Status = { }, visibilityLocalized () { return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility) + }, + isEdited () { + return this.status.edited_at !== null + }, + editingAvailable () { + return this.$store.state.instance.editingAvailable } }, methods: { diff --git a/src/components/status/status.scss b/src/components/status/status.scss index cc9d4eb7..448577d3 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -160,7 +160,8 @@ margin-right: 0.2em; } - & .heading-reply-row { + & .heading-reply-row, + & .heading-edited-row { position: relative; align-content: baseline; font-size: 0.85em; diff --git a/src/components/status/status.vue b/src/components/status/status.vue index aef375fa..a160d197 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -329,6 +329,24 @@ class="mentions-line" />
    +
    + + + +
    fileType.fileType(file.mimetype)) }, + translationLanguages () { + return (this.$store.getters.mergedConfig.supportedTranslationLanguages.source || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name })) + }, ...mapGetters(['mergedConfig']) }, components: { - RichContent + RichContent, + Select }, mounted () { this.status.attentions && this.status.attentions.forEach(attn => { @@ -126,6 +132,10 @@ const StatusContent = { }, generateTagLink (tag) { return `/tag/${tag}` + }, + translateStatus () { + const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage + this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo, from: this.translateFrom }) } } } diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss index 0f946f79..230a27ac 100644 --- a/src/components/status_body/status_body.scss +++ b/src/components/status_body/status_body.scss @@ -4,6 +4,12 @@ display: flex; flex-direction: column; + .translation { + border: 1px solid var(--accent, $fallback--link); + border-radius: var(--panelRadius, $fallback--panelRadius); + margin-top: 1em; + padding: 0.5em; + } .emoji { --_still_image-label-scale: 0.5; --emoji-size: 38px; diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue index 321f3c4b..ed19261d 100644 --- a/src/components/status_body/status_body.vue +++ b/src/components/status_body/status_body.vue @@ -56,6 +56,44 @@ :attentions="status.attentions" @parseReady="onParseReady" /> +
    +

    {{ $t('status.translated_from', { language: status.translation.detected_language }) }}

    + +
    + + {{ ' ' }} + + {{ ' ' }} + +
    +