From 0f2a8c69a967adfdcbdba26d380dfe9327b01638 Mon Sep 17 00:00:00 2001 From: Sam Therapy Date: Fri, 29 Oct 2021 00:43:35 +0000 Subject: [PATCH] Make media modal be aware of multi-touch actions Originally the media viewer would think every touch is a swipe (one-finger touch event), so we would encounter the case where a two-finger scale event would incorrectly change the current media. This is now fixed. --- README.md | 12 + package.json | 1 + src/App.js | 2 +- src/App.vue | 5 +- src/_variables.scss | 2 + src/components/chat/chat_layout.js | 108 +++ src/components/chat_avatar/chat_avatar.js | 34 + src/components/chat_avatar/chat_avatar.vue | 130 +++ .../chat_message_date/chat_message_date.vue | 2 +- src/components/conversation/conversation.js | 377 ++++++++- src/components/conversation/conversation.vue | 253 +++++- src/components/emoji_input/emoji_input.js | 4 +- .../interface_language_switcher.vue | 3 +- src/components/media_modal/media_modal.js | 54 +- src/components/media_modal/media_modal.vue | 65 +- src/components/media_upload/media_upload.vue | 21 +- src/components/nav_panel/nav_panel.js | 3 + src/components/nav_panel/nav_panel.vue | 83 +- src/components/notification/notification.js | 2 +- src/components/notification/notification.vue | 3 + src/components/pinch_zoom/pinch_zoom.js | 13 + src/components/pinch_zoom/pinch_zoom.vue | 11 + .../post_status_form/post_status_form.js | 2 + .../post_status_form/post_status_form.vue | 7 + .../settings_modal/tabs/general_tab.js | 10 + .../settings_modal/tabs/general_tab.vue | 56 ++ .../tabs/security_tab/security_tab.js | 2 +- .../settings_modal/tabs/version_tab.js | 4 +- src/components/status/status.js | 99 ++- src/components/status/status.scss | 9 - src/components/status/status.vue | 52 +- src/components/status_body/status_body.js | 16 +- .../status_content/status_content.js | 48 +- .../status_content/status_content.vue | 163 +++- src/components/swipe_click/swipe_click.js | 84 ++ src/components/swipe_click/swipe_click.vue | 14 + src/components/thread_tree/thread_tree.js | 90 ++ src/components/thread_tree/thread_tree.vue | 128 +++ src/components/timeago/timeago.vue | 2 +- src/components/user_panel/user_panel.js | 3 + src/components/user_panel/user_panel.vue | 23 +- src/i18n/en.json | 26 +- src/i18n/en_nyan.json | 775 ++++++++++++++++++ src/i18n/messages.js | 1 + src/main.js | 2 + src/modules/config.js | 12 +- src/modules/instance.js | 3 + src/modules/statuses.js | 8 + src/modules/users.js | 6 + .../entity_normalizer.service.js | 1 + .../gesture_service/gesture_service.js | 151 +++- static/themes/breezy-dark.json | 3 +- yarn.lock | 11 + 53 files changed, 2876 insertions(+), 123 deletions(-) create mode 100644 src/components/chat/chat_layout.js create mode 100644 src/components/chat_avatar/chat_avatar.js create mode 100644 src/components/chat_avatar/chat_avatar.vue create mode 100644 src/components/pinch_zoom/pinch_zoom.js create mode 100644 src/components/pinch_zoom/pinch_zoom.vue create mode 100644 src/components/swipe_click/swipe_click.js create mode 100644 src/components/swipe_click/swipe_click.vue create mode 100644 src/components/thread_tree/thread_tree.js create mode 100644 src/components/thread_tree/thread_tree.vue create mode 100644 src/i18n/en_nyan.json diff --git a/README.md b/README.md index 54529a70..602f2cdd 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,18 @@ ![screenshot](/uploads/796c5ecf985ed1e2b0943ee0df131ed0/DJVqSJ0.png) +# Changes in this Fork + +* script tag in index.html for [pleroma-mod-loader](https://git.pleroma.social/absturztaube/pleroma-mod-loader) +* ability to move notifications to a seperate column +* insert zero width space when padding of emojis is disabled +* add custom language "English (Nyan)" +* pointing version links to my gitlab repos +* optional compact styles provided by craftplacer +* tags as buttons bellow a post +* [pinch and pan media](https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1403) +* swap of react and favorite button in status + # For Translators To translate Pleroma-FE, add your language to [src/i18n/messages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/messages.js). Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js. diff --git a/package.json b/package.json index 5134a8b1..77c2be36 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@fortawesome/free-regular-svg-icons": "^5.15.1", "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/vue-fontawesome": "^2.0.0", + "@kazvmoe-infra/pinch-zoom-element": "https://lily.kazv.moe/infra/pinch-zoom-element.git", "body-scroll-lock": "^2.6.4", "chromatism": "^3.0.0", "cropperjs": "^1.4.3", diff --git a/src/App.js b/src/App.js index abc2656f..3e09ed4c 100644 --- a/src/App.js +++ b/src/App.js @@ -89,7 +89,7 @@ export default { 'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0 } }, - notifsAlign () { + thirdColumnAlign () { return { 'order': this.$store.getters.mergedConfig.sidebarRight ? 0 : 99 } diff --git a/src/App.vue b/src/App.vue index e8f80262..6955a4bc 100644 --- a/src/App.vue +++ b/src/App.vue @@ -26,9 +26,9 @@
- +
@@ -52,8 +52,8 @@ - { + this.updateSize() + }) + }, + unsetMobileChatLayout () { + let html = document.querySelector('html') + if (html) { + html.style.overflow = 'visible' + html.style.height = 'unset' + } + + let body = document.querySelector('body') + if (body) { + body.style.height = 'unset' + } + + let app = document.getElementById('app') + if (app) { + app.style.height = '100%' + app.style.overflow = 'visible' + app.style.minHeight = '100vh' + } + + let appBgWrapper = document.getElementById('app_bg_wrapper') + if (appBgWrapper) { + appBgWrapper.style.overflow = 'visible' + } + + let main = document.getElementsByClassName('main')[0] + if (main) { + main.style.overflow = 'visible' + main.style.height = 'unset' + } + + let content = document.getElementById('content') + if (content) { + content.style.paddingTop = '60px' + content.style.height = 'unset' + content.style.overflow = 'unset' + } + } + } +} + +export default ChatLayout diff --git a/src/components/chat_avatar/chat_avatar.js b/src/components/chat_avatar/chat_avatar.js new file mode 100644 index 00000000..8603a2cc --- /dev/null +++ b/src/components/chat_avatar/chat_avatar.js @@ -0,0 +1,34 @@ +import StillImage from '../still-image/still-image.vue' +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import { mapState } from 'vuex' + +const ChatAvatar = { + props: ['users', 'fallbackUser', 'width', 'height'], + components: { + StillImage + }, + methods: { + getUserProfileLink (user) { + return generateProfileLink(user.id, user.screen_name) + } + }, + computed: { + firstUser () { + return this.users[0] || this.fallbackUser + }, + secondUser () { + return this.users[1] + }, + thirdUser () { + return this.users[2] + }, + fourthUser () { + return this.users[3] + }, + ...mapState({ + betterShadow: state => state.interface.browserSupport.cssFilter + }) + } +} + +export default ChatAvatar diff --git a/src/components/chat_avatar/chat_avatar.vue b/src/components/chat_avatar/chat_avatar.vue new file mode 100644 index 00000000..e39b998e --- /dev/null +++ b/src/components/chat_avatar/chat_avatar.vue @@ -0,0 +1,130 @@ + + + + diff --git a/src/components/chat_message_date/chat_message_date.vue b/src/components/chat_message_date/chat_message_date.vue index 98349b75..0e689322 100644 --- a/src/components/chat_message_date/chat_message_date.vue +++ b/src/components/chat_message_date/chat_message_date.vue @@ -18,7 +18,7 @@ export default { if (this.date.getTime() === today.getTime()) { return this.$t('display_date.today') } else { - return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' }) + return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale.split('_')[0]), { day: 'numeric', month: 'long' }) } } } diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 069c0b40..b9ebe7eb 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,5 +1,19 @@ import { reduce, filter, findIndex, clone, get } from 'lodash' import Status from '../status/status.vue' +import ThreadTree from '../thread_tree/thread_tree.vue' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faAngleDoubleDown, + faAngleDoubleLeft, + faChevronLeft +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faAngleDoubleDown, + faAngleDoubleLeft, + faChevronLeft +) const sortById = (a, b) => { const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id @@ -35,7 +49,10 @@ const conversation = { data () { return { highlight: null, - expanded: false + expanded: false, + threadDisplayStatusObject: {}, // id => 'showing' | 'hidden' + statusContentPropertiesObject: {}, + inlineDivePosition: null } }, props: [ @@ -53,12 +70,47 @@ const conversation = { } }, computed: { - hideStatus () { - if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { - return this.virtualHidden && this.$refs.statusComponent[0].suspendable - } else { - return this.virtualHidden + maxDepthToShowByDefault () { + // maxDepthInThread = max number of depths that is *visible* + // since our depth starts with 0 and "showing" means "showing children" + // there is a -2 here + const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2 + return maxDepth >= 1 ? maxDepth : 1 + }, + displayStyle () { + return this.$store.getters.mergedConfig.conversationDisplay + }, + isTreeView () { + return this.displayStyle === 'tree' || this.displayStyle === 'simple_tree' + }, + treeViewIsSimple () { + return this.displayStyle === 'simple_tree' + }, + isLinearView () { + return this.displayStyle === 'linear' + }, + otherRepliesButtonPosition () { + return this.$store.getters.mergedConfig.conversationOtherRepliesButton + }, + showOtherRepliesButtonBelowStatus () { + return this.otherRepliesButtonPosition === 'below' + }, + showOtherRepliesButtonInsideStatus () { + return this.otherRepliesButtonPosition === 'inside' + }, + suspendable () { + if (this.isTreeView) { + return Object.entries(this.statusContentProperties) + .every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0) } + if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { + return this.$refs.statusComponent.every(s => s.suspendable) + } else { + return true + } + }, + hideStatus () { + return this.virtualHidden && this.suspendable }, status () { return this.$store.state.statuses.allStatusesObject[this.statusId] @@ -90,6 +142,123 @@ const conversation = { return sortAndFilterConversation(conversation, this.status) }, + conversationDive () { + }, + statusMap () { + return this.conversation.reduce((res, s) => { + res[s.id] = s + return res + }, {}) + }, + threadTree () { + const reverseLookupTable = this.conversation.reduce((table, status, index) => { + table[status.id] = index + return table + }, {}) + + const threads = this.conversation.reduce((a, cur) => { + const id = cur.id + a.forest[id] = this.getReplies(id) + .map(s => s.id) + + return a + }, { + forest: {} + }) + + const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => { + if (processed[id]) { + return [] + } + + processed[id] = true + return [{ + status: this.conversation[reverseLookupTable[id]], + id, + depth + }, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), []) + }).reduce((a, b) => a.concat(b), []) + + const linearized = walk(threads.forest, this.topLevel.map(k => k.id)) + + return linearized + }, + replyIds () { + return this.conversation.map(k => k.id) + .reduce((res, id) => { + res[id] = (this.replies[id] || []).map(k => k.id) + return res + }, {}) + }, + totalReplyCount () { + const sizes = {} + const subTreeSizeFor = (id) => { + if (sizes[id]) { + return sizes[id] + } + sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0) + return sizes[id] + } + this.conversation.map(k => k.id).map(subTreeSizeFor) + return Object.keys(sizes).reduce((res, id) => { + res[id] = sizes[id] - 1 // exclude itself + return res + }, {}) + }, + totalReplyDepth () { + const depths = {} + const subTreeDepthFor = (id) => { + if (depths[id]) { + return depths[id] + } + depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0) + return depths[id] + } + this.conversation.map(k => k.id).map(subTreeDepthFor) + return Object.keys(depths).reduce((res, id) => { + res[id] = depths[id] - 1 // exclude itself + return res + }, {}) + }, + depths () { + return this.threadTree.reduce((a, k) => { + a[k.id] = k.depth + return a + }, {}) + }, + topLevel () { + const topLevel = this.conversation.reduce((tl, cur) => + tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation) + return topLevel + }, + otherTopLevelCount () { + return this.topLevel.length - 1 + }, + showingTopLevel () { + if (this.canDive && this.diveRoot) { + return [this.statusMap[this.diveRoot]] + } + return this.topLevel + }, + diveRoot () { + const statusId = this.inlineDivePosition || this.statusId + const isTopLevel = !this.parentOf(statusId) + return isTopLevel ? null : statusId + }, + diveDepth () { + return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0 + }, + diveMode () { + return this.canDive && !!this.diveRoot + }, + shouldShowAllConversationButton () { + // The "show all conversation" button tells the user that there exist + // other toplevel statuses, so do not show it if there is only a single root + return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1 + }, + shouldShowAncestors () { + return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length + }, replies () { let i = 1 // eslint-disable-next-line camelcase @@ -109,15 +278,71 @@ const conversation = { }, {}) }, isExpanded () { - return this.expanded || this.isPage + return !!(this.expanded || this.isPage) }, hiddenStyle () { const height = (this.status && this.status.virtualHeight) || '120px' return this.virtualHidden ? { height } : {} + }, + threadDisplayStatus () { + return this.conversation.reduce((a, k) => { + const id = k.id + const depth = this.depths[id] + const status = (() => { + if (this.threadDisplayStatusObject[id]) { + return this.threadDisplayStatusObject[id] + } + if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) { + return 'showing' + } else { + return 'hidden' + } + })() + + a[id] = status + return a + }, {}) + }, + statusContentProperties () { + return this.conversation.reduce((a, k) => { + const id = k.id + const props = (() => { + const def = { + showingTall: false, + expandingSubject: false, + showingLongSubject: false, + isReplying: false, + mediaPlaying: [] + } + + if (this.statusContentPropertiesObject[id]) { + return { + ...def, + ...this.statusContentPropertiesObject[id] + } + } + return def + })() + + a[id] = props + return a + }, {}) + }, + canDive () { + return this.isTreeView && this.isExpanded + }, + focused () { + return (id) => { + return (this.isExpanded) && id === this.highlight + } + }, + maybeHighlight () { + return this.isExpanded ? this.highlight : null } }, components: { - Status + Status, + ThreadTree }, watch: { statusId (newVal, oldVal) { @@ -132,6 +357,8 @@ const conversation = { expanded (value) { if (value) { this.fetchConversation() + } else { + this.resetDisplayState() } }, virtualHidden (value) { @@ -161,8 +388,8 @@ const conversation = { getReplies (id) { return this.replies[id] || [] }, - focused (id) { - return (this.isExpanded) && id === this.statusId + getHighlight () { + return this.isExpanded ? this.highlight : null }, setHighlight (id) { if (!id) return @@ -170,15 +397,139 @@ const conversation = { this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchEmojiReactionsBy', id) }, - getHighlight () { - return this.isExpanded ? this.highlight : null - }, toggleExpanded () { this.expanded = !this.expanded }, getConversationId (statusId) { const status = this.$store.state.statuses.allStatusesObject[statusId] return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id')) + }, + setThreadDisplay (id, nextStatus) { + this.threadDisplayStatusObject = { + ...this.threadDisplayStatusObject, + [id]: nextStatus + } + }, + toggleThreadDisplay (id) { + const curStatus = this.threadDisplayStatus[id] + const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing' + this.setThreadDisplay(id, nextStatus) + }, + setThreadDisplayRecursively (id, nextStatus) { + this.setThreadDisplay(id, nextStatus) + this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus)) + }, + showThreadRecursively (id) { + this.setThreadDisplayRecursively(id, 'showing') + }, + setStatusContentProperty (id, name, value) { + this.statusContentPropertiesObject = { + ...this.statusContentPropertiesObject, + [id]: { + ...this.statusContentPropertiesObject[id], + [name]: value + } + } + }, + toggleStatusContentProperty (id, name) { + this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name]) + }, + leastVisibleAncestor (id) { + let cur = id + let parent = this.parentOf(cur) + while (cur) { + // if the parent is showing it means cur is visible + if (this.threadDisplayStatus[parent] === 'showing') { + return cur + } + parent = this.parentOf(parent) + cur = this.parentOf(cur) + } + // nothing found, fall back to toplevel + return this.topLevel[0] ? this.topLevel[0].id : undefined + }, + diveIntoStatus (id, preventScroll) { + this.tryScrollTo(id) + }, + diveToTopLevel () { + this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id) + }, + // only used when we are not on a page + undive () { + this.inlineDivePosition = null + this.setHighlight(this.statusId) + }, + tryScrollTo (id) { + if (!id) { + return + } + if (this.isPage) { + // set statusId + this.$router.push({ name: 'conversation', params: { id } }) + } else { + this.inlineDivePosition = id + } + // Because the conversation can be unmounted when out of sight + // and mounted again when it comes into sight, + // the `mounted` or `created` function in `status` should not + // contain scrolling calls, as we do not want the page to jump + // when we scroll with an expanded conversation. + // + // Now the method is to rely solely on the `highlight` watcher + // in `status` components. + // In linear views, all statuses are rendered at all times, but + // in tree views, it is possible that a change in active status + // removes and adds status components (e.g. an originally child + // status becomes an ancestor status, and thus they will be + // different). + // Here, let the components be rendered first, in order to trigger + // the `highlight` watcher. + this.$nextTick(() => { + this.setHighlight(id) + }) + }, + goToCurrent () { + this.tryScrollTo(this.diveRoot || this.topLevel[0].id) + }, + statusById (id) { + return this.statusMap[id] + }, + parentOf (id) { + const status = this.statusById(id) + if (!status) { + return undefined + } + const { in_reply_to_status_id: parentId } = status + if (!this.statusMap[parentId]) { + return undefined + } + return parentId + }, + parentOrSelf (id) { + return this.parentOf(id) || id + }, + // Ancestors of some status, from top to bottom + ancestorsOf (id) { + const ancestors = [] + let cur = this.parentOf(id) + while (cur) { + ancestors.unshift(this.statusMap[cur]) + cur = this.parentOf(cur) + } + return ancestors + }, + topLevelAncestorOrSelfId (id) { + let cur = id + let parent = this.parentOf(id) + while (parent) { + cur = this.parentOf(cur) + parent = this.parentOf(parent) + } + return cur + }, + resetDisplayState () { + this.undive() + this.threadDisplayStatusObject = {} } } } diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 3fb26d92..4d64cf08 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -18,24 +18,168 @@ {{ $t('timeline.collapse') }} - +
+
+ + + + {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }} + + +
+
+
+
+ +
+
+ + + + {{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }} + + +
+
+
+
+ +
+
+ +
+
.conversation-status { + border-top-width: 1px; + border-top-style: solid; + border-top-color: var(--border, $fallback--border); + } + + /* expanded conversation in timeline */ + &.status-fadein.-expanded .thread-body { + border-left-width: 4px; + border-left-style: solid; + border-left-color: $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); + } + /* &.-expanded { */ + /* .conversation-status:last-child { */ + /* border-bottom: none; */ + /* } */ + /* } */ } diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index 902ec384..af6283d7 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -248,8 +248,8 @@ const EmojiInput = { * them, masto seem to be rendering :emoji::emoji: correctly now so why not */ const isSpaceRegex = /\s/ - const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : '' - const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : '' + const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : '​' + const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : '​' const newValue = [ before, diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue index cf307a24..10ab22a5 100644 --- a/src/components/interface_language_switcher/interface_language_switcher.vue +++ b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -45,13 +45,14 @@ export default { methods: { getLanguageName (code) { const specialLanguageNames = { + 'en_nyan': 'English (Nyan)', 'ja_easy': 'やさしいにほんご', 'zh': '简体中文', 'zh_Hant': '繁體中文' } const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code) const browserLocale = localeService.internalToBrowserLocale(code) - return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1) + return languageName.charAt(0).toLocaleUpperCase(browserLocale.split('_')[0]) + languageName.slice(1) } } } diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index e7384c93..b44a2b63 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -1,8 +1,10 @@ import StillImage from '../still-image/still-image.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' import Modal from '../modal/modal.vue' -import fileTypeService from '../../services/file_type/file_type.service.js' +import PinchZoom from '../pinch_zoom/pinch_zoom.vue' +import SwipeClick from '../swipe_click/swipe_click.vue' import GestureService from '../../services/gesture_service/gesture_service' +import fileTypeService from '../../services/file_type/file_type.service.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronLeft, @@ -18,6 +20,8 @@ const MediaModal = { components: { StillImage, VideoAttachment, + PinchZoom, + SwipeClick, Modal }, computed: { @@ -40,29 +44,25 @@ const MediaModal = { return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null } }, - created () { - this.mediaSwipeGestureRight = GestureService.swipeGesture( - GestureService.DIRECTION_RIGHT, - this.goPrev, - 50 - ) - this.mediaSwipeGestureLeft = GestureService.swipeGesture( - GestureService.DIRECTION_LEFT, - this.goNext, - 50 - ) + data () { + return { + swipeDirection: GestureService.DIRECTION_LEFT, + swipeThreshold: () => { + const considerableMoveRatio = 1 / 4 + return window.innerWidth * considerableMoveRatio + }, + pinchZoomMinScale: 1, + pinchZoomScaleResetLimit: 1.2 + } }, methods: { - mediaTouchStart (e) { - GestureService.beginSwipe(e, this.mediaSwipeGestureRight) - GestureService.beginSwipe(e, this.mediaSwipeGestureLeft) - }, - mediaTouchMove (e) { - GestureService.updateSwipe(e, this.mediaSwipeGestureRight) - GestureService.updateSwipe(e, this.mediaSwipeGestureLeft) - }, hide () { - this.$store.dispatch('closeMediaViewer') + // HACK: Closing immediately via a touch will cause the click + // to be processed on the content below the overlay + const transitionTime = 100 // ms + setTimeout(() => { + this.$store.dispatch('closeMediaViewer') + }, transitionTime) }, goPrev () { if (this.canNavigate) { @@ -76,6 +76,18 @@ const MediaModal = { this.$store.dispatch('setCurrent', this.media[nextIndex]) } }, + handleSwipePreview (offsets) { + this.$refs.pinchZoom.setTransform({ scale: 1, x: offsets[0], y: 0 }) + }, + handleSwipeEnd (sign) { + console.log('handleSwipeEnd:', sign) + this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 }) + if (sign > 0) { + this.goNext() + } else if (sign < 0) { + this.goPrev() + } + }, handleKeyupEvent (e) { if (this.showing && e.keyCode === 27) { // escape this.hide() diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 54bc5335..6a6eff45 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -4,16 +4,33 @@ class="media-modal-view" @backdropClicked="hide" > - + + + + diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index 37bcb409..4b7fa488 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -48,6 +48,9 @@ const NavPanel = { } }, computed: { + compactNavPanel () { + return this.$store.getters.mergedConfig.compactNavPanel || false + }, ...mapState({ currentUser: state => state.users.currentUser, followRequestCount: state => state.api.followRequests.length, diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 7ae7b1d6..e59036a3 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -1,5 +1,8 @@