Merge branch 'develop' into future-2024-05-31

This commit is contained in:
dakkar 2024-06-08 16:45:53 +01:00
commit 5dc8c2827c
32 changed files with 415 additions and 222 deletions

View file

@ -334,10 +334,12 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
return false;
}
let renoting = false;
const keymap = {
'r': () => reply(true),
'e|a|plus': () => react(true),
'q': () => renote(appearNote.value.visibility),
'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
'up|k|shift+tab': focusBefore,
'down|j|tab': focusAfter,
'esc': blur,
@ -464,7 +466,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}).finally(() => { renoting = false });
}
} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
@ -483,7 +485,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}).finally(() => renoting = false);
}
}
}

View file

@ -346,10 +346,12 @@ if ($i) {
});
}
let renoting = false;
const keymap = {
'r': () => reply(true),
'e|a|plus': () => react(true),
'q': () => renote(appearNote.value.visibility),
'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
'esc': blur,
'm|o': () => showMenu(true),
's': () => showContent.value !== showContent.value,
@ -489,7 +491,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}).finally(() => { renoting = false });
} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
@ -506,7 +508,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}).finally(() => { renoting = false });
}
}

View file

@ -73,7 +73,7 @@ export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints>
*/
reversed?: boolean;
offsetMode?: boolean;
offsetMode?: boolean | ComputedRef<boolean>;
pageEl?: HTMLElement;
};
@ -240,10 +240,11 @@ const fetchMore = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
moreFetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false;
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
...params,
limit: SECOND_FETCH_LIMIT,
...(props.pagination.offsetMode ? {
...(offsetMode ? {
offset: offset.value,
} : {
untilId: Array.from(items.value.keys()).at(-1),
@ -304,10 +305,11 @@ const fetchMoreAhead = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
moreFetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false;
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
...params,
limit: SECOND_FETCH_LIMIT,
...(props.pagination.offsetMode ? {
...(offsetMode ? {
offset: offset.value,
} : {
sinceId: Array.from(items.value.keys()).at(-1),

View file

@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text dir="auto" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">

View file

@ -335,10 +335,12 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
return false;
}
let renoting = false;
const keymap = {
'r': () => reply(true),
'e|a|plus': () => react(true),
'q': () => renote(appearNote.value.visibility),
'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
'up|k|shift+tab': focusBefore,
'down|j|tab': focusAfter,
'esc': blur,
@ -465,7 +467,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}).finally(() => { renoting = false });
}
} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
@ -484,7 +486,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}).finally(() => { renoting = false });
}
}
}

View file

@ -355,10 +355,12 @@ if ($i) {
});
}
let renoting = false;
const keymap = {
'r': () => reply(true),
'e|a|plus': () => react(true),
'q': () => renote(appearNote.value.visibility),
'(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } },
'esc': blur,
'm|o': () => showMenu(true),
's': () => showContent.value !== showContent.value,
@ -498,7 +500,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}).finally(() => { renoting = false });
} else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
@ -515,7 +517,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) {
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}).finally(() => { renoting = false });
}
}

View file

