Skip to content

Commit e4cf426

Browse files
committed
Feat: センシティブなメディアをタップ中だけ表示するオプションを追加
1 parent 499c303 commit e4cf426

10 files changed

Lines changed: 151 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
-
55

66
### Client
7-
-
7+
- Feat: センシティブなメディアをタップ中だけ表示するオプションを追加
88

99
### Server
1010
- Enhance: リモートノートクリーニングジョブのスキップ処理のパフォーマンス改善

locales/ja-JP.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ rename: "名前を変更"
401401
avatar: "アイコン"
402402
banner: "バナー"
403403
displayOfSensitiveMedia: "センシティブなメディアの表示"
404+
revealSensitiveMediaByTapHold: "タップ中だけセンシティブなメディアを表示"
404405
whenServerDisconnected: "サーバーとの接続が失われたとき"
405406
disconnectedFromServer: "サーバーから切断されました"
406407
reload: "リロード"

packages/frontend/src/components/MkMediaAudio.vue

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@ SPDX-License-Identifier: AGPL-3.0-only
1212
$style.audioContainer,
1313
(audio.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive,
1414
]"
15+
@mousedown="onPointerDown"
16+
@touchstart="onPointerDown"
17+
@mouseup="onPointerUp"
18+
@mouseleave="onPointerUp"
19+
@touchend="onPointerUp"
20+
@touchcancel="onPointerUp"
1521
@contextmenu.stop
1622
@keydown.stop
1723
>
18-
<button v-if="hide" :class="$style.hidden" @click="reveal">
24+
<button v-if="isActuallyHidden" :class="$style.hidden" @click="reveal">
1925
<div :class="$style.hiddenTextWrapper">
2026
<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
2127
<b v-else style="display: block;"><i class="ti ti-music"></i> {{ prefer.s.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
@@ -156,15 +162,32 @@ const playerEl = useTemplateRef('playerEl');
156162
const audioEl = useTemplateRef('audioEl');
157163
158164
const hide = ref(shouldHideFileByDefault(props.audio));
165+
const isRevealingByTap = ref(false);
166+
const isActuallyHidden = computed(() => hide.value && !isRevealingByTap.value);
159167
160168
async function reveal() {
169+
if (prefer.s.revealSensitiveMediaByTapHold) {
170+
return;
171+
}
172+
161173
if (!(await canRevealFile(props.audio))) {
162174
return;
163175
}
164176
165177
hide.value = false;
166178
}
167179
180+
function onPointerDown() {
181+
if (!prefer.s.revealSensitiveMediaByTapHold) return;
182+
if (!hide.value) return;
183+
isRevealingByTap.value = true;
184+
}
185+
186+
function onPointerUp() {
187+
if (!prefer.s.revealSensitiveMediaByTapHold) return;
188+
isRevealingByTap.value = false;
189+
}
190+
168191
// Menu
169192
const menuShowing = ref(false);
170193

packages/frontend/src/components/MkMediaBanner.vue

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only
44
-->
55

66
<template>
7-
<div :class="$style.root">
7+
<div :class="$style.root" @mousedown="onPointerDown" @touchstart="onPointerDown" @mouseup="onPointerUp" @mouseleave="onPointerUp" @touchend="onPointerUp" @touchcancel="onPointerUp">
88
<MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/>
9-
<div v-else-if="hide" :class="$style.sensitive" @click="reveal">
9+
<div v-else-if="isActuallyHidden" :class="$style.sensitive" @click="reveal">
1010
<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
1111
<b>{{ i18n.ts.sensitive }}</b>
1212
<span>{{ i18n.ts.clickToShow }}</span>
@@ -24,25 +24,43 @@ SPDX-License-Identifier: AGPL-3.0-only
2424
</template>
2525

2626
<script lang="ts" setup>
27-
import { ref } from 'vue';
27+
import { computed, ref } from 'vue';
2828
import * as Misskey from 'misskey-js';
2929
import { i18n } from '@/i18n.js';
3030
import MkMediaAudio from '@/components/MkMediaAudio.vue';
31+
import { prefer } from '@/preferences.js';
3132
import { shouldHideFileByDefault, canRevealFile } from '@/utility/sensitive-file.js';
3233
3334
const props = defineProps<{
3435
media: Misskey.entities.DriveFile;
3536
}>();
3637
3738
const hide = ref(shouldHideFileByDefault(props.media));
39+
const isRevealingByTap = ref(false);
40+
const isActuallyHidden = computed(() => hide.value && !isRevealingByTap.value);
3841
3942
async function reveal() {
43+
if (prefer.s.revealSensitiveMediaByTapHold) {
44+
return;
45+
}
46+
4047
if (!(await canRevealFile(props.media))) {
4148
return;
4249
}
4350
4451
hide.value = false;
4552
}
53+
54+
function onPointerDown() {
55+
if (!prefer.s.revealSensitiveMediaByTapHold) return;
56+
if (!hide.value) return;
57+
isRevealingByTap.value = true;
58+
}
59+
60+
function onPointerUp() {
61+
if (!prefer.s.revealSensitiveMediaByTapHold) return;
62+
isRevealingByTap.value = false;
63+
}
4664
</script>
4765

4866
<style lang="scss" module>

packages/frontend/src/components/MkMediaImage.vue

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
44
-->
55

66
<template>
7-
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="reveal" @contextmenu.stop="onContextmenu">
7+
<div :class="[isActuallyHidden ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="reveal" @contextmenu.stop="onContextmenu" @mousedown="onPointerDown" @touchstart="onPointerDown" @mouseup="onPointerUp" @mouseleave="onPointerUp" @touchend="onPointerUp" @touchcancel="onPointerUp">
88
<component
99
:is="disableImageLink ? 'div' : 'a'"
1010
v-bind="disableImageLink ? {
@@ -20,20 +20,20 @@ SPDX-License-Identifier: AGPL-3.0-only
2020
<MkImgWithBlurhash
2121
v-if="prefer.s.enableHighQualityImagePlaceholders"
2222
:hash="image.blurhash"
23-
:src="(prefer.s.dataSaver.media && hide) ? null : url"
24-
:forceBlurhash="hide"
25-
:cover="hide || cover"
23+
:src="(prefer.s.dataSaver.media && isActuallyHidden) ? null : url"
24+
:forceBlurhash="isActuallyHidden"
25+
:cover="cover"
2626
:alt="image.comment || image.name"
2727
:title="image.comment || image.name"
2828
:width="image.properties.width"
2929
:height="image.properties.height"
30-
:style="hide ? 'filter: brightness(0.7);' : null"
30+
:style="isActuallyHidden ? 'filter: brightness(0.7);' : null"
3131
:class="$style.image"
3232
/>
3333
<div
34-
v-else-if="prefer.s.dataSaver.media || hide"
34+
v-else-if="prefer.s.dataSaver.media || isActuallyHidden"
3535
:title="image.comment || image.name"
36-
:style="hide ? 'background: #888;' : null"
36+
:style="isActuallyHidden ? 'background: #888;' : null"
3737
:class="$style.image"
3838
></div>
3939
<img
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
4444
:class="$style.image"
4545
/>
4646
</component>
47-
<template v-if="hide">
47+
<template v-if="isActuallyHidden">
4848
<div :class="$style.hiddenText">
4949
<div :class="$style.hiddenTextWrapper">
5050
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
@@ -93,6 +93,9 @@ const props = withDefaults(defineProps<{
9393
9494
const hide = ref(true);
9595
96+
const isRevealingByTap = ref(false);
97+
const isActuallyHidden = computed(() => hide.value && !isRevealingByTap.value);
98+
9699
const url = computed(() => (props.raw || prefer.s.loadRawImages)
97100
? props.image.url
98101
: prefer.s.disableShowingAnimatedImages
@@ -107,6 +110,9 @@ async function reveal(ev: PointerEvent) {
107110
108111
if (hide.value) {
109112
ev.stopPropagation();
113+
if (prefer.s.revealSensitiveMediaByTapHold) {
114+
return;
115+
}
110116
if (!(await canRevealFile(props.image))) {
111117
return;
112118
}
@@ -115,6 +121,17 @@ async function reveal(ev: PointerEvent) {
115121
}
116122
}
117123
124+
function onPointerDown() {
125+
if (!prefer.s.revealSensitiveMediaByTapHold) return;
126+
if (!hide.value) return;
127+
isRevealingByTap.value = true;
128+
}
129+
130+
function onPointerUp() {
131+
if (!prefer.s.revealSensitiveMediaByTapHold) return;
132+
isRevealingByTap.value = false;
133+
}
134+
118135
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
119136
watch(() => props.image, (newImage) => {
120137
hide.value = shouldHideFileByDefault(newImage);

packages/frontend/src/components/MkMediaVideo.vue

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@ SPDX-License-Identifier: AGPL-3.0-only
1616
@mouseover.passive="onMouseOver"
1717
@mousemove.passive="onMouseMove"
1818
@mouseleave.passive="onMouseLeave"
19+
@mousedown="onPointerDown"
20+
@touchstart="onPointerDown"
21+
@mouseup="onPointerUp"
22+
@touchend="onPointerUp"
23+
@touchcancel="onPointerUp"
1924
@contextmenu.stop
2025
@keydown.stop
2126
>
22-
<button v-if="hide" :class="$style.hidden" @click="reveal">
27+
<button v-if="isActuallyHidden" :class="$style.hidden" @click="reveal">
2328
<div :class="$style.hiddenTextWrapper">
2429
<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
2530
<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ prefer.s.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
@@ -178,15 +183,32 @@ function hasFocus() {
178183
179184
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
180185
const hide = ref(shouldHideFileByDefault(props.video));
186+
const isRevealingByTap = ref(false);
187+
const isActuallyHidden = computed(() => hide.value && !isRevealingByTap.value);
181188
182189
async function reveal() {
190+
if (prefer.s.revealSensitiveMediaByTapHold) {
191+
return;
192+
}
193+
183194
if (!(await canRevealFile(props.video))) {
184195
return;
185196
}
186197
187198
hide.value = false;
188199
}
189200
201+
function onPointerDown() {
202+
if (!prefer.s.revealSensitiveMediaByTapHold) return;
203+
if (!hide.value) return;
204+
isRevealingByTap.value = true;
205+
}
206+
207+
function onPointerUp() {
208+
if (!prefer.s.revealSensitiveMediaByTapHold) return;
209+
isRevealingByTap.value = false;
210+
}
211+
190212
// Menu
191213
const menuShowing = ref(false);
192214
@@ -370,6 +392,8 @@ function onMouseMove() {
370392
}
371393
372394
function onMouseLeave() {
395+
onPointerUp();
396+
373397
if (controlStateTimer) {
374398
window.clearTimeout(controlStateTimer);
375399
}

packages/frontend/src/components/MkNoteMediaGrid.vue

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ SPDX-License-Identifier: AGPL-3.0-only
55

66
<template>
77
<template v-for="file in note.files">
8-
<div
9-
v-if="isHiding(file)"
10-
:class="[$style.filePreview, { [$style.square]: square }]"
11-
@click="reveal(file)"
8+
<div
9+
v-if="isHiding(file)"
10+
:class="[$style.filePreview, { [$style.square]: square }]"
11+
@click="reveal(file)"
12+
@mousedown="onPointerDown(file)"
13+
@touchstart="onPointerDown(file)"
14+
@mouseup="onPointerUp(file)"
15+
@mouseleave="onPointerUp(file)"
16+
@touchend="onPointerUp(file)"
17+
@touchcancel="onPointerUp(file)"
1218
>
1319
<MkDriveFileThumbnail
1420
:file="file"
@@ -26,7 +32,15 @@ SPDX-License-Identifier: AGPL-3.0-only
2632
</div>
2733
</div>
2834
</div>
29-
<MkA v-else :class="[$style.filePreview, { [$style.square]: square }]" :to="notePage(note)">
35+
<MkA
36+
v-else
37+
:class="[$style.filePreview, { [$style.square]: square }]"
38+
:to="notePage(note)"
39+
@mouseup="onPointerUp(file)"
40+
@mouseleave="onPointerUp(file)"
41+
@touchend="onPointerUp(file)"
42+
@touchcancel="onPointerUp(file)"
43+
>
3044
<MkDriveFileThumbnail
3145
:file="file"
3246
fit="cover"
@@ -57,6 +71,10 @@ defineProps<{
5771
const showingFiles = ref<Set<string>>(new Set());
5872
5973
function isHiding(file: Misskey.entities.DriveFile) {
74+
if (prefer.s.revealSensitiveMediaByTapHold && showingFiles.value.has(file.id)) {
75+
return false;
76+
}
77+
6078
if (shouldHideFileByDefault(file) && !showingFiles.value.has(file.id)) {
6179
if (!file.isSensitive && !file.type.startsWith('image/')) {
6280
return false;
@@ -67,12 +85,27 @@ function isHiding(file: Misskey.entities.DriveFile) {
6785
}
6886
6987
async function reveal(file: Misskey.entities.DriveFile) {
88+
if (prefer.s.revealSensitiveMediaByTapHold) {
89+
return;
90+
}
91+
7092
if (!(await canRevealFile(file))) {
7193
return;
7294
}
7395
7496
showingFiles.value.add(file.id);
7597
}
98+
99+
function onPointerDown(file: Misskey.entities.DriveFile) {
100+
if (!prefer.s.revealSensitiveMediaByTapHold) return;
101+
if (!isHiding(file)) return;
102+
showingFiles.value.add(file.id);
103+
}
104+
105+
function onPointerUp(file: Misskey.entities.DriveFile) {
106+
if (!prefer.s.revealSensitiveMediaByTapHold) return;
107+
showingFiles.value.delete(file.id);
108+
}
76109
</script>
77110

78111
<style lang="scss" module>

packages/frontend/src/pages/settings/preferences.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,14 @@ SPDX-License-Identifier: AGPL-3.0-only
350350
</SearchMarker>
351351
</template>
352352

353+
<SearchMarker :keywords="['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'tap', 'hold', 'press']">
354+
<MkPreferenceContainer k="revealSensitiveMediaByTapHold">
355+
<MkSwitch v-model="revealSensitiveMediaByTapHold">
356+
<template #label><SearchLabel>{{ i18n.ts.revealSensitiveMediaByTapHold }}</SearchLabel></template>
357+
</MkSwitch>
358+
</MkPreferenceContainer>
359+
</SearchMarker>
360+
353361
<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility']">
354362
<MkPreferenceContainer k="nsfw">
355363
<MkSelect
@@ -997,6 +1005,7 @@ const showAvailableReactionsFirstInNote = prefer.model('showAvailableReactionsFi
9971005
const useGroupedNotifications = prefer.model('useGroupedNotifications');
9981006
const alwaysConfirmFollow = prefer.model('alwaysConfirmFollow');
9991007
const confirmWhenRevealingSensitiveMedia = prefer.model('confirmWhenRevealingSensitiveMedia');
1008+
const revealSensitiveMediaByTapHold = prefer.model('revealSensitiveMediaByTapHold');
10001009
const confirmOnReact = prefer.model('confirmOnReact');
10011010
const defaultNoteVisibility = prefer.model('defaultNoteVisibility');
10021011
const defaultNoteLocalOnly = prefer.model('defaultNoteLocalOnly');

packages/frontend/src/preferences/def.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ export const PREF_DEF = definePreferences({
213213
nsfw: {
214214
default: 'respect' as 'respect' | 'force' | 'ignore',
215215
},
216+
revealSensitiveMediaByTapHold: {
217+
default: false,
218+
},
216219
highlightSensitiveMedia: {
217220
default: false,
218221
},

packages/i18n/src/autogen/locale.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,10 @@ export interface Locale extends ILocale {
16141614
* センシティブなメディアの表示
16151615
*/
16161616
"displayOfSensitiveMedia": string;
1617+
/**
1618+
* タップ中だけセンシティブなメディアを表示
1619+
*/
1620+
"revealSensitiveMediaByTapHold": string;
16171621
/**
16181622
* サーバーとの接続が失われたとき
16191623
*/

0 commit comments

Comments
 (0)