merge: Merge Upstream Changes (!408)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/408

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Amelia Yukii <amelia.yukii@shourai.de>
This commit is contained in:
Marie 2024-02-12 23:14:55 +00:00
commit 4007fbb8d8
149 changed files with 1936 additions and 697 deletions

View file

@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { abuseUserReport } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAbuseReport from './MkAbuseReport.vue';
@ -44,9 +44,9 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/admin/resolve-abuse-user-report', async (req, res, ctx) => {
action('POST /api/admin/resolve-abuse-user-report')(await req.json());
return res(ctx.json({}));
http.post('/api/admin/resolve-abuse-user-report', async ({ request }) => {
action('POST /api/admin/resolve-abuse-user-report')(await request.json());
return HttpResponse.json({});
}),
],
},

View file

@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAbuseReportWindow from './MkAbuseReportWindow.vue';
@ -44,9 +44,9 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/report-abuse', async (req, res, ctx) => {
action('POST /api/users/report-abuse')(await req.json());
return res(ctx.json({}));
http.post('/api/users/report-abuse', async ({ request }) => {
action('POST /api/users/report-abuse')(await request.json());
return HttpResponse.json({});
}),
],
},

View file

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAchievements from './MkAchievements.vue';
@ -39,8 +39,8 @@ export const Empty = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/achievements', (req, res, ctx) => {
return res(ctx.json([]));
http.post('/api/users/achievements', () => {
return HttpResponse.json([]);
}),
],
},
@ -52,8 +52,8 @@ export const All = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/achievements', (req, res, ctx) => {
return res(ctx.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 }))));
http.post('/api/users/achievements', () => {
return HttpResponse.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 })));
}),
],
},

View file

@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions';
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAutocomplete from './MkAutocomplete.vue';
@ -99,11 +99,11 @@ export const User = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/search-by-username-and-host', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/users/search-by-username-and-host', () => {
return HttpResponse.json([
userDetailed('44', 'mizuki', 'misskey-hub.net', 'Mizuki'),
userDetailed('49', 'momoko', 'misskey-hub.net', 'Momoko'),
]));
]);
}),
],
},
@ -132,12 +132,12 @@ export const Hashtag = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/hashtags/search', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/hashtags/search', () => {
return HttpResponse.json([
'気象警報注意報',
'気象警報',
'気象情報',
]));
]);
}),
],
},

View file

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAvatars from './MkAvatars.vue';
@ -38,12 +38,12 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/show', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/users/show', () => {
return HttpResponse.json([
userDetailed('17'),
userDetailed('20'),
userDetailed('18'),
]));
]);
}),
],
},

View file