@ -393,67 +393,67 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
}
case 'center': {
return [h('div', {
return [h('bdi',h('div', {
style: 'text-align:center;',
}, genEl(token.children, scale))];
}, genEl(token.children, scale)))];
}
case 'url': {
return [h(MkUrl, {
return [h('bdi',h(MkUrl, {
key: Math.random(),
url: token.props.url,
rel: 'nofollow noopener',
})];
}))];
}
case 'link': {
return [h(MkLink, {
return [h('bdi',h(MkLink, {
key: Math.random(),
url: token.props.url,
rel: 'nofollow noopener',
}, genEl(token.children, scale, true))];
}, genEl(token.children, scale, true)))];
}
case 'mention': {
return [h(MkMention, {
return [h('bdi',h(MkMention, {
key: Math.random(),
host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
username: token.props.username,
})];
}))];
}
case 'hashtag': {
return [h(MkA, {
return [h('bdi',h(MkA, {
key: Math.random(),
to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
style: 'color:var(--hashtag);',
}, `#${token.props.hashtag}`)];
}, `#${token.props.hashtag}`))];
}
case 'blockCode': {
return [h(MkCode, {
return [h('bdi',h(MkCode, {
key: Math.random(),
code: token.props.code,
lang: token.props.lang ?? undefined,
})];
}))];
}
case 'inlineCode': {
return [h(MkCodeInline, {
return [h('bdi',h(MkCodeInline, {
key: Math.random(),
code: token.props.code,
})];
}))];
}
case 'quote': {
if (!props.nowrap) {
return [h('div', {
return [h('bdi',h('div', {
style: QUOTE_STYLE,
}, genEl(token.children, scale, true))];
}, genEl(token.children, scale, true)))];
} else {
return [h('span', {
return [h('bdi',h('span', {
style: QUOTE_STYLE,
}, genEl(token.children, scale, true))];
}, genEl(token.children, scale, true)))];
}
}
@ -497,17 +497,17 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
}
case 'mathInline': {
return [h(MkFormula, {
return [h('bdi',h(MkFormula, {
formula: token.props.formula,
block: false,
})];
}))];
}
case 'mathBlock': {
return [h(MkFormula, {
return [h('bdi',h(MkFormula, {
formula: token.props.formula,
block: true,
})];
}))];
}
case 'search': {
@ -530,8 +530,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
}
}).flat(Infinity) as (VNode | string)[];
return h('span', {
return h('bdi', h('span', {
// https://codeday.me/jp/qa/20190424/690106.html
style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
}, genEl(rootAst, props.rootScale ?? 1));
}, genEl(rootAst, props.rootScale ?? 1)));
}

View file

@ -22,7 +22,7 @@
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: www.google.com xn--931a.moe launcher.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com;
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.listenbrainz.org;
frame-src *;"
/>
<meta property="og:site_name" content="[DEV BUILD] Misskey" />

View file

@ -215,7 +215,7 @@ function gravity() {
function iLoveMisskey() {
os.post({
initialText: 'I $[jelly ❤] #Misskey',
initialText: 'I $[jelly ❤] #Sharkey',
instant: true,
});
}

View file

@ -98,6 +98,9 @@ const selectedEmojis = ref<string[]>([]);
const pagination = {
endpoint: 'admin/emoji/list' as const,
limit: 30,
offsetMode: computed(() => (
(query.value && query.value !== '') ? true : false
)),
params: computed(() => ({
query: (query.value && query.value !== '') ? query.value : null,
})),

View file

@ -117,6 +117,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkSwitch v-model="useGroupedNotifications">{{ i18n.ts.useGroupedNotifications }}</MkSwitch>
<MkSwitch v-model="enableFaviconNotificationDot">{{ i18n.ts.enableFaviconNotificationDot }}</MkSwitch>
<MkRadios v-model="notificationPosition">
<template #label>{{ i18n.ts.position }}</template>
<option value="leftTop"><i class="ph-arrow-up-left ph-bold ph-lg"></i> {{ i18n.ts.leftTop }}</option>
@ -353,6 +355,7 @@ const oneko = computed(defaultStore.makeGetterSetter('oneko'));
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
const enableFaviconNotificationDot = computed(defaultStore.makeGetterSetter('enableFaviconNotificationDot'));
const warnMissingAltText = computed(defaultStore.makeGetterSetter('warnMissingAltText'));
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));

View file

@ -73,6 +73,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'showReactionsCount',
'loadRawImages',
'warnMissingAltText',
'enableFaviconNotificationDot',
'imageNewTab',
'dataSaver',
'disableShowingAnimatedImages',

View file

