aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/boot/after_store.js1
-rw-r--r--src/components/media_modal/media_modal.js68
-rw-r--r--src/components/media_modal/media_modal.vue147
-rw-r--r--src/components/pinch_zoom/pinch_zoom.js13
-rw-r--r--src/components/pinch_zoom/pinch_zoom.vue11
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.vue10
-rw-r--r--src/components/status/status.js16
-rw-r--r--src/components/status/status.vue7
-rw-r--r--src/components/still-image/still-image.vue1
-rw-r--r--src/components/swipe_click/swipe_click.js84
-rw-r--r--src/components/swipe_click/swipe_click.vue14
-rw-r--r--src/components/timeline/timeline_quick_settings.js7
-rw-r--r--src/components/timeline/timeline_quick_settings.vue9
-rw-r--r--src/components/user_avatar/user_avatar.js13
-rw-r--r--src/components/user_avatar/user_avatar.vue10
-rw-r--r--src/i18n/en.json5
-rw-r--r--src/main.js2
-rw-r--r--src/modules/config.js2
-rw-r--r--src/modules/instance.js2
-rw-r--r--src/services/gesture_service/gesture_service.js137
20 files changed, 494 insertions, 65 deletions
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index cc0c7c5e..c4a0a800 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -115,6 +115,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('nsfwCensorImage')
copyInstanceOption('background')
copyInstanceOption('hidePostStats')
+ copyInstanceOption('hideBotIndication')
copyInstanceOption('hideUserStats')
copyInstanceOption('hideFilteredStatuses')
copyInstanceOption('logo')
diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js
index b8bce730..01a90377 100644
--- a/src/components/media_modal/media_modal.js
+++ b/src/components/media_modal/media_modal.js
@@ -1,32 +1,45 @@
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 Flash from 'src/components/flash/flash.vue'
+import fileTypeService from '../../services/file_type/file_type.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronLeft,
faChevronRight,
- faCircleNotch
+ faCircleNotch,
+ faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add(
faChevronLeft,
faChevronRight,
- faCircleNotch
+ faCircleNotch,
+ faTimes
)
const MediaModal = {
components: {
StillImage,
VideoAttachment,
+ PinchZoom,
+ SwipeClick,
Modal,
Flash
},
data () {
return {
- loading: false
+ loading: false,
+ swipeDirection: GestureService.DIRECTION_LEFT,
+ swipeThreshold: () => {
+ const considerableMoveRatio = 1 / 4
+ return window.innerWidth * considerableMoveRatio
+ },
+ pinchZoomMinScale: 1,
+ pinchZoomScaleResetLimit: 1.2
}
},
computed: {
@@ -52,32 +65,26 @@ const MediaModal = {
return this.currentMedia ? this.getType(this.currentMedia) : null
}
},
- created () {
- this.mediaSwipeGestureRight = GestureService.swipeGesture(
- GestureService.DIRECTION_RIGHT,
- this.goPrev,
- 50
- )
- this.mediaSwipeGestureLeft = GestureService.swipeGesture(
- GestureService.DIRECTION_LEFT,
- this.goNext,
- 50
- )
- },
methods: {
getType (media) {
return fileTypeService.fileType(media.mimetype)
},
- 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)
+ },
+ hideIfNotSwiped (event) {
+ // If we have swiped over SwipeClick, do not trigger hide
+ const comp = this.$refs.swipeClick
+ if (!comp) {
+ this.hide()
+ } else {
+ comp.$gesture.click(event)
+ }
},
goPrev () {
if (this.canNavigate) {
@@ -102,6 +109,17 @@ const MediaModal = {
onImageLoaded () {
this.loading = false
},
+ handleSwipePreview (offsets) {
+ this.$refs.pinchZoom.setTransform({ scale: 1, x: offsets[0], y: 0 })
+ },
+ 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 8680267b..708a43c6 100644
--- a/src/components/media_modal/media_modal.vue
+++ b/src/components/media_modal/media_modal.vue
@@ -2,20 +2,38 @@
<Modal
v-if="showing"
class="media-modal-view"
- @backdropClicked="hide"
+ @backdropClicked="hideIfNotSwiped"
>
- <img
+ <SwipeClick
v-if="type === 'image'"
- :class="{ loading }"
- class="modal-image"
- :src="currentMedia.url"
- :alt="currentMedia.description"
- :title="currentMedia.description"
- @touchstart.stop="mediaTouchStart"
- @touchmove.stop="mediaTouchMove"
- @click="hide"
- @load="onImageLoaded"
+ ref="swipeClick"
+ class="modal-image-container"
+ :direction="swipeDirection"
+ :threshold="swipeThreshold"
+ @preview-requested="handleSwipePreview"
+ @swipe-finished="handleSwipeEnd"
+ @swipeless-clicked="hide"
>
+ <PinchZoom
+ ref="pinchZoom"
+ class="modal-image-container-inner"
+ selector=".modal-image"
+ reach-min-scale-strategy="reset"
+ stop-propagate-handled="stop-propgate-handled"
+ :allow-pan-min-scale="pinchZoomMinScale"
+ :min-scale="pinchZoomMinScale"
+ :reset-to-min-scale-limit="pinchZoomScaleResetLimit"
+ >
+ <img
+ :class="{ loading }"
+ class="modal-image"
+ :src="currentMedia.url"
+ :alt="currentMedia.description"
+ :title="currentMedia.description"
+ @load="onImageLoaded"
+ >
+ </PinchZoom>
+ </SwipeClick>
<VideoAttachment
v-if="type === 'video'"
class="modal-image"
@@ -40,25 +58,36 @@
<button
v-if="canNavigate"
:title="$t('media_modal.previous')"
- class="modal-view-button-arrow modal-view-button-arrow--prev"
+ class="modal-view-button modal-view-button-arrow modal-view-button-arrow--prev"
@click.stop.prevent="goPrev"
>
<FAIcon
- class="arrow-icon"
+ class="button-icon arrow-icon"
icon="chevron-left"
/>
</button>
<button
v-if="canNavigate"
:title="$t('media_modal.next')"
- class="modal-view-button-arrow modal-view-button-arrow--next"
+ class="modal-view-button modal-view-button-arrow modal-view-button-arrow--next"
@click.stop.prevent="goNext"
>
<FAIcon
- class="arrow-icon"
+ class="button-icon arrow-icon"
icon="chevron-right"
/>
</button>
+ <button
+ class="modal-view-button modal-view-button-hide"
+ :title="$t('media_modal.hide')"
+ @click.stop.prevent="hide"
+ >
+ <FAIcon
+ class="button-icon"
+ icon="times"
+ />
+ </button>
+
<span
v-if="description"
class="description"
@@ -86,11 +115,17 @@
<script src="./media_modal.js"></script>
<style lang="scss">
+$modal-view-button-icon-height: 3em;
+$modal-view-button-icon-half-height: calc(#{$modal-view-button-icon-height} / 2);
+$modal-view-button-icon-width: 3em;
+$modal-view-button-icon-margin: 0.5em;
+
.modal-view.media-modal-view {
z-index: 1001;
flex-direction: column;
- .modal-view-button-arrow {
+ .modal-view-button-arrow,
+ .modal-view-button-hide {
opacity: 0.75;
&:focus,
@@ -103,6 +138,7 @@
opacity: 1;
}
}
+ overflow: hidden;
}
.media-modal-view {
@@ -115,6 +151,29 @@
}
}
+ .modal-image-container {
+ display: flex;
+ overflow: hidden;
+ align-items: center;
+ flex-direction: column;
+ max-width: 100%;
+ max-height: 100%;
+ width: 100%;
+ height: 100%;
+ flex-grow: 1;
+ justify-content: center;
+
+ &-inner {
+ width: 100%;
+ height: 100%;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+
.description,
.counter {
/* Hardcoded since background is also hardcoded */
@@ -134,9 +193,8 @@
}
.modal-image {
- max-width: 90%;
- max-height: 90%;
- box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
+ max-width: 100%;
+ max-height: 100%;
image-orientation: from-image; // NOTE: only FF supports this
animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;
@@ -159,13 +217,7 @@
}
}
- .modal-view-button-arrow {
- position: absolute;
- display: block;
- top: 50%;
- margin-top: -50px;
- width: 70px;
- height: 100px;
+ .modal-view-button {
border: 0;
padding: 0;
opacity: 0;
@@ -175,14 +227,33 @@
overflow: visible;
cursor: pointer;
transition: opacity 333ms cubic-bezier(.4,0,.22,1);
+ height: $modal-view-button-icon-height;
+ width: $modal-view-button-icon-width;
- .arrow-icon {
+ .button-icon {
position: absolute;
- top: 35px;
- height: 30px;
- width: 32px;
+ height: $modal-view-button-icon-height;
+ width: $modal-view-button-icon-width;
font-size: 14px;
- line-height: 30px;
+ line-height: $modal-view-button-icon-height;
+ color: #FFF;
+ text-align: center;
+ background-color: rgba(0,0,0,.3);
+ }
+ }
+
+ .modal-view-button-arrow {
+ position: absolute;
+ display: block;
+ top: 50%;
+ margin-top: $modal-view-button-icon-half-height;
+ width: $modal-view-button-icon-width;
+ height: $modal-view-button-icon-height;
+
+ .arrow-icon {
+ position: absolute;
+ top: 0;
+ line-height: $modal-view-button-icon-height;
color: #FFF;
text-align: center;
background-color: rgba(0,0,0,.3);
@@ -191,16 +262,26 @@
&--prev {
left: 0;
.arrow-icon {
- left: 6px;
+ left: $modal-view-button-icon-margin;
}
}
&--next {
right: 0;
.arrow-icon {
- right: 6px;
+ right: $modal-view-button-icon-margin;
}
}
}
+
+ .modal-view-button-hide {
+ position: absolute;
+ top: 0;
+ right: 0;
+ .button-icon {
+ top: $modal-view-button-icon-margin;
+ right: $modal-view-button-icon-margin;
+ }
+ }
}
</style>
diff --git a/src/components/pinch_zoom/pinch_zoom.js b/src/components/pinch_zoom/pinch_zoom.js
new file mode 100644
index 00000000..82670ddf
--- /dev/null
+++ b/src/components/pinch_zoom/pinch_zoom.js
@@ -0,0 +1,13 @@
+import PinchZoom from '@kazvmoe-infra/pinch-zoom-element'
+
+export default {
+ methods: {
+ setTransform ({ scale, x, y }) {
+ this.$el.setTransform({ scale, x, y })
+ }
+ },
+ created () {
+ // Make lint happy
+ (() => PinchZoom)()
+ }
+}
diff --git a/src/components/pinch_zoom/pinch_zoom.vue b/src/components/pinch_zoom/pinch_zoom.vue
new file mode 100644
index 00000000..18d69719
--- /dev/null
+++ b/src/components/pinch_zoom/pinch_zoom.vue
@@ -0,0 +1,11 @@
+<template>
+ <pinch-zoom
+ class="pinch-zoom-parent"
+ v-bind="$attrs"
+ v-on="$listeners"
+ >
+ <slot />
+ </pinch-zoom>
+</template>
+
+<script src="./pinch_zoom.js"></script>
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index eb1705e7..0774c818 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -38,10 +38,20 @@
</ul>
</li>
<li>
+ <BooleanSetting path="muteBotStatuses">
+ {{ $t('settings.mute_bot_posts') }}
+ </BooleanSetting>
+ </li>
+ <li>
<BooleanSetting path="hidePostStats">
{{ $t('settings.hide_post_stats') }}
</BooleanSetting>
</li>
+ <li>
+ <BooleanSetting path="hideBotIndication">
+ {{ $t('settings.hide_bot_indication') }}
+ </BooleanSetting>
+ </li>
<ChoiceSetting
id="replyVisibility"
path="replyVisibility"
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 7bdcb665..4c0ef3e0 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -225,6 +225,12 @@ const Status = {
muteWordHits () {
return muteWordHits(this.status, this.muteWords)
},
+ botStatus () {
+ return this.status.user.bot
+ },
+ botIndicator () {
+ return this.botStatus && !this.hideBotIndication
+ },
mentionsLine () {
if (!this.headTailLinks) return []
const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url))
@@ -250,7 +256,9 @@ const Status = {
// Thread is muted
status.thread_muted ||
// Wordfiltered
- this.muteWordHits.length > 0
+ this.muteWordHits.length > 0 ||
+ // bot status
+ (this.muteBotStatuses && this.botStatus && !this.compact)
return !this.unmuted && !this.shouldNotMute && reasonsToMute
},
userIsMuted () {
@@ -352,6 +360,12 @@ const Status = {
hidePostStats () {
return this.mergedConfig.hidePostStats
},
+ muteBotStatuses () {
+ return this.mergedConfig.muteBotStatuses
+ },
+ hideBotIndication () {
+ return this.mergedConfig.hideBotIndication
+ },
currentUser () {
return this.$store.state.users.currentUser
},
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 45a21d33..1679834e 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -77,6 +77,7 @@
<UserAvatar
v-if="retweet"
class="left-side repeater-avatar"
+ :bot="botIndicator"
:better-shadow="betterShadow"
:user="statusoid.user"
/>
@@ -124,6 +125,7 @@
@click.stop.prevent.capture.native="toggleUserExpanded"
>
<UserAvatar
+ :bot="botIndicator"
:compact="compact"
:better-shadow="betterShadow"
:user="status.user"
@@ -451,7 +453,10 @@
class="gravestone"
>
<div class="left-side">
- <UserAvatar :compact="compact" />
+ <UserAvatar
+ :compact="compact"
+ :bot="botIndicator"
+ />
</div>
<div class="right-side">
<div class="deleted-text">
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index cca75fcb..4ea21506 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -19,6 +19,7 @@
@load="onLoad"
@error="onError"
>
+ <slot/>
</div>
</template>
diff --git a/src/components/swipe_click/swipe_click.js b/src/components/swipe_click/swipe_click.js
new file mode 100644
index 00000000..238e6df8
--- /dev/null
+++ b/src/components/swipe_click/swipe_click.js
@@ -0,0 +1,84 @@
+import GestureService from '../../services/gesture_service/gesture_service'
+
+/**
+ * props:
+ * direction: a vector that indicates the direction of the intended swipe
+ * threshold: the minimum distance in pixels the swipe has moved on `direction'
+ * for swipe-finished() to have a non-zero sign
+ * perpendicularTolerance: see gesture_service
+ *
+ * Events:
+ * preview-requested(offsets)
+ * Emitted when the pointer has moved.
+ * offsets: the offsets from the start of the swipe to the current cursor position
+ *
+ * swipe-canceled()
+ * Emitted when the swipe has been canceled due to a pointercancel event.
+ *
+ * swipe-finished(sign: 0|-1|1)
+ * Emitted when the swipe has finished.
+ * sign: if the swipe does not meet the threshold, 0
+ * if the swipe meets the threshold in the positive direction, 1
+ * if the swipe meets the threshold in the negative direction, -1
+ *
+ * swipeless-clicked()
+ * Emitted when there is a click without swipe.
+ * This and swipe-finished() cannot be emitted for the same pointerup event.
+ */
+const SwipeClick = {
+ props: {
+ direction: {
+ type: Array
+ },
+ threshold: {
+ type: Function,
+ default: () => 30
+ },
+ perpendicularTolerance: {
+ type: Number,
+ default: 1.0
+ }
+ },
+ methods: {
+ handlePointerDown (event) {
+ this.$gesture.start(event)
+ },
+ handlePointerMove (event) {
+ this.$gesture.move(event)
+ },
+ handlePointerUp (event) {
+ this.$gesture.end(event)
+ },
+ handlePointerCancel (event) {
+ this.$gesture.cancel(event)
+ },
+ handleNativeClick (event) {
+ this.$gesture.click(event)
+ },
+ preview (offsets) {
+ this.$emit('preview-requested', offsets)
+ },
+ end (sign) {
+ this.$emit('swipe-finished', sign)
+ },
+ click () {
+ this.$emit('swipeless-clicked')
+ },
+ cancel () {
+ this.$emit('swipe-canceled')
+ }
+ },
+ created () {
+ this.$gesture = new GestureService.SwipeAndClickGesture({
+ direction: this.direction,
+ threshold: this.threshold,
+ perpendicularTolerance: this.perpendicularTolerance,
+ swipePreviewCallback: this.preview,
+ swipeEndCallback: this.end,
+ swipeCancelCallback: this.cancel,
+ swipelessClickCallback: this.click
+ })
+ }
+}
+
+export default SwipeClick
diff --git a/src/components/swipe_click/swipe_click.vue b/src/components/swipe_click/swipe_click.vue
new file mode 100644
index 00000000..5372071d
--- /dev/null
+++ b/src/components/swipe_click/swipe_click.vue
@@ -0,0 +1,14 @@
+<template>
+ <div
+ v-bind="$attrs"
+ @pointerdown="handlePointerDown"
+ @pointermove="handlePointerMove"
+ @pointerup="handlePointerUp"
+ @pointercancel="handlePointerCancel"
+ @click="handleNativeClick"
+ >
+ <slot />
+ </div>
+</template>
+
+<script src="./swipe_click.js"></script>
diff --git a/src/components/timeline/timeline_quick_settings.js b/src/components/timeline/timeline_quick_settings.js
index 7b4931ce..92d5ac14 100644
--- a/src/components/timeline/timeline_quick_settings.js
+++ b/src/components/timeline/timeline_quick_settings.js
@@ -53,6 +53,13 @@ const TimelineQuickSettings = {
const value = !this.hideMutedPosts
this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
}
+ },
+ muteBotStatuses: {
+ get () { return this.mergedConfig.muteBotStatuses },
+ set () {
+ const value = !this.muteBotStatuses
+ this.$store.dispatch('setOption', { name: 'muteBotStatuses', value })
+ }
}
}
}
diff --git a/src/components/timeline/timeline_quick_settings.vue b/src/components/timeline/timeline_quick_settings.vue
index 98996ebd..4d67e06b 100644
--- a/src/components/timeline/timeline_quick_settings.vue
+++ b/src/components/timeline/timeline_quick_settings.vue
@@ -41,6 +41,15 @@
</div>
<button
class="button-default dropdown-item"
+ @click="muteBotStatuses = !muteBotStatuses"
+ >
+ <span
+ class="menu-checkbox"
+ :class="{ 'menu-checkbox-checked': muteBotStatuses }"
+ />{{ $t('settings.mute_bot_posts') }}
+ </button>
+ <button
+ class="button-default dropdown-item"
@click="hideMedia = !hideMedia"
>
<span
diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js
index 94653004..33d9a258 100644
--- a/src/components/user_avatar/user_avatar.js
+++ b/src/components/user_avatar/user_avatar.js
@@ -1,10 +1,21 @@
import StillImage from '../still-image/still-image.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+
+import {
+ faRobot
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faRobot
+)
+
const UserAvatar = {
props: [
'user',
'betterShadow',
- 'compact'
+ 'compact',
+ 'bot'
],
data () {
return {
diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue
index 4040e263..29e03bcb 100644
--- a/src/components/user_avatar/user_avatar.vue
+++ b/src/components/user_avatar/user_avatar.vue
@@ -7,7 +7,9 @@
:src="imgSrc(user.profile_image_url_original)"
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
:image-load-error="imageLoadError"
- />
+ >
+ <FAIcon v-if="bot" icon="robot" class="bot-indicator" />
+ </StillImage>
<div
v-else
class="Avatar -placeholder"
@@ -36,6 +38,12 @@
height: 100%;
}
+ & > .bot-indicator {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ }
+
&.better-shadow {
box-shadow: var(--_avatarShadowInset);
filter: var(--_avatarShadowFilter);
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 6fe19866..153a0a53 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -119,7 +119,8 @@
"media_modal": {
"previous": "Previous",
"next": "Next",
- "counter": "{current} / {total}"
+ "counter": "{current} / {total}",
+ "hide": "Close media viewer"
},
"nav": {
"about": "About",
@@ -351,6 +352,8 @@
"hide_attachments_in_tl": "Hide attachments in timeline",
"hide_media_previews": "Hide media previews",
"hide_muted_posts": "Hide posts of muted users",
+ "mute_bot_posts": "Mute bot posts",
+ "hide_bot_indication": "Hide bot indication in posts",
"hide_all_muted_posts": "Hide muted posts",
"max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)",
"hide_isp": "Hide instance-specific panel",
diff --git a/src/main.js b/src/main.js
index 3895da89..03493525 100644
--- a/src/main.js
+++ b/src/main.js
@@ -45,6 +45,8 @@ Vue.use(VueClickOutside)
Vue.use(PortalVue)
Vue.use(VBodyScrollLock)
+Vue.config.ignoredElements = ['pinch-zoom']
+
Vue.component('FAIcon', FontAwesomeIcon)
Vue.component('FALayers', FontAwesomeLayers)
diff --git a/src/modules/config.js b/src/modules/config.js
index f3452612..c9ee4a51 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -29,6 +29,7 @@ export const defaultState = {
hideMutedPosts: undefined, // instance default
hideMutedThreads: undefined, // instance default
hideWordFilteredPosts: undefined, // instance default
+ muteBotStatuses: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default
padEmoji: true,
hideAttachments: false,
@@ -81,6 +82,7 @@ export const defaultState = {
mentionLinkShowYous: undefined, // instance default
mentionLinkBoldenYou: undefined, // instance default
hidePostStats: undefined, // instance default
+ hideBotIndication: undefined, // instance default
hideUserStats: undefined, // instance default
virtualScrolling: undefined, // instance default
sensitiveByDefault: undefined, // instance default
diff --git a/src/modules/instance.js b/src/modules/instance.js
index f09dd0b3..79c54096 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -33,8 +33,10 @@ const defaultState = {
hideMutedThreads: true,
hideWordFilteredPosts: false,
hidePostStats: false,
+ hideBotIndication: false,
hideSitename: false,
hideUserStats: false,
+ muteBotStatuses: false,
loginMethod: 'password',
logo: '/static/logo.svg',
logoMargin: '.2em',
diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js
index 88a328f3..265a7f25 100644
--- a/src/services/gesture_service/gesture_service.js
+++ b/src/services/gesture_service/gesture_service.js
@@ -4,9 +4,15 @@ const DIRECTION_RIGHT = [1, 0]
const DIRECTION_UP = [0, -1]
const DIRECTION_DOWN = [0, 1]
+const BUTTON_LEFT = 0
+
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
-const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
+const touchCoord = touch => [touch.screenX, touch.screenY]
+
+const touchEventCoord = e => touchCoord(e.touches[0])
+
+const pointerEventCoord = e => [e.clientX, e.clientY]
const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1])
@@ -61,6 +67,132 @@ const updateSwipe = (event, gesture) => {
gesture._swiping = false
}
+class SwipeAndClickGesture {
+ // swipePreviewCallback(offsets: Array[Number])
+ // offsets: the offset vector which the underlying component should move, from the starting position
+ // swipeEndCallback(sign: 0|-1|1)
+ // sign: if the swipe does not meet the threshold, 0
+ // if the swipe meets the threshold in the positive direction, 1
+ // if the swipe meets the threshold in the negative direction, -1
+ constructor ({
+ direction,
+ // swipeStartCallback
+ swipePreviewCallback,
+ swipeEndCallback,
+ swipeCancelCallback,
+ swipelessClickCallback,
+ threshold = 30,
+ perpendicularTolerance = 1.0,
+ disableClickThreshold = 1
+ }) {
+ const nop = () => {}
+ this.direction = direction
+ this.swipePreviewCallback = swipePreviewCallback || nop
+ this.swipeEndCallback = swipeEndCallback || nop
+ this.swipeCancelCallback = swipeCancelCallback || nop
+ this.swipelessClickCallback = swipelessClickCallback || nop
+ this.threshold = typeof threshold === 'function' ? threshold : () => threshold
+ this.disableClickThreshold = typeof disableClickThreshold === 'function' ? disableClickThreshold : () => disableClickThreshold
+ this.perpendicularTolerance = perpendicularTolerance
+ this._reset()
+ }
+
+ _reset () {
+ this._startPos = [0, 0]
+ this._pointerId = -1
+ this._swiping = false
+ this._swiped = false
+ this._preventNextClick = false
+ }
+
+ start (event) {
+ // Only handle left click
+ if (event.button !== BUTTON_LEFT) {
+ return
+ }
+
+ this._startPos = pointerEventCoord(event)
+ this._pointerId = event.pointerId
+ this._swiping = true
+ this._swiped = false
+ }
+
+ move (event) {
+ if (this._swiping && this._pointerId === event.pointerId) {
+ this._swiped = true
+
+ const coord = pointerEventCoord(event)
+ const delta = deltaCoord(this._startPos, coord)
+
+ this.swipePreviewCallback(delta)
+ }
+ }
+
+ cancel (event) {
+ if (!this._swiping || this._pointerId !== event.pointerId) {
+ return
+ }
+
+ this.swipeCancelCallback()
+ }
+
+ end (event) {
+ if (!this._swiping) {
+ return
+ }
+
+ if (this._pointerId !== event.pointerId) {
+ return
+ }
+
+ this._swiping = false
+
+ // movement too small
+ const coord = pointerEventCoord(event)
+ const delta = deltaCoord(this._startPos, coord)
+
+ const sign = (() => {
+ if (vectorLength(delta) < this.threshold()) {
+ return 0
+ }
+ // movement is opposite from direction
+ const isPositive = dotProduct(delta, this.direction) > 0
+
+ // movement perpendicular to direction is too much
+ const towardsDir = project(delta, this.direction)
+ const perpendicularDir = perpendicular(this.direction)
+ const towardsPerpendicular = project(delta, perpendicularDir)
+ if (
+ vectorLength(towardsDir) * this.perpendicularTolerance <
+ vectorLength(towardsPerpendicular)
+ ) {
+ return 0
+ }
+
+ return isPositive ? 1 : -1
+ })()
+
+ if (this._swiped) {
+ this.swipeEndCallback(sign)
+ }
+ this._reset()
+ // Only a mouse will fire click event when
+ // the end point is far from the starting point
+ // so for other kinds of pointers do not check
+ // whether we have swiped
+ if (vectorLength(delta) >= this.disableClickThreshold() && event.pointerType === 'mouse') {
+ this._preventNextClick = true
+ }
+ }
+
+ click (event) {
+ if (!this._preventNextClick) {
+ this.swipelessClickCallback()
+ }
+ this._reset()
+ }
+}
+
const GestureService = {
DIRECTION_LEFT,
DIRECTION_RIGHT,
@@ -68,7 +200,8 @@ const GestureService = {
DIRECTION_DOWN,
swipeGesture,
beginSwipe,
- updateSwipe
+ updateSwipe,
+ SwipeAndClickGesture
}
export default GestureService