mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-04-28 09:36:56 +00:00
Merge branch 'develop' into merge/2024-02-03
# Conflicts: # locales/index.d.ts # packages/backend/src/core/entities/UserEntityService.ts # packages/frontend/src/_dev_boot_.ts # packages/misskey-js/src/autogen/types.ts # sharkey-locales/en-US.yml
This commit is contained in:
commit
f36029f795
24 changed files with 512 additions and 35 deletions
|
@ -3,10 +3,12 @@
|
|||
# **What does this MR do?**
|
||||
<!-- Please give us a brief description of what this PR does. -->
|
||||
|
||||
%{all_commits}
|
||||
|
||||
# **Contribution Guidelines**
|
||||
By submitting this merge request, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md)
|
||||
- [ ] I agree to follow this project's Contribution Guidelines
|
||||
- [ ] I have made sure to test this merge request
|
||||
|
||||
<!-- Uncomment if your merge request has multiple authors -->
|
||||
<!-- Co-authored-by: Name <email@email.com> -->
|
||||
<!-- %{co_authored_by} -->
|
||||
|
|
34
locales/index.d.ts
vendored
34
locales/index.d.ts
vendored
|
@ -11977,6 +11977,40 @@ export interface Locale extends ILocale {
|
|||
* Adding entries here will override the default robots.txt packaged with Sharkey.
|
||||
*/
|
||||
"robotsTxtDescription": string;
|
||||
/**
|
||||
* Default content warning for new posts
|
||||
*/
|
||||
"defaultCW": string;
|
||||
/**
|
||||
* The value here will be auto-filled as the content warning for all new posts and replies.
|
||||
*/
|
||||
"defaultCWDescription": string;
|
||||
/**
|
||||
* Automatic CW priority
|
||||
*/
|
||||
"defaultCWPriority": string;
|
||||
/**
|
||||
* Select preferred action when default CW and keep CW settings are both enabled at the same time.
|
||||
*/
|
||||
"defaultCWPriorityDescription": string;
|
||||
"_defaultCWPriority": {
|
||||
/**
|
||||
* Use Default (use the default CW, ignoring the inherited CW)
|
||||
*/
|
||||
"default": string;
|
||||
/**
|
||||
* Use Parent (use the inherited CW, ignoring the default CW)
|
||||
*/
|
||||
"parent": string;
|
||||
/**
|
||||
* Use Default, then Parent (use the default CW, and append the inherited CW)
|
||||
*/
|
||||
"defaultParent": string;
|
||||
/**
|
||||
* Use Parent, then Default (use the inherited CW, and append the default CW)
|
||||
*/
|
||||
"parentDefault": string;
|
||||
};
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
export class AddUserProfileDefaultCw1738446745738 {
|
||||
name = 'AddUserProfileDefaultCw1738446745738'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "default_cw" text`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "default_cw"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
export class AddUserProfileDefaultCwPriority1738468079662 {
|
||||
name = 'AddUserProfileDefaultCwPriority1738468079662'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_default_cw_priority_enum" AS ENUM ('default', 'parent', 'defaultParent', 'parentDefault')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "default_cw_priority" "public"."user_profile_default_cw_priority_enum" NOT NULL DEFAULT 'parent'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "default_cw_priority"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_default_cw_priority_enum"`);
|
||||
}
|
||||
}
|
|
@ -263,6 +263,67 @@ export class MfmService {
|
|||
break;
|
||||
}
|
||||
|
||||
case 'rp': break;
|
||||
case 'rt': {
|
||||
appendChildren(node.childNodes);
|
||||
break;
|
||||
}
|
||||
case 'ruby': {
|
||||
if (node.childNodes) {
|
||||
/*
|
||||
we get:
|
||||
```
|
||||
<ruby>
|
||||
some text <rp>(</rp> <rt>annotation</rt> <rp>)</rp>
|
||||
more text <rt>more annotation<rt>
|
||||
</ruby>
|
||||
```
|
||||
|
||||
and we want to produce:
|
||||
```
|
||||
$[ruby $[group some text] annotation]
|
||||
$[ruby $[group more text] more annotation]
|
||||
```
|
||||
|
||||
that `group` is a hack, because when the `ruby` render
|
||||
sees just text inside the `$[ruby]`, it splits on
|
||||
whitespace, considers the first "word" to be the main
|
||||
content, and the rest the annotation
|
||||
|
||||
with that `group`, we force it to consider the whole
|
||||
group as the main content
|
||||
|
||||
(note that the `rp` are to be ignored, they only exist
|
||||
for browsers who don't understand ruby)
|
||||
*/
|
||||
let nonRtNodes = [];
|
||||
// scan children, ignore `rp`, split on `rt`
|
||||
for (const child of node.childNodes) {
|
||||
if (treeAdapter.isTextNode(child)) {
|
||||
nonRtNodes.push(child);
|
||||
continue;
|
||||
}
|
||||
if (!treeAdapter.isElementNode(child)) {
|
||||
continue;
|
||||
}
|
||||
if (child.nodeName === 'rp') {
|
||||
continue;
|
||||
}
|
||||
if (child.nodeName === 'rt') {
|
||||
text += '$[ruby $[group ';
|
||||
appendChildren(nonRtNodes);
|
||||
text += '] ';
|
||||
analyze(child);
|
||||
text += '] ';
|
||||
nonRtNodes = [];
|
||||
continue;
|
||||
}
|
||||
nonRtNodes.push(child);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: // includes inline elements
|
||||
{
|
||||
appendChildren(node.childNodes);
|
||||
|
@ -381,6 +442,14 @@ export class MfmService {
|
|||
}
|
||||
}
|
||||
|
||||
// hack for ruby, should never be needed because we should
|
||||
// never send this out to other instances
|
||||
case 'group': {
|
||||
const el = doc.createElement('span');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
}
|
||||
|
||||
default: {
|
||||
return fnDefault(node);
|
||||
}
|
||||
|
@ -559,11 +628,65 @@ export class MfmService {
|
|||
},
|
||||
|
||||
async fn(node) {
|
||||
const el = doc.createElement('span');
|
||||
el.textContent = '*';
|
||||
await appendChildren(node.children, el);
|
||||
el.textContent += '*';
|
||||
return el;
|
||||
switch (node.props.name) {
|
||||
case 'group': { // hack for ruby
|
||||
const el = doc.createElement('span');
|
||||
await appendChildren(node.children, el);
|
||||
return el;
|
||||
}
|
||||
case 'ruby': {
|
||||
if (node.children.length === 1) {
|
||||
const child = node.children[0];
|
||||
const text = child.type === 'text' ? child.props.text : '';
|
||||
const rubyEl = doc.createElement('ruby');
|
||||
const rtEl = doc.createElement('rt');
|
||||
|
||||
const rpStartEl = doc.createElement('rp');
|
||||
rpStartEl.appendChild(doc.createTextNode('('));
|
||||
const rpEndEl = doc.createElement('rp');
|
||||
rpEndEl.appendChild(doc.createTextNode(')'));
|
||||
|
||||
rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
|
||||
rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
|
||||
rubyEl.appendChild(rpStartEl);
|
||||
rubyEl.appendChild(rtEl);
|
||||
rubyEl.appendChild(rpEndEl);
|
||||
return rubyEl;
|
||||
} else {
|
||||
const rt = node.children.at(-1);
|
||||
|
||||
if (!rt) {
|
||||
const el = doc.createElement('span');
|
||||
await appendChildren(node.children, el);
|
||||
return el;
|
||||
}
|
||||
|
||||
const text = rt.type === 'text' ? rt.props.text : '';
|
||||
const rubyEl = doc.createElement('ruby');
|
||||
const rtEl = doc.createElement('rt');
|
||||
|
||||
const rpStartEl = doc.createElement('rp');
|
||||
rpStartEl.appendChild(doc.createTextNode('('));
|
||||
const rpEndEl = doc.createElement('rp');
|
||||
rpEndEl.appendChild(doc.createTextNode(')'));
|
||||
|
||||
await appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
|
||||
rtEl.appendChild(doc.createTextNode(text.trim()));
|
||||
rubyEl.appendChild(rpStartEl);
|
||||
rubyEl.appendChild(rtEl);
|
||||
rubyEl.appendChild(rpEndEl);
|
||||
return rubyEl;
|
||||
}
|
||||
}
|
||||
|
||||
default: {
|
||||
const el = doc.createElement('span');
|
||||
el.textContent = '*';
|
||||
await appendChildren(node.children, el);
|
||||
el.textContent += '*';
|
||||
return el;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
blockCode(node) {
|
||||
|
|
|
@ -55,6 +55,8 @@ import type { NoteEntityService } from './NoteEntityService.js';
|
|||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import type { PageEntityService } from './PageEntityService.js';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
const Ajv = _Ajv.default;
|
||||
const ajv = new Ajv();
|
||||
|
||||
|
@ -669,6 +671,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
achievements: profile!.achievements,
|
||||
loggedInDays: profile!.loggedInDates.length,
|
||||
policies: this.roleService.getUserPolicies(user.id),
|
||||
defaultCW: profile!.defaultCW,
|
||||
defaultCWPriority: profile!.defaultCWPriority,
|
||||
} : {}),
|
||||
|
||||
...(opts.includeSecrets ? {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||
import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js';
|
||||
import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes, noteVisibilities, defaultCWPriorities } from '@/types.js';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiPage } from './Page.js';
|
||||
|
@ -36,10 +36,10 @@ export class MiUserProfile {
|
|||
})
|
||||
public birthday: string | null;
|
||||
|
||||
@Column("varchar", {
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
comment: "The ListenBrainz username of the User.",
|
||||
comment: 'The ListenBrainz username of the User.',
|
||||
})
|
||||
public listenbrainz: string | null;
|
||||
|
||||
|
@ -290,6 +290,19 @@ export class MiUserProfile {
|
|||
unlockedAt: number;
|
||||
}[];
|
||||
|
||||
@Column('text', {
|
||||
name: 'default_cw',
|
||||
nullable: true,
|
||||
})
|
||||
public defaultCW: string | null;
|
||||
|
||||
@Column('enum', {
|
||||
name: 'default_cw_priority',
|
||||
enum: defaultCWPriorities,
|
||||
default: 'parent',
|
||||
})
|
||||
public defaultCWPriority: typeof defaultCWPriorities[number];
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
|
|
|
@ -752,6 +752,15 @@ export const packedMeDetailedOnlySchema = {
|
|||
},
|
||||
},
|
||||
//#endregion
|
||||
defaultCW: {
|
||||
type: 'string',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
defaultCWPriority: {
|
||||
type: 'string',
|
||||
enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -13,10 +13,11 @@ export const meta = {
|
|||
|
||||
requireCredential: false,
|
||||
|
||||
// 2 calls per second
|
||||
// Up to 10 calls, then 4 / second.
|
||||
// This allows for reliable automation.
|
||||
limit: {
|
||||
duration: 1000,
|
||||
max: 2,
|
||||
max: 10,
|
||||
dripRate: 250,
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -31,10 +31,12 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
|
||||
// 3 calls per second
|
||||
// up to 20 calls, then 1 per second.
|
||||
// This handles bursty traffic when all tabs reload as a group
|
||||
limit: {
|
||||
duration: 1000,
|
||||
max: 3,
|
||||
max: 20,
|
||||
dripSize: 1,
|
||||
dripRate: 1000,
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -133,6 +133,12 @@ export const meta = {
|
|||
id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
|
||||
httpStatusCode: 422,
|
||||
},
|
||||
|
||||
maxCwLength: {
|
||||
message: 'You tried setting a default content warning which is too long.',
|
||||
code: 'MAX_CW_LENGTH',
|
||||
id: '7004c478-bda3-4b4f-acb2-4316398c9d52',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
|
@ -243,6 +249,12 @@ export const paramDef = {
|
|||
uniqueItems: true,
|
||||
items: { type: 'string' },
|
||||
},
|
||||
defaultCW: { type: 'string', nullable: true },
|
||||
defaultCWPriority: {
|
||||
type: 'string',
|
||||
enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -494,6 +506,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
updates.alsoKnownAs = newAlsoKnownAs.size > 0 ? Array.from(newAlsoKnownAs) : null;
|
||||
}
|
||||
|
||||
let defaultCW = ps.defaultCW;
|
||||
if (defaultCW !== undefined) {
|
||||
if (defaultCW === '') defaultCW = null;
|
||||
if (defaultCW && defaultCW.length > this.config.maxCwLength) {
|
||||
throw new ApiError(meta.errors.maxCwLength);
|
||||
}
|
||||
|
||||
profileUpdates.defaultCW = defaultCW;
|
||||
}
|
||||
if (ps.defaultCWPriority !== undefined) {
|
||||
profileUpdates.defaultCWPriority = ps.defaultCWPriority;
|
||||
}
|
||||
|
||||
//#region emojis/tags
|
||||
|
||||
let emojis = [] as string[];
|
||||
|
|
|
@ -57,10 +57,10 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
|
||||
// 5 calls per 2 seconds
|
||||
// up to 50 calls @ 4 per second
|
||||
limit: {
|
||||
duration: 1000 * 2,
|
||||
max: 5,
|
||||
max: 50,
|
||||
dripRate: 250,
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
if (supportedLangs.includes(navigator.language)) {
|
||||
lang = navigator.language;
|
||||
} else {
|
||||
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
||||
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language.split('-')[0]);
|
||||
|
||||
// Fallback
|
||||
if (lang == null) lang = 'en-US';
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
if (supportedLangs.includes(navigator.language)) {
|
||||
lang = navigator.language;
|
||||
} else {
|
||||
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
||||
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language.split('-')[0]);
|
||||
|
||||
// Fallback
|
||||
if (lang == null) lang = 'en-US';
|
||||
|
|
|
@ -58,6 +58,8 @@ export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
|||
export const followingVisibilities = ['public', 'followers', 'private'] as const;
|
||||
export const followersVisibilities = ['public', 'followers', 'private'] as const;
|
||||
|
||||
export const defaultCWPriorities = ['default', 'parent', 'defaultParent', 'parentDefault'] as const;
|
||||
|
||||
/**
|
||||
* ユーザーがエクスポートできるものの種類
|
||||
*
|
||||
|
|
|
@ -45,6 +45,50 @@ describe('MfmService', () => {
|
|||
const output = '<p><pre><code><p>Hello, world!</p></code></pre></p>';
|
||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
|
||||
test('ruby', () => {
|
||||
const input = '$[ruby some text ignore me]';
|
||||
const output = '<p><ruby>some<rp>(</rp><rt>text</rt><rp>)</rp></ruby></p>';
|
||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
|
||||
test('ruby2', () => {
|
||||
const input = '$[ruby *some text* ignore me]';
|
||||
const output = '<p><ruby><i>some text</i><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>';
|
||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
|
||||
test('ruby 3', () => {
|
||||
const input = '$[ruby $[group *some* text] ignore me]';
|
||||
const output = '<p><ruby><span><i>some</i> text</span><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>';
|
||||
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toMastoApiHtml', () => {
|
||||
test('br', async () => {
|
||||
const input = 'foo\nbar\nbaz';
|
||||
const output = '<p><span>foo<br>bar<br>baz</span></p>';
|
||||
assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
|
||||
});
|
||||
|
||||
test('br alt', async () => {
|
||||
const input = 'foo\r\nbar\rbaz';
|
||||
const output = '<p><span>foo<br>bar<br>baz</span></p>';
|
||||
assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
|
||||
});
|
||||
|
||||
test('escape', async () => {
|
||||
const input = '```\n<p>Hello, world!</p>\n```';
|
||||
const output = '<p><pre><code><p>Hello, world!</p></code></pre></p>';
|
||||
assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
|
||||
});
|
||||
|
||||
test('ruby', async () => {
|
||||
const input = '$[ruby $[group *some* text] ignore me]';
|
||||
const output = '<p><ruby><span><span>*some*</span><span> text</span></span><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>';
|
||||
assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromHtml', () => {
|
||||
|
@ -133,5 +177,12 @@ describe('MfmService', () => {
|
|||
test('hashtag', () => {
|
||||
assert.deepStrictEqual(mfmService.fromHtml('<p>a <a href="https://example.com/tags/a">#a</a> d</p>', ['#a']), 'a #a d');
|
||||
});
|
||||
|
||||
test('ruby', () => {
|
||||
assert.deepStrictEqual(
|
||||
mfmService.fromHtml('<ruby> <i>some</i> text <rp>(</rp><rt>ignore me</rt><rp>)</rp> and <rt>more</rt></ruby>'),
|
||||
'$[ruby $[group <i>some</i> text ] ignore me] $[ruby $[group and ] more]'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button
|
||||
class="_button"
|
||||
:class="[$style.root, { [$style.wait]: wait, [$style.active]: isFollowing || hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large }]"
|
||||
:disabled="wait"
|
||||
:disabled="wait || disabled"
|
||||
@click="onClick"
|
||||
>
|
||||
<template v-if="!wait">
|
||||
|
@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -51,13 +51,16 @@ const props = withDefaults(defineProps<{
|
|||
user: Misskey.entities.UserDetailed,
|
||||
full?: boolean,
|
||||
large?: boolean,
|
||||
disabled?: boolean,
|
||||
}>(), {
|
||||
full: false,
|
||||
large: false,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(_: 'update:user', value: Misskey.entities.UserDetailed): void
|
||||
(_: 'update:user', value: Misskey.entities.UserDetailed): void,
|
||||
(_: 'update:wait', value: boolean): void,
|
||||
}>();
|
||||
|
||||
const isFollowing = ref(props.user.isFollowing);
|
||||
|
@ -65,6 +68,9 @@ const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFro
|
|||
const wait = ref(false);
|
||||
const connection = useStream().useChannel('main');
|
||||
|
||||
// Emit the "wait" status so external components can synchronize state
|
||||
watch(wait, value => emit('update:wait', value));
|
||||
|
||||
if (props.user.isFollowing == null && $i) {
|
||||
misskeyApi('users/show', {
|
||||
userId: props.user.id,
|
||||
|
|
|
@ -122,6 +122,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<template v-else-if="notification.type === 'follow'">
|
||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
||||
<div v-if="full" :class="$style.followRequestCommands">
|
||||
<MkFollowButton v-if="userDetailed" :class="$style.followCommandButton" :user="userDetailed" :transparent="false" :full="false"/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="notification.type === 'followRequestAccepted'">
|
||||
<div :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</div>
|
||||
|
@ -136,6 +139,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
|
||||
<MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
|
||||
<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
||||
<MkFollowButton v-if="userDetailed" :class="$style.followCommandButton" :user="userDetailed" :transparent="false" :full="false"/>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else-if="notification.type === 'test'" :class="$style.text">{{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }}</span>
|
||||
|
@ -179,8 +183,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { Ref, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { UserDetailed } from 'misskey-js/autogen/models.js';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
||||
|
@ -190,6 +195,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
|
@ -202,6 +208,26 @@ const props = withDefaults(defineProps<{
|
|||
full: false,
|
||||
});
|
||||
|
||||
const userDetailed: Ref<UserDetailed | null> = ref(null);
|
||||
|
||||
// watch() is required because computed() doesn't support async.
|
||||
watch(props, async () => {
|
||||
const type = props.notification.type;
|
||||
|
||||
// To avoid extra lookups, only do the query when it actually matters.
|
||||
if (type === 'follow' || type === 'receiveFollowRequest') {
|
||||
const user = await misskeyApi('users/show', {
|
||||
userId: props.notification.userId,
|
||||
});
|
||||
|
||||
userDetailed.value = user;
|
||||
followRequestDone.value = !user.hasPendingFollowRequestToYou;
|
||||
} else {
|
||||
userDetailed.value = null;
|
||||
followRequestDone.value = false;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
type ExportCompletedNotification = Misskey.entities.Notification & { type: 'exportCompleted' };
|
||||
|
||||
const exportEntityName = {
|
||||
|
@ -216,7 +242,7 @@ const exportEntityName = {
|
|||
userList: i18n.ts.lists,
|
||||
} as const satisfies Record<ExportCompletedNotification['exportedEntity'], string>;
|
||||
|
||||
const followRequestDone = ref(false);
|
||||
const followRequestDone = ref(true);
|
||||
|
||||
const acceptFollowRequest = () => {
|
||||
if (!('user' in props.notification)) return;
|
||||
|
@ -434,13 +460,24 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
|||
.followRequestCommands {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
max-width: 300px;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.followRequestCommandButton {
|
||||
max-width: 175px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flexSpacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.followCommandButton {
|
||||
margin-left: auto;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reactionsItem {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
|
|
@ -366,6 +366,29 @@ if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
|
|||
cw.value = props.reply.cw;
|
||||
}
|
||||
|
||||
// apply default CW
|
||||
if ($i.defaultCW) {
|
||||
useCw.value = true;
|
||||
|
||||
if (!cw.value || $i.defaultCWPriority === 'default') {
|
||||
cw.value = $i.defaultCW;
|
||||
} else if ($i.defaultCWPriority !== 'parent') {
|
||||
// This is a fancy way of simulating /\bsearch\b/ without a regular expression.
|
||||
// We're checking to see whether the default CW appears inside the existing CW, but *only* if there's word boundaries.
|
||||
const parts = cw.value.split($i.defaultCW);
|
||||
const hasExistingDefaultCW = parts.length === 2 && !/\w$/.test(parts[0]) && !/^\w/.test(parts[1]);
|
||||
if (!hasExistingDefaultCW) {
|
||||
// We need to merge the CWs
|
||||
if ($i.defaultCWPriority === 'defaultParent') {
|
||||
cw.value = `${$i.defaultCW}, ${cw.value}`;
|
||||
} else if ($i.defaultCWPriority === 'parentDefault') {
|
||||
cw.value = `${cw.value}, ${$i.defaultCW}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// else { do nothing, because existing CW takes priority. }
|
||||
}
|
||||
|
||||
function watchForDraft() {
|
||||
watch(text, () => saveDraft());
|
||||
watch(useCw, () => saveDraft());
|
||||
|
|
|
@ -358,6 +358,10 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
|
||||
}
|
||||
}
|
||||
case 'group': { // this is mostly a hack for the insides of `ruby`
|
||||
style = '';
|
||||
break;
|
||||
}
|
||||
case 'unixtime': {
|
||||
const child = token.children[0];
|
||||
const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
|
||||
|
|
|
@ -155,10 +155,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkSwitch v-model="keepCw" @update:modelValue="save()">{{ i18n.ts.keepCw }}</MkSwitch>
|
||||
|
||||
<MkInput v-model="defaultCW" type="text" manualSave @update:modelValue="save()">
|
||||
<template #label>{{ i18n.ts.defaultCW }}</template>
|
||||
<template #caption>{{ i18n.ts.defaultCWDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkSelect v-model="defaultCWPriority" :disabled="!defaultCW || !keepCw" @update:modelValue="save()">
|
||||
<template #label>{{ i18n.ts.defaultCWPriority }}</template>
|
||||
<template #caption>{{ i18n.ts.defaultCWPriorityDescription }}</template>
|
||||
<option value="default">{{ i18n.ts._defaultCWPriority.default }}</option>
|
||||
<option value="parent">{{ i18n.ts._defaultCWPriority.parent }}</option>
|
||||
<option value="parentDefault">{{ i18n.ts._defaultCWPriority.parentDefault }}</option>
|
||||
<option value="defaultParent">{{ i18n.ts._defaultCWPriority.defaultParent }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<MkSwitch v-model="keepCw" @update:modelValue="save()">{{ i18n.ts.keepCw }}</MkSwitch>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -194,6 +208,8 @@ const hideOnlineStatus = ref($i.hideOnlineStatus);
|
|||
const publicReactions = ref($i.publicReactions);
|
||||
const followingVisibility = ref($i.followingVisibility);
|
||||
const followersVisibility = ref($i.followersVisibility);
|
||||
const defaultCW = ref($i.defaultCW);
|
||||
const defaultCWPriority = ref($i.defaultCWPriority);
|
||||
|
||||
const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
|
||||
const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
|
||||
|
@ -252,6 +268,8 @@ function save() {
|
|||
publicReactions: !!publicReactions.value,
|
||||
followingVisibility: followingVisibility.value,
|
||||
followersVisibility: followersVisibility.value,
|
||||
defaultCWPriority: defaultCWPriority.value,
|
||||
defaultCW: defaultCW.value,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/>
|
||||
|
||||
<div :key="user.id" class="main _panel">
|
||||
<div class="banner-container" :style="style">
|
||||
<div class="banner-container" :class="{ [$style.bannerContainerTall]: useTallBanner }" :style="style">
|
||||
<div ref="bannerEl" class="banner" :style="style"></div>
|
||||
<div class="fade"></div>
|
||||
<div class="title">
|
||||
|
@ -39,12 +39,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<li v-if="user.isBlocking">{{ i18n.ts.blocked }}</li>
|
||||
<li v-if="user.isBlocked && $i.isModerator">{{ i18n.ts.blockingYou }}</li>
|
||||
</ul>
|
||||
<div class="actions">
|
||||
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
|
||||
<MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||
<div :class="$style.actions" class="actions">
|
||||
<button :class="$style.actionsMenu" class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
|
||||
<MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :class="$style.actionsFollow" :disabled="disableFollowControls" :inline="true" :transparent="false" :full="true" class="koudoku" @update:wait="onFollowButtonDisabledChanged"/>
|
||||
<div v-if="hasFollowRequest" :class="$style.actionsBanner">{{ i18n.ts.receiveFollowRequest }}</div>
|
||||
<MkButton v-if="hasFollowRequest" :class="$style.actionsAccept" :disabled="disableFollowControls" :inline="true" :transparent="false" :full="true" rounded primary @click="acceptFollowRequest"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
|
||||
<MkButton v-if="hasFollowRequest" :class="$style.actionsReject" :disabled="disableFollowControls" :inline="true" :transparent="false" :full="true" rounded danger @click="rejectFollowRequest"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
<MkAvatar class="avatar" :class="{ [$style.avatarTall]: useTallBanner }" :user="user" indicator/>
|
||||
<div class="title">
|
||||
<MkUserName :user="user" :nowrap="false" class="name"/>
|
||||
<div class="bottom">
|
||||
|
@ -220,8 +223,8 @@ import MkSparkle from '@/components/MkSparkle.vue';
|
|||
|
||||
const MkNote = defineAsyncComponent(() =>
|
||||
defaultStore.state.noteDesign === 'sharkey'
|
||||
? import('@/components/SkNote.vue')
|
||||
: import('@/components/MkNote.vue'),
|
||||
? import('@/components/SkNote.vue')
|
||||
: import('@/components/MkNote.vue'),
|
||||
);
|
||||
|
||||
function calcAge(birthdate: string): number {
|
||||
|
@ -387,6 +390,42 @@ async function updateMemo() {
|
|||
isEditingMemo.value = false;
|
||||
}
|
||||
|
||||
// Set true to disable the follow / follow request controls
|
||||
const disableFollowControls = ref(false);
|
||||
const hasFollowRequest = computed(() => user.value.hasPendingFollowRequestToYou);
|
||||
const useTallBanner = computed(() => hasFollowRequest.value && narrow.value);
|
||||
|
||||
async function onFollowButtonDisabledChanged(disabled: boolean) {
|
||||
try {
|
||||
// Refresh the UI after MkFollowButton changes the follow relation
|
||||
if (!disabled) {
|
||||
user.value = await os.apiWithDialog('users/show', { userId: user.value.id });
|
||||
}
|
||||
} finally {
|
||||
disableFollowControls.value = disabled;
|
||||
}
|
||||
}
|
||||
|
||||
async function acceptFollowRequest() {
|
||||
try {
|
||||
disableFollowControls.value = true;
|
||||
await os.apiWithDialog('following/requests/accept', { userId: user.value.id });
|
||||
user.value = await os.apiWithDialog('users/show', { userId: user.value.id });
|
||||
} finally {
|
||||
disableFollowControls.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectFollowRequest() {
|
||||
try {
|
||||
disableFollowControls.value = true;
|
||||
await os.apiWithDialog('following/requests/reject', { userId: user.value.id });
|
||||
user.value = await os.apiWithDialog('users/show', { userId: user.value.id });
|
||||
} finally {
|
||||
disableFollowControls.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch([props.user], () => {
|
||||
memoDraft.value = props.user.memo;
|
||||
});
|
||||
|
@ -863,4 +902,48 @@ onUnmounted(() => {
|
|||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-template-rows: min-content min-content min-content;
|
||||
grid-template-columns: min-content auto 1fr;
|
||||
grid-template-areas:
|
||||
"menu follow follow"
|
||||
"banner banner banner"
|
||||
"accept accept reject";
|
||||
}
|
||||
|
||||
.actionsMenu {
|
||||
grid-area: menu;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.actionsFollow {
|
||||
grid-area: follow;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.actionsBanner {
|
||||
grid-area: banner;
|
||||
justify-self: center;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.actionsAccept {
|
||||
grid-area: accept;
|
||||
}
|
||||
|
||||
.actionsReject {
|
||||
grid-area: reject;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.bannerContainerTall {
|
||||
height: 200px !important;
|
||||
}
|
||||
|
||||
.avatarTall {
|
||||
top: 150px !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4244,6 +4244,9 @@ export type components = {
|
|||
/** Format: date-time */
|
||||
lastUsed: string;
|
||||
}[];
|
||||
defaultCW: string | null;
|
||||
/** @enum {string} */
|
||||
defaultCWPriority: 'default' | 'parent' | 'defaultParent' | 'parentDefault';
|
||||
};
|
||||
UserDetailedNotMe: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'];
|
||||
MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly'];
|
||||
|
@ -22777,6 +22780,9 @@ export type operations = {
|
|||
};
|
||||
emailNotificationTypes?: string[];
|
||||
alsoKnownAs?: string[];
|
||||
defaultCW?: string | null;
|
||||
/** @enum {string} */
|
||||
defaultCWPriority?: 'default' | 'parent' | 'defaultParent' | 'parentDefault';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -438,4 +438,14 @@ _permissions:
|
|||
robotsTxt: "Custom robots.txt"
|
||||
robotsTxtDescription: "Adding entries here will override the default robots.txt packaged with Sharkey."
|
||||
|
||||
defaultCW: "Default content warning for new posts"
|
||||
defaultCWDescription: "The value here will be auto-filled as the content warning for all new posts and replies."
|
||||
defaultCWPriority: "Automatic CW priority"
|
||||
defaultCWPriorityDescription: "Select preferred action when default CW and keep CW settings are both enabled at the same time."
|
||||
_defaultCWPriority:
|
||||
default: "Use Default (use the default CW, ignoring the inherited CW)"
|
||||
parent: "Use Parent (use the inherited CW, ignoring the default CW)"
|
||||
defaultParent: "Use Default, then Parent (use the default CW, and append the inherited CW)"
|
||||
parentDefault: "Use Parent, then Default (use the inherited CW, and append the default CW)"
|
||||
|
||||
id: "ID"
|
||||
|
|
Loading…
Add table
Reference in a new issue