@ -236,22 +236,24 @@ const moderationNote = ref(props.user.moderationNote);
const editModerationNote = ref(false);
const noteview = ref<string | null>(null);
let listenbrainzdata = false;
const listenbrainzdata = ref(false);
if (props.user.listenbrainz) {
try {
const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
});
const data = await response.json();
if (data.payload.listens && data.payload.listens.length !== 0) {
listenbrainzdata = true;
(async function() {
try {
const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
});
const data = await response.json();
if (data.payload.listens && data.payload.listens.length !== 0) {
listenbrainzdata.value = true;
}
} catch (err) {
listenbrainzdata.value = false;
}
} catch (err) {
listenbrainzdata = false;
}
})()
}
const background = computed(() => {

View file

@ -0,0 +1,114 @@
import tinycolor from 'tinycolor2';
class FavIconDot {
canvas: HTMLCanvasElement;
src: string | null = null;
ctx: CanvasRenderingContext2D | null = null;
faviconImage: HTMLImageElement | null = null;
faviconEL: HTMLLinkElement | undefined;
hasLoaded: Promise<void> | undefined;
constructor() {
this.canvas = document.createElement('canvas');
}
/**
* Must be called before calling any other functions
*/
public async setup() {
const element: HTMLLinkElement = await this.getOrMakeFaviconElement();
this.faviconEL = element;
this.src = this.faviconEL.getAttribute('href');
this.ctx = this.canvas.getContext('2d');
this.faviconImage = document.createElement('img');
this.hasLoaded = new Promise((resolve, reject) => {
(this.faviconImage as HTMLImageElement).addEventListener('load', () => {
this.canvas.width = (this.faviconImage as HTMLImageElement).width;
this.canvas.height = (this.faviconImage as HTMLImageElement).height;
resolve();
});
(this.faviconImage as HTMLImageElement).addEventListener('error', () => {
reject('Failed to create favicon img element');
});
});
this.faviconImage.src = this.faviconEL.href;
}
private async getOrMakeFaviconElement(): Promise<HTMLLinkElement> {
return new Promise((resolve, reject) => {
const favicon = (document.querySelector('link[rel=icon]') ?? this.createFaviconElem()) as HTMLLinkElement;
favicon.addEventListener('load', () => {
resolve(favicon);
});
favicon.onerror = () => {
reject('Failed to load favicon');
};
resolve(favicon);
});
}
private createFaviconElem() {
const newLink = document.createElement('link');
newLink.setAttribute('rel', 'icon');
newLink.setAttribute('href', '/favicon.ico');
newLink.setAttribute('type', 'image/x-icon');
document.head.appendChild(newLink);
return newLink;
}
private drawIcon() {
if (!this.ctx || !this.faviconImage) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(this.faviconImage, 0, 0, this.faviconImage.width, this.faviconImage.height);
}
private drawDot() {
if (!this.ctx || !this.faviconImage) return;
this.ctx.beginPath();
this.ctx.arc(this.faviconImage.width - 10, 10, 10, 0, 2 * Math.PI);
const computedStyle = getComputedStyle(document.documentElement);
this.ctx.fillStyle = tinycolor(computedStyle.getPropertyValue('--navIndicator')).toHexString();
this.ctx.strokeStyle = 'white';
this.ctx.fill();
this.ctx.stroke();
}
private setFavicon() {
if (this.faviconEL) this.faviconEL.href = this.canvas.toDataURL('image/png');
}
async setVisible(isVisible: boolean) {
// Wait for it to have loaded the icon
await this.hasLoaded;
this.drawIcon();
if (isVisible) this.drawDot();
this.setFavicon();
}
}
let icon: FavIconDot | undefined = undefined;
export function setFavIconDot(visible: boolean) {
const setIconVisibility = async () => {
if (!icon) {
icon = new FavIconDot();
await icon.setup();
}
(icon as FavIconDot).setVisible(visible);
};
// If document is already loaded, set visibility immediately
if (document.readyState === 'complete') {
setIconVisibility();
} else {
// Otherwise, set visibility when window loads
window.addEventListener('load', setIconVisibility);
}
}

View file

@ -271,6 +271,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: true,
},
enableFaviconNotificationDot: {
where: 'device',
default: true,
},
imageNewTab: {
where: 'device',
default: false,

View file

@ -565,6 +565,8 @@ html[data-color-mode=dark] ._woodenFrame {
// MFM -----------------------------
div > bdi, p > bdi { display: block }
._mfm_blur_ {
filter: blur(6px);
transition: filter 0.3s;

View file

@ -47,8 +47,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import { defineAsyncComponent, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { setFavIconDot } from '../../scripts/favicon-dot';
import { swInject } from './sw-inject.js';
import XNotification from './notification.vue';
import { popups } from '@/os.js';
@ -93,6 +94,12 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
if ($i) {
const connection = useStream().useChannel('main', null, 'UI');
connection.on('notification', onNotification);
// For the favicon notification dot
watch(() => $i?.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot, (hasAny) => setFavIconDot(hasAny as boolean));
if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) setFavIconDot(true);
globalEvents.on('clientNotification', notification => onNotification(notification, true));
//#region Listen message from SW