mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-03 23:14:13 +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