aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-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
14 files changed, 348 insertions, 62 deletions
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);