Merge remote-tracking branch 'misskey/develop' into future-2024-04-25

This commit is contained in:
dakkar 2024-04-25 11:31:35 +01:00
commit 4fe8a26081
58 changed files with 1305 additions and 306 deletions

View file

@ -25,7 +25,7 @@
"@rollup/plugin-replace": "5.0.5",
"@rollup/pluginutils": "5.1.0",
"@transfem-org/sfm-js": "0.24.4",
"@syuilo/aiscript": "0.17.0",
"@syuilo/aiscript": "0.18.0",
"@phosphor-icons/web": "^2.0.3",
"@twemoji/parser": "15.0.0",
"@vitejs/plugin-vue": "5.0.4",

View file

@ -4,37 +4,59 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root" class="_panel">
<b>{{ clip.name }}</b>
<div v-if="clip.description" :class="$style.description">{{ clip.description }}</div>
<div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div>
<div :class="$style.user">
<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
<MkA :to="`/clips/${clip.id}`" :class="$style.link">
<div :class="$style.root" class="_panel _gaps_s">
<b>{{ clip.name }}</b>
<div :class="$style.description">
<div v-if="clip.description"><Mfm :text="clip.description" :plain="true" :nowrap="true"/></div>
<div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div>
<div v-if="clip.notesCount != null">{{ i18n.ts.notesCount }}: {{ number(clip.notesCount) }} / {{ $i?.policies.noteEachClipsLimit }} ({{ i18n.tsx.remainingN({ n: remaining }) }})</div>
</div>
<div :class="$style.divider"></div>
<div>
<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
</div>
</div>
</div>
</MkA>
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { computed } from 'vue';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import number from '@/filters/number.js';
defineProps<{
clip: any;
const props = defineProps<{
clip: Misskey.entities.Clip;
}>();
const remaining = computed(() => {
return ($i?.policies && props.clip.notesCount != null) ? ($i.policies.noteEachClipsLimit - props.clip.notesCount) : i18n.ts.unknown;
});
</script>
<style lang="scss" module>
.root {
.link {
display: block;
&:hover {
text-decoration: none;
color: var(--accent);
}
}
.root {
padding: 16px;
}
.description {
padding: 8px 0;
.divider {
height: 1px;
background: var(--divider);
}
.user {
padding-top: 16px;
border-top: solid 0.5px var(--divider);
.description {
font-size: 90%;
}
.userAvatar {

View file

@ -93,6 +93,18 @@ async function onClick() {
userId: props.user.id,
});
} else {
if (defaultStore.state.alwaysConfirmFollow) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }),
});
if (canceled) {
wait.value = false;
return;
}
}
if (hasPendingFollowRequestFromYou.value) {
await misskeyApi('following/requests/cancel', {
userId: props.user.id,

View file

@ -394,7 +394,7 @@ function addMissingMention() {
for (const x of extractMentions(ast)) {
if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) {
misskeyApi('users/show', { username: x.username, host: x.host }).then(user => {
visibleUsers.value.push(user);
pushVisibleUser(user);
});
}
}
@ -685,6 +685,7 @@ function saveDraft() {
localOnly: localOnly.value,
files: files.value,
poll: poll.value,
visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined,
},
};
@ -997,6 +998,15 @@ onMounted(() => {
if (draft.data.poll) {
poll.value = draft.data.poll;
}
if (draft.data.visibleUserIds) {
misskeyApi('users/show', { userIds: draft.data.visibleUserIds }).then(users => {
for (let i = 0; i < users.length; i++) {
if (users[i].id === draft.data.visibleUserIds[i]) {
pushVisibleUser(users[i]);
}
}
});
}
}
}

View file

@ -9,6 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSelect v-model="type" :class="$style.typeSelect">
<option value="isLocal">{{ i18n.ts._role._condition.isLocal }}</option>
<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option>
<option value="isSuspended">{{ i18n.ts._role._condition.isSuspended }}</option>
<option value="isLocked">{{ i18n.ts._role._condition.isLocked }}</option>
<option value="isBot">{{ i18n.ts._role._condition.isBot }}</option>
<option value="isCat">{{ i18n.ts._role._condition.isCat }}</option>
<option value="isExplorable">{{ i18n.ts._role._condition.isExplorable }}</option>
<option value="roleAssignedTo">{{ i18n.ts._role._condition.roleAssignedTo }}</option>
<option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>
<option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option>

View file

@ -9,11 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="800">
<div v-if="clip" class="_gaps">
<div class="_panel">
<div v-if="clip.description" :class="$style.description">
<Mfm :text="clip.description" :isNote="false"/>
<div class="_gaps_s" :class="$style.description">
<div v-if="clip.description">
<Mfm :text="clip.description" :isNote="false"/>
</div>
<div v-else>({{ i18n.ts.noDescription }})</div>
<div>
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
</div>
</div>
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
<MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
<div :class="$style.user">
<MkAvatar :user="clip.user" :class="$style.avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
</div>

View file

@ -48,7 +48,7 @@ import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import { useRouter } from '@/router/supplier.js';
const PRESET_DEFAULT = `/// @ 0.16.0
const PRESET_DEFAULT = `/// @ 0.18.0
var name = ""
@ -60,13 +60,13 @@ Ui:render([
Ui:C:button({
text: "Hello"
onClick: @() {
Mk:dialog(null \`Hello, {name}!\`)
Mk:dialog(null, \`Hello, {name}!\`)
}
})
])
`;
const PRESET_OMIKUJI = `/// @ 0.16.0
const PRESET_OMIKUJI = `/// @ 0.18.0
//
//
@ -81,11 +81,11 @@ let choices = [
"大凶"
]
// ID+
let random = Math:gen_rng(\`{USER_ID}{Date:year()}{Date:month()}{Date:day()}\`)
// PlayID+ID+
let random = Math:gen_rng(\`{THIS_ID}{USER_ID}{Date:year()}{Date:month()}{Date:day()}\`)
//
let chosen = choices[random(0 (choices.len - 1))]
let chosen = choices[random(0, (choices.len - 1))]
//
let result = \`今日のあなたの運勢は **{chosen}** です。\`
@ -109,7 +109,7 @@ Ui:render([
])
`;
const PRESET_SHUFFLE = `/// @ 0.16.0
const PRESET_SHUFFLE = `/// @ 0.18.0
//
let string = "ペペロンチーノ"
@ -123,13 +123,13 @@ var cursor = 0
@do() {
if (cursor != 0) {
results = results.slice(0 (cursor + 1))
results = results.slice(0, (cursor + 1))
cursor = 0
}
let chars = []
for (let i, length) {
let r = Math:rnd(0 (length - 1))
let r = Math:rnd(0, (length - 1))
chars.push(string.pick(r))
}
let result = chars.join("")
@ -188,27 +188,27 @@ var cursor = 0
do()
`;
const PRESET_QUIZ = `/// @ 0.16.0
const PRESET_QUIZ = `/// @ 0.18.0
let title = '地理クイズ'
let qas = [{
q: 'オーストラリアの首都は?'
choices: ['シドニー' 'キャンベラ' 'メルボルン']
choices: ['シドニー', 'キャンベラ', 'メルボルン']
a: 'キャンベラ'
aDescription: '最大の都市はシドニーですが首都はキャンベラです。'
} {
q: '国土面積2番目の国は'
choices: ['カナダ' 'アメリカ' '中国']
choices: ['カナダ', 'アメリカ', '中国']
a: 'カナダ'
aDescription: '大きい順にロシア、カナダ、アメリカ、中国です。'
} {
q: '二重内陸国ではないのは?'
choices: ['リヒテンシュタイン' 'ウズベキスタン' 'レソト']
choices: ['リヒテンシュタイン', 'ウズベキスタン', 'レソト']
a: 'レソト'
aDescription: 'レソトは(一重)内陸国です。'
} {
q: '閘門がない運河は?'
choices: ['キール運河' 'スエズ運河' 'パナマ運河']
choices: ['キール運河', 'スエズ運河', 'パナマ運河']
a: 'スエズ運河'
aDescription: 'スエズ運河は高低差がないので閘門はありません。'
}]
@ -296,12 +296,12 @@ qaEls.push(Ui:C:container({
onClick: finish
})
]
} 'footer'))
}, 'footer'))
Ui:render(qaEls)
`;
const PRESET_TIMELINE = `/// @ 0.16.0
const PRESET_TIMELINE = `/// @ 0.18.0
// API
@fetch() {
@ -315,7 +315,7 @@ const PRESET_TIMELINE = `/// @ 0.16.0
])
//
let notes = Mk:api("notes/local-timeline" {})
let notes = Mk:api("notes/local-timeline", {})
// UI
let noteEls = []

View file

@ -11,16 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="tab === 'my'" key="my" class="_gaps">
<MkButton primary rounded class="add" @click="create"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton>
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps">
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">
<MkClipPreview :clip="item"/>
</MkA>
<MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps">
<MkClipPreview v-for="item in items" :key="item.id" :clip="item"/>
</MkPagination>
</div>
<div v-else-if="tab === 'favorites'" key="favorites" class="_gaps">
<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`">
<MkClipPreview :clip="item"/>
</MkA>
<MkClipPreview v-for="item in favorites" :key="item.id" :clip="item"/>
</div>
</MkHorizontalSwipe>
</MkSpacer>

View file

@ -30,9 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="clips && clips.length > 0" class="_margin">
<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
<div class="_gaps">
<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`">
<MkClipPreview :clip="item"/>
</MkA>
<MkClipPreview v-for="item in clips" :key="item.id" :clip="item"/>
</div>
</div>
<div v-if="!showPrev" class="_buttons" :class="$style.loadPrev">

View file

@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
:enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
:leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
mode="out-in"
>
<div v-if="page" :key="page.id" class="_gaps">
<div :class="$style.pageMain">
@ -41,8 +42,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.pageBannerTitle" class="_gaps_s">
<h1>{{ page.title || page.name }}</h1>
<div v-if="page.user" :class="$style.pageBannerTitleUser">
<MkAvatar :user="page.user" :class="$style.avatar" indicator link preview/> <MkA :to="`/@${username}`"><MkUserName :user="page.user" :nowrap="false"/></MkA>
<div :class="$style.pageBannerTitleSub">
<div v-if="page.user" :class="$style.pageBannerTitleUser">
<MkAvatar :user="page.user" :class="$style.avatar" indicator link preview/> <MkA :to="`/@${username}`"><MkUserName :user="page.user" :nowrap="false"/></MkA>
</div>
<div :class="$style.pageBannerTitleSubActions">
<MkA v-if="page.userId === $i?.id" v-tooltip="i18n.ts._pages.editThisPage" :to="`/pages/edit/${page.id}`" class="_button" :class="$style.generalActionButton"><i class="ti ti-pencil ti-fw"></i></MkA>
<button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button>
</div>
</div>
</div>
</div>
@ -355,8 +362,15 @@ definePageMetadata(() => ({
margin: 0;
}
.pageBannerTitleSub {
display: flex;
align-items: center;
width: 100%;
}
.pageBannerTitleUser {
--height: 32px;
flex-shrink: 0;
.avatar {
height: var(--height);
@ -365,6 +379,14 @@ definePageMetadata(() => ({
line-height: var(--height);
}
.pageBannerTitleSubActions {
flex-shrink: 0;
display: flex;
align-items: center;
gap: var(--marginHalf);
margin-left: auto;
}
}
}

View file

@ -44,6 +44,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.keepOriginalUploading }}</template>
<template #caption>{{ i18n.ts.keepOriginalUploadingDescription }}</template>
</MkSwitch>
<MkSwitch v-model="keepOriginalFilename">
<template #label>{{ i18n.ts.keepOriginalFilename }}</template>
<template #caption>{{ i18n.ts.keepOriginalFilenameDescription }}</template>
</MkSwitch>
<MkSwitch v-model="alwaysMarkNsfw" @update:modelValue="saveProfile()">
<template #label>{{ i18n.ts.alwaysMarkSensitive }}</template>
</MkSwitch>
@ -91,6 +95,7 @@ const meterStyle = computed(() => {
});
const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading'));
const keepOriginalFilename = computed(defaultStore.makeGetterSetter('keepOriginalFilename'));
misskeyApi('drive').then(info => {
capacity.value = info.capacity;

View file

@ -191,6 +191,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="showBots">{{ i18n.ts.showBots }}</MkSwitch>
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
<MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
<MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</MkSwitch>
</div>
<MkSelect v-model="serverDisconnectedBehavior">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@ -366,6 +367,7 @@ const showVisibilitySelectorOnBoost = computed(defaultStore.makeGetterSetter('sh
const visibilityOnBoost = computed(defaultStore.makeGetterSetter('visibilityOnBoost'));
const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer'));
const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
@ -424,6 +426,7 @@ watch([
enableSeasonalScreenEffect,
showVisibilitySelectorOnBoost,
visibilityOnBoost,
alwaysConfirmFollow,
], async () => {
await reloadAsk();
});

View file

@ -26,6 +26,14 @@ export async function getNoteClipMenu(props: {
isDeleted: Ref<boolean>;
currentClip?: Misskey.entities.Clip;
}) {
function getClipName(clip: Misskey.entities.Clip) {
if ($i && clip.userId === $i.id && clip.notesCount != null) {
return `${clip.name} (${clip.notesCount}/${$i.policies.noteEachClipsLimit})`;
} else {
return clip.name;
}
}
const isRenote = (
props.note.renote != null &&
props.note.text == null &&
@ -37,7 +45,7 @@ export async function getNoteClipMenu(props: {
const clips = await clipsCache.fetch();
const menu: MenuItem[] = [...clips.map(clip => ({
text: clip.name,
text: getClipName(clip),
action: () => {
claimAchievement('noteClipped1');
os.promiseDialog(
@ -50,7 +58,18 @@ export async function getNoteClipMenu(props: {
text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }),
});
if (!confirm.canceled) {
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }).then(() => {
clipsCache.set(clips.map(c => {
if (c.id === clip.id) {
return {
...c,
notesCount: Math.max(0, ((c.notesCount ?? 0) - 1)),
};
} else {
return c;
}
}));
});
if (props.currentClip?.id === clip.id) props.isDeleted.value = true;
}
} else {
@ -60,7 +79,18 @@ export async function getNoteClipMenu(props: {
});
}
},
);
).then(() => {
clipsCache.set(clips.map(c => {
if (c.id === clip.id) {
return {
...c,
notesCount: (c.notesCount ?? 0) + 1,
};
} else {
return c;
}
}));
});
},
})), { type: 'divider' }, {
icon: 'ph-plus ph-bold ph-lg',

View file

@ -15,6 +15,16 @@ const fallbackName = (key: string) => `idbfallback::${key}`;
let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true;
// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。
// バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと
// see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
if (window.Cypress) {
idbAvailable = false;
console.log('Cypress detected. It will use localStorage.');
}
if (idbAvailable) {
await iset('idb-test', 'test')
.catch(err => {

View file

@ -5,6 +5,7 @@
import { reactive, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { v4 as uuid } from 'uuid';
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
import { getCompressionConfig } from './upload/compress-config.js';
import { defaultStore } from '@/store.js';
@ -39,13 +40,16 @@ export function uploadFile(
if (folder && typeof folder === 'object') folder = folder.id;
return new Promise((resolve, reject) => {
const id = Math.random().toString();
const id = uuid();
const reader = new FileReader();
reader.onload = async (): Promise<void> => {
const filename = name ?? file.name ?? 'untitled';
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
const ctx = reactive<Uploading>({
id: id,
name: name ?? file.name ?? 'untitled',
id,
name: defaultStore.state.keepOriginalFilename ? filename : id + extension,
progressMax: undefined,
progressValue: undefined,
img: window.URL.createObjectURL(file),

View file

@ -499,6 +499,14 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
keepOriginalFilename: {
where: 'device',
default: true,
},
alwaysConfirmFollow: {
where: 'device',
default: true,
},
sound_masterVolume: {
where: 'device',