@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- eslint-disable vue/no-v-html -->
<template>
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }]" v-html="html"></div>
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { bundledLanguagesInfo } from 'shiki';
import type { BuiltinLanguage } from 'shiki';
import { getHighlighter } from '@/scripts/code-highlighter.js';
import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
import { defaultStore } from '@/store.js';
const props = defineProps<{
code: string;
@ -21,11 +22,23 @@ const props = defineProps<{
}>();
const highlighter = await getHighlighter();
const darkMode = defaultStore.reactiveState.darkMode;
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
const [lightThemeName, darkThemeName] = await Promise.all([
getTheme('light', true),
getTheme('dark', true),
]);
const html = computed(() => highlighter.codeToHtml(props.code, {
lang: codeLang.value,
theme: 'dark-plus',
themes: {
fallback: 'dark-plus',
light: lightThemeName,
dark: darkThemeName,
},
defaultColor: false,
cssVariablePrefix: '--shiki-',
}));
async function fetchLanguage(to: string): Promise<void> {
@ -79,6 +92,16 @@ watch(() => props.lang, (to) => {
margin: .5em 0;
overflow: auto;
border-radius: var(--radius-sm);
border: 1px solid var(--divider);
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
color: var(--shiki-fallback);
background-color: var(--shiki-fallback-bg);
& span {
color: var(--shiki-fallback);
background-color: var(--shiki-fallback-bg);
}
& pre,
& code {
@ -86,6 +109,26 @@ watch(() => props.lang, (to) => {
}
}
.light.codeBlockRoot :global(.shiki) {
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
& span {
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
}
}
.dark.codeBlockRoot :global(.shiki) {
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
& span {
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
}
}
.codeBlockRoot.codeEditor {
min-width: 100%;
height: 100%;
@ -94,6 +137,7 @@ watch(() => props.lang, (to) => {
padding: 12px;
margin: 0;
border-radius: var(--radius-sm);
border: none;
min-height: 130px;
pointer-events: none;
min-width: calc(100% - 24px);
@ -105,6 +149,11 @@ watch(() => props.lang, (to) => {
text-rendering: inherit;
text-transform: inherit;
white-space: pre;
& span {
display: inline-block;
min-height: 1em;
}
}
}
</style>

View file

@ -53,7 +53,6 @@ function copy() {
}
.codeBlockCopyButton {
color: #D4D4D4;
position: absolute;
top: 8px;
right: 8px;
@ -67,8 +66,7 @@ function copy() {
.codeBlockFallbackRoot {
display: block;
overflow-wrap: anywhere;
color: #D4D4D4;
background: #1E1E1E;
background: var(--bg);
padding: 1em;
margin: .5em 0;
overflow: auto;
@ -93,8 +91,8 @@ function copy() {
border-radius: var(--radius-sm);
padding: 24px;
margin-top: 4px;
color: #D4D4D4;
background: #1E1E1E;
color: var(--fg);
background: var(--bg);
}
.codePlaceholderContainer {

View file

@ -196,10 +196,11 @@ watch(v, newValue => {
resize: none;
text-align: left;
color: transparent;
caret-color: rgb(225, 228, 232);
caret-color: var(--fg);
background-color: transparent;
border: 0;
border-radius: var(--radius-sm);
box-sizing: border-box;
outline: 0;
min-width: calc(100% - 24px);
height: 100%;
@ -212,6 +213,6 @@ watch(v, newValue => {
}
.textarea::selection {
color: #fff;
color: var(--bg);
}
</style>

View file

@ -18,8 +18,7 @@ const props = defineProps<{
display: inline-block;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
overflow-wrap: anywhere;
color: #D4D4D4;
background: #1E1E1E;
background: var(--bg);
padding: .1em;
border-radius: .3em;
}

View file

@ -63,18 +63,25 @@ const loading = ref(true);
const ok = async () => {
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => {
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
const croppedImage = await cropper?.getCropperImage();
const croppedSection = await cropper?.getCropperSelection();
// ()
const zoomedRate = croppedImage.getBoundingClientRect().width / croppedImage.clientWidth;
const widthToRender = croppedSection.getBoundingClientRect().width / zoomedRate;
const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender });
croppedCanvas?.toBlob(blob => {
if (!blob) return;
const formData = new FormData();
formData.append('file', blob);
formData.append('name', `cropped_${props.file.name}`);
formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false');
formData.append('comment', props.file.comment ?? 'null');
if (props.file.comment) { formData.append('comment', props.file.comment);}
formData.append('i', $i!.token);
if (props.uploadFolder || props.uploadFolder === null) {
formData.append('folderId', props.uploadFolder ?? 'null');
} else if (defaultStore.state.uploadFolder) {
if (props.uploadFolder) {
formData.append('folderId', props.uploadFolder);
} else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) {
formData.append('folderId', defaultStore.state.uploadFolder);
}

View file

@ -118,6 +118,7 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
import { $i } from '@/account.js';
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
const props = withDefaults(defineProps<{
showPinned?: boolean;
@ -126,6 +127,7 @@ const props = withDefaults(defineProps<{
asDrawer?: boolean;
asWindow?: boolean;
asReactionPicker?: boolean; // 使使
targetNote?: Misskey.entities.Note;
}>(), {
showPinned: true,
});
@ -340,7 +342,7 @@ watch(q, () => {
});
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) ?? false;
return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji);
}
function focus() {

View file

@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:showPinned="showPinned"
:pinnedEmojis="pinnedEmojis"
:asReactionPicker="asReactionPicker"
:targetNote="targetNote"
:asDrawer="type === 'drawer'"
:max-height="maxHeight"
@chosen="chosen"
@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
@ -43,6 +45,7 @@ const props = withDefaults(defineProps<{
showPinned?: boolean;
pinnedEmojis?: string[],
asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note;
choseAndClose?: boolean;
}>(), {
manualShowing: null,

View file

@ -13,12 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
:front="true"
@closed="emit('closed')"
>
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/>
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" :targetNote="targetNote" asWindow :class="$style.picker" @chosen="chosen"/>
</MkWindow>
</template>
<script lang="ts" setup>
import { } from 'vue';
import * as Misskey from 'misskey-js';
import MkWindow from '@/components/MkWindow.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
@ -26,6 +27,7 @@ withDefaults(defineProps<{
src?: HTMLElement;
showPinned?: boolean;
asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note
}>(), {
showPinned: true,
});

View file

@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</Transition>
</div>
</template>
<script lang="ts" setup>
import { ref, shallowRef, computed, nextTick, watch } from 'vue';
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import { defaultStore } from '@/store.js';
import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js';
const rootEl = shallowRef<HTMLDivElement>();
@ -49,16 +49,16 @@ const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontal
// //
//
const MIN_SWIPE_DISTANCE = 50;
const MIN_SWIPE_DISTANCE = 20;
//
const SWIPE_DISTANCE_THRESHOLD = 125;
const SWIPE_DISTANCE_THRESHOLD = 70;
// Y
const SWIPE_ABORT_Y_THRESHOLD = 75;
//
const MAX_SWIPE_DISTANCE = 150;
const MAX_SWIPE_DISTANCE = 120;
// //
@ -68,7 +68,6 @@ let startScreenY: number | null = null;
const currentTabIndex = computed(() => props.tabs.findIndex(tab => tab.key === tabModel.value));
const pullDistance = ref(0);
const isSwiping = ref(false);
const isSwipingForClass = ref(false);
let swipeAborted = false;
@ -77,6 +76,8 @@ function touchStart(event: TouchEvent) {
if (event.touches.length !== 1) return;
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
startScreenX = event.touches[0].screenX;
startScreenY = event.touches[0].screenY;
}
@ -90,6 +91,8 @@ function touchMove(event: TouchEvent) {
if (swipeAborted) return;
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
let distanceX = event.touches[0].screenX - startScreenX;
let distanceY = event.touches[0].screenY - startScreenY;
@ -139,6 +142,8 @@ function touchEnd(event: TouchEvent) {
if (!isSwiping.value) return;
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
const distance = event.changedTouches[0].screenX - startScreenX;
if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) {
@ -162,6 +167,24 @@ function touchEnd(event: TouchEvent) {
}, 400);
}
/** 横スワイプに関与する可能性のある要素を調べる */
function hasSomethingToDoWithXSwipe(el: HTMLElement) {
if (['INPUT', 'TEXTAREA'].includes(el.tagName)) return true;
if (el.isContentEditable) return true;
if (el.scrollWidth > el.clientWidth) return true;
const style = window.getComputedStyle(el);
if (['absolute', 'fixed', 'sticky'].includes(style.position)) return true;
if (['scroll', 'auto'].includes(style.overflowX)) return true;
if (style.touchAction === 'pan-x') return true;
if (el.parentElement && el.parentElement !== rootEl.value) {
return hasSomethingToDoWithXSwipe(el.parentElement);
} else {
return false;
}
}
const transitionName = ref<'swipeAnimationLeft' | 'swipeAnimationRight' | undefined>(undefined);
watch(tabModel, (newTab, oldTab) => {
@ -182,6 +205,7 @@ watch(tabModel, (newTab, oldTab) => {
<style lang="scss" module>
.transitionRoot {
touch-action: pan-y pinch-zoom;
display: grid;
grid-template-columns: 100%;
overflow: clip;

View file

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed, inviteCode } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkInviteCode from './MkInviteCode.vue';
@ -39,8 +39,8 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/show', (req, res, ctx) => {
return res(ctx.json(userDetailed(req.params.userId as string)));
http.post('/api/users/show', ({ params }) => {
return HttpResponse.json(userDetailed(params.userId as string));
}),
],
},

View file

@ -119,6 +119,7 @@ function close() {
margin-top: 12px;
font-size: 0.8em;
line-height: 1.5em;
text-align: center;
}
> .indicatorWithValue {

View file

@ -285,13 +285,11 @@ const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter(u => u !== renoteUrl && u !== renoteUri) : null);
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
const collapsed = ref(defaultStore.state.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
@ -624,7 +622,7 @@ function react(viaKeyboard = false): void {
}
} else {
blur();
reactionPicker.show(reactButton.value ?? null, reaction => {
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
sound.playMisskeySfx('reaction');
if (props.mock) {

View file

@ -303,8 +303,6 @@ const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
const isDeleted = ref(false);
@ -313,7 +311,7 @@ const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : fals
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false);
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
const urls = parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null;
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
@ -612,7 +610,7 @@ function react(viaKeyboard = false): void {
}
} else {
blur();
reactionPicker.show(reactButton.value ?? null, reaction => {
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
sound.playMisskeySfx('reaction');
misskeyApi('notes/reactions/create', {

View file

@ -76,7 +76,7 @@ const buttonsLeft = computed(() => {
});
const buttonsRight = computed(() => {
const buttons = [{
icon: 'ph-arrow-clockwise ph-bold ph-lg',
icon: 'ph-arrows-clockwise ph-bold ph-lg',
title: i18n.ts.reload,
onClick: reload,
}, {

View file

@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@/scripts/scroll.js';
import { isHorizontalSwipeSwiping } from '@/scripts/touch.js';
const SCROLL_STOP = 10;
const MAX_PULL_DISTANCE = Infinity;
@ -129,7 +130,7 @@ function moveEnd() {
function moving(event: TouchEvent | PointerEvent) {
if (!isPullStart.value || isRefreshing.value || disabled) return;
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value)) {
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value) || isHorizontalSwipeSwiping.value) {
pullDistance.value = 0;
isPullEnd.value = false;
moveEnd();
@ -148,6 +149,10 @@ function moving(event: TouchEvent | PointerEvent) {
if (event.cancelable) event.preventDefault();
}
if (pullDistance.value > SCROLL_STOP) {
event.stopPropagation();
}
isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
}

View file

@ -32,6 +32,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import * as sound from '@/scripts/sound.js';
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
import { customEmojis } from '@/custom-emojis.js';
const props = defineProps<{
reaction: string;
@ -48,13 +50,19 @@ const emit = defineEmits<{
const buttonEl = shallowRef<HTMLElement>();
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
const isCustomEmoji = computed(() => props.reaction.includes(':'));
const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null);
const canToggle = computed(() => {
return !props.reaction.match(/@\w/) && $i
&& (emoji.value && checkReactionPermissions($i, props.note, emoji.value))
|| !isCustomEmoji.value;
});
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
async function toggleReaction() {
if (!canToggle.value) return;
// TODO: 使
const oldReaction = props.note.myReaction;
if (oldReaction) {
const confirm = await os.confirm({
@ -101,8 +109,8 @@ async function toggleReaction() {
}
async function menu(ev) {
if (!canToggle.value) return;
if (!props.reaction.includes(':')) return;
if (!canGetInfo.value) return;
os.popupMenu([{
text: i18n.ts.info,
icon: 'ph-info ph-bold ph-lg',

View file

@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
@ -138,6 +138,7 @@ function show() {
active: computed(() => v.value === option.props?.value),
action: () => {
v.value = option.props?.value;
changed.value = true;
emit('changeByUser', v.value);
},
});
@ -288,6 +289,10 @@ function show() {
padding-left: 6px;
}
.save {
margin: 8px 0 0 0;
}
.chevron {
transition: transform 0.1s ease-out;
}

View file

@ -19,7 +19,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import { ChannelConnection as Connection } from 'misskey-js';
import MkNotes from '@/components/MkNotes.vue';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { useStream } from '@/stream.js';
@ -90,8 +89,8 @@ function prepend(note) {
}
}
let connection: Connection;
let connection2: Connection;
let connection: Misskey.ChannelConnection | null = null;
let connection2: Misskey.ChannelConnection | null = null;
let paginationQuery: Paging | null = null;
const stream = useStream();
@ -163,7 +162,7 @@ function connectChannel() {
roleId: props.role,
});
}
if (props.src !== 'directs' && props.src !== 'mentions') connection.on('note', prepend);
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
}
function disconnectChannel() {

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-if="player.url.startsWith('http://') || player.url.startsWith('https://')"
sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin"
scrolling="no"
:allow="player.allow.join(';')"
:allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
:class="$style.playerIframe"
:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')"
:style="{ border: 0 }"

View file

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js';
import { userDetailed } from '../../.storybook/fakes.js';
import MkUserSetupDialog_Follow from './MkUserSetupDialog.Follow.vue';
@ -38,17 +38,17 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]));
]);
}),
rest.post('/api/pinned-users', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/pinned-users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]));
]);
}),
],
},

View file

@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import Misskey from 'misskey-js';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue';

View file

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js';
import { userDetailed } from '../../.storybook/fakes.js';
import MkUserSetupDialog from './MkUserSetupDialog.vue';
@ -38,17 +38,17 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]));
]);
}),
rest.post('/api/pinned-users', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/pinned-users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]));
]);
}),
],
},

View file

@ -286,13 +286,11 @@ const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter(u => u !== renoteUrl && u !== renoteUri) : null);
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
const collapsed = ref(defaultStore.state.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
@ -625,7 +623,7 @@ function react(viaKeyboard = false): void {
}
} else {
blur();
reactionPicker.show(reactButton.value ?? null, reaction => {
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
sound.playMisskeySfx('reaction');
if (props.mock) {

View file

@ -312,8 +312,6 @@ const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
const isDeleted = ref(false);
@ -322,7 +320,7 @@ const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : fals
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false);
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
const urls = parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null;
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
@ -621,7 +619,7 @@ function react(viaKeyboard = false): void {
}
} else {
blur();
reactionPicker.show(reactButton.value ?? null, reaction => {
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
sound.playMisskeySfx('reaction');
misskeyApi('notes/reactions/create', {

View file

@ -7,7 +7,7 @@
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../../.storybook/mocks.js';
import MkUrl from './MkUrl.vue';
export const Default = {
@ -59,8 +59,8 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.get('/url', (req, res, ctx) => {
return res(ctx.json({
http.get('/url', () => {
return HttpResponse.json({
title: 'Misskey Hub',
icon: 'https://misskey-hub.net/favicon.ico',
description: 'Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。',
@ -74,7 +74,7 @@ export const Default = {
sitename: 'misskey-hub.net',
sensitive: false,
url: 'https://misskey-hub.net/',
}));
});
}),
],
},

View file

@ -4,7 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<KeepAlive :max="defaultStore.state.numberOfPageCache">
<KeepAlive
:max="defaultStore.state.numberOfPageCache"
:exclude="pageCacheController"
>
<Suspense :timeout="0">
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
@ -16,9 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue';
import { IRouter, Resolved } from '@/nirax.js';
import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue';
import { IRouter, Resolved, RouteDef } from '@/nirax.js';
import { defaultStore } from '@/store.js';
import { globalEvents } from '@/events.js';
import MkLoadingPage from '@/pages/_loading_.vue';
const props = defineProps<{
router?: IRouter;
@ -46,20 +51,47 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
}
const current = resolveNested(router.current)!;
const currentPageComponent = shallowRef(current.route.component);
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
const currentPageProps = ref(current.props);
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
function onChange({ resolved, key: newKey }) {
const current = resolveNested(resolved);
if (current == null) return;
if (current == null || 'redirect' in current.route) return;
currentPageComponent.value = current.route.component;
currentPageProps.value = current.props;
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
nextTick(() => {
//
if (clearCacheRequested.value) {
clearCacheRequested.value = false;
}
});
}
router.addListener('change', onChange);
// #region
/**
* キャッシュクリアが有効になったら全キャッシュをクリアする
*
* keepAlive側にwatcherがあるのですぐ消えるとはおもうけど念のためページ遷移完了まではキャッシュを無効化しておく
* キャッシュ有効時向けにexcludeを使いたい場合はpageCacheControllerに並列に突っ込むのではなく下に追記すること
*/
const pageCacheController = computed(() => clearCacheRequested.value ? /.*/ : undefined);
const clearCacheRequested = ref(false);
globalEvents.on('requestClearPageCache', () => {
if (_DEV_) console.log('clear page cache requested');
if (!clearCacheRequested.value) {
clearCacheRequested.value = true;
}
});
// #endregion
onBeforeUnmount(() => {
router.removeListener('change', onChange);
});