Merge branch 'develop' into refactor-ui

This commit is contained in:
syuilo 2021-09-26 03:31:44 +09:00
commit 2e898c173c
7 changed files with 204 additions and 35 deletions

View file

@ -10,11 +10,15 @@
## 12.x.x (unreleased)
### Improvements
- アニメーションを減らす設定をメニューのアニメーションにも適用するように
- クライアント: アニメーションを減らす設定をメニューのアニメーションにも適用するように
- クライアント: MFM関数構文のサジェストを実装
- ActivityPub: HTML -> MFMの変換を強化
### Bugfixes
- Fix createDeleteAccountJob
- admin inbox queue does not show individual jobs
- クライアント: ヘッダーのタブが折り返される問題を修正
- クライアント: ヘッダーにタブが表示されている状態でタイトルをクリックしたときにタブ選択が表示されるのを修正
## 12.91.0 (2021/09/22)

View file

@ -10,12 +10,12 @@
</li>
<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $ts.selectUser }}</li>
</ol>
<ol class="hashtags" ref="suggests" v-if="hashtags.length > 0">
<ol class="hashtags" ref="suggests" v-else-if="hashtags.length > 0">
<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1">
<span class="name">{{ hashtag }}</span>
</li>
</ol>
<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
<ol class="emojis" ref="suggests" v-else-if="emojis.length > 0">
<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
<span class="emoji" v-else-if="!$store.state.useOsNativeEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
@ -24,6 +24,11 @@
<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
</li>
</ol>
<ol class="mfmTags" ref="suggests" v-else-if="mfmTags.length > 0">
<li v-for="tag in mfmTags" @click="complete(type, tag)" @keydown="onKeydown" tabindex="-1">
<span class="tag">{{ tag }}</span>
</li>
</ol>
</div>
</template>
@ -106,6 +111,8 @@ emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
//#endregion
const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle'];
export default defineComponent({
props: {
type: {
@ -137,11 +144,6 @@ export default defineComponent({
type: Number,
required: true,
},
showing: {
type: Boolean,
required: true
},
},
emits: ['done', 'closed'],
@ -154,18 +156,11 @@ export default defineComponent({
hashtags: [],
emojis: [],
items: [],
mfmTags: [],
select: -1,
}
},
watch: {
showing() {
if (!this.showing) {
this.$emit('closed');
}
}
},
updated() {
this.setPosition();
this.items = (this.$refs.suggests as Element | undefined)?.children || [];
@ -236,7 +231,7 @@ export default defineComponent({
}
}
if (this.type == 'user') {
if (this.type === 'user') {
if (this.q == null) {
this.users = [];
this.fetching = false;
@ -262,7 +257,7 @@ export default defineComponent({
sessionStorage.setItem(cacheKey, JSON.stringify(users));
});
}
} else if (this.type == 'hashtag') {
} else if (this.type === 'hashtag') {
if (this.q == null || this.q == '') {
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
this.fetching = false;
@ -286,7 +281,7 @@ export default defineComponent({
});
}
}
} else if (this.type == 'emoji') {
} else if (this.type === 'emoji') {
if (this.q == null || this.q == '') {
// 使
this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null);
@ -314,6 +309,13 @@ export default defineComponent({
}
this.emojis = matched;
} else if (this.type === 'mfmTag') {
if (this.q == null || this.q == '') {
this.mfmTags = MFM_TAGS;
return;
}
this.mfmTags = MFM_TAGS.filter(tag => tag.startsWith(this.q));
}
},
@ -490,5 +492,11 @@ export default defineComponent({
margin: 0 0 0 8px;
}
}
> .mfmTags > li {
.name {
}
}
}
</style>

View file

@ -5,9 +5,11 @@
<template #prefix><i class="fas fa-search"></i></template>
</MkInput>
<!-- たくさんあると邪魔
<div class="tags">
<span class="tag _button" v-for="tag in tags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
</div>
-->
</div>
<MkFolder class="emojis" v-if="searchEmojis">

View file

@ -7,9 +7,9 @@ export class Autocomplete {
private suggestion: {
x: Ref<number>;
y: Ref<number>;
q: Ref<string>;
q: Ref<string | null>;
close: Function;
};
} | null;
private textarea: any;
private vm: any;
private currentType: string;
@ -70,11 +70,13 @@ export class Autocomplete {
const mentionIndex = text.lastIndexOf('@');
const hashtagIndex = text.lastIndexOf('#');
const emojiIndex = text.lastIndexOf(':');
const mfmTagIndex = text.lastIndexOf('$');
const max = Math.max(
mentionIndex,
hashtagIndex,
emojiIndex);
emojiIndex,
mfmTagIndex);
if (max == -1) {
this.close();
@ -83,6 +85,7 @@ export class Autocomplete {
const isMention = mentionIndex != -1;
const isHashtag = hashtagIndex != -1;
const isMfmTag = mfmTagIndex != -1;
const isEmoji = emojiIndex != -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
let opened = false;
@ -114,6 +117,14 @@ export class Autocomplete {
}
}
if (isMfmTag && !opened) {
const mfmTag = text.substr(mfmTagIndex + 1);
if (!mfmTag.includes(' ')) {
this.open('mfmTag', mfmTag.replace('[', ''));
opened = true;
}
}
if (!opened) {
this.close();
}
@ -122,7 +133,7 @@ export class Autocomplete {
/**
*
*/
private async open(type: string, q: string) {
private async open(type: string, q: string | null) {
if (type != this.currentType) {
this.close();
}
@ -244,6 +255,22 @@ export class Autocomplete {
const pos = trimmedBefore.length + value.length;
this.textarea.setSelectionRange(pos, pos);
});
} else if (type == 'mfmTag') {
const source = this.text;
const before = source.substr(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
const after = source.substr(caret);
// 挿入
this.text = `${trimmedBefore}$[${value} ]${after}`;
// キャレットを戻す
this.vm.$nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 3);
this.textarea.setSelectionRange(pos, pos);
});
}
}
}

View file

@ -141,6 +141,7 @@ export default defineComponent({
showTabsPopup(ev) {
if (!this.hasTabs) return;
if (!this.narrow) return;
ev.preventDefault();
ev.stopPropagation();
const menu = this.info.tabs.map(tab => ({
@ -218,6 +219,7 @@ export default defineComponent({
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
> .avatar {
$size: 32px;
@ -263,6 +265,8 @@ export default defineComponent({
> .tabs {
margin-left: 16px;
font-size: 0.8em;
overflow: auto;
white-space: nowrap;
> .tab {
display: inline-block;

View file

@ -5,7 +5,9 @@ import { URL } from 'url';
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
export function fromHtml(html: string, hashtagNames?: string[]): string {
export function fromHtml(html: string, hashtagNames?: string[]): string | null {
if (html == null) return null;
const dom = parse5.parseFragment(html);
let text = '';
@ -19,6 +21,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
function getText(node: parse5.Node): string {
if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n';
if (node.childNodes) {
return node.childNodes.map(n => getText(n)).join('');
@ -27,6 +30,14 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
return '';
}
function appendChildren(childNodes: parse5.ChildNode[]): void {
if (childNodes) {
for (const n of childNodes) {
analyze(n);
}
}
}
function analyze(node: parse5.Node) {
if (treeAdapter.isTextNode(node)) {
text += node.value;
@ -42,6 +53,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
break;
case 'a':
{
const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href');
@ -87,23 +99,111 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
text += generateLink();
}
break;
}
case 'h1':
{
text += '【';
appendChildren(node.childNodes);
text += '】\n';
break;
}
case 'b':
case 'strong':
{
text += '**';
appendChildren(node.childNodes);
text += '**';
break;
}
case 'small':
{
text += '<small>';
appendChildren(node.childNodes);
text += '</small>';
break;
}
case 's':
case 'del':
{
text += '~~';
appendChildren(node.childNodes);
text += '~~';
break;
}
case 'i':
case 'em':
{
text += '<i>';
appendChildren(node.childNodes);
text += '</i>';
break;
}
// block code (<pre><code>)
case 'pre': {
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
text += '```\n';
text += getText(node.childNodes[0]);
text += '\n```\n';
} else {
appendChildren(node.childNodes);
}
break;
}
// inline code (<code>)
case 'code': {
text += '`';
appendChildren(node.childNodes);
text += '`';
break;
}
case 'blockquote': {
const t = getText(node);
if (t) {
text += '> ';
text += t.split('\n').join(`\n> `);
}
break;
}
case 'p':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
{
text += '\n\n';
if (node.childNodes) {
for (const n of node.childNodes) {
analyze(n);
}
}
appendChildren(node.childNodes);
break;
}
default:
if (node.childNodes) {
for (const n of node.childNodes) {
analyze(n);
}
}
// other block elements
case 'div':
case 'header':
case 'footer':
case 'article':
case 'li':
case 'dt':
case 'dd':
{
text += '\n';
appendChildren(node.childNodes);
break;
}
default: // includes inline elements
{
appendChildren(node.childNodes);
break;
}
}
}
}

View file

@ -19,6 +19,30 @@ describe('toHtml', () => {
});
describe('fromHtml', () => {
it('p', () => {
assert.deepStrictEqual(fromHtml('<p>a</p><p>b</p>'), 'a\n\nb');
});
it('block element', () => {
assert.deepStrictEqual(fromHtml('<div>a</div><div>b</div>'), 'a\nb');
});
it('inline element', () => {
assert.deepStrictEqual(fromHtml('<ul><li>a</li><li>b</li></ul>'), 'a\nb');
});
it('block code', () => {
assert.deepStrictEqual(fromHtml('<pre><code>a\nb</code></pre>'), '```\na\nb\n```');
});
it('inline code', () => {
assert.deepStrictEqual(fromHtml('<code>a</code>'), '`a`');
});
it('quote', () => {
assert.deepStrictEqual(fromHtml('<blockquote>a\nb</blockquote>'), '> a\n> b');
});
it('br', () => {
assert.deepStrictEqual(fromHtml('<p>abc<br><br/>d</p>'), 'abc\n\nd');
});