mirror of
https://activitypub.software/TransFem-org/Sharkey
synced 2024-12-22 16:30:13 +00:00
Merge branch 'develop' into future
This commit is contained in:
commit
4ddee7b01e
23 changed files with 557 additions and 84 deletions
|
@ -415,7 +415,7 @@ antennaKeywordsDescription: "Separate with spaces for an AND condition or with l
|
||||||
notifyAntenna: "Notify about new notes"
|
notifyAntenna: "Notify about new notes"
|
||||||
withFileAntenna: "Only notes with files"
|
withFileAntenna: "Only notes with files"
|
||||||
enableServiceworker: "Enable Push-Notifications for your Browser"
|
enableServiceworker: "Enable Push-Notifications for your Browser"
|
||||||
antennaUsersDescription: "List one username per line"
|
antennaUsersDescription: "List one username per line. Use \"*@instance.com\" to specify all users of an instance"
|
||||||
caseSensitive: "Case sensitive"
|
caseSensitive: "Case sensitive"
|
||||||
withReplies: "Include replies"
|
withReplies: "Include replies"
|
||||||
connectedTo: "Following account(s) are connected"
|
connectedTo: "Following account(s) are connected"
|
||||||
|
@ -1830,7 +1830,7 @@ _registry:
|
||||||
domain: "Domain"
|
domain: "Domain"
|
||||||
createKey: "Create key"
|
createKey: "Create key"
|
||||||
_aboutMisskey:
|
_aboutMisskey:
|
||||||
about: "Sharkey is open-source software based on Misskey which has been in developed since 2014 by syuilo."
|
about: "Sharkey is open-source software based on Misskey which has been in development by syuilo since 2014."
|
||||||
contributors: "Main contributors"
|
contributors: "Main contributors"
|
||||||
allContributors: "All contributors"
|
allContributors: "All contributors"
|
||||||
source: "Source code"
|
source: "Source code"
|
||||||
|
@ -2478,6 +2478,7 @@ _moderationLogTypes:
|
||||||
unsetUserAvatar: "Unset this user's avatar"
|
unsetUserAvatar: "Unset this user's avatar"
|
||||||
unsetUserBanner: "Unset this user's banner"
|
unsetUserBanner: "Unset this user's banner"
|
||||||
_mfm:
|
_mfm:
|
||||||
|
uncommonFeature: "This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks"
|
||||||
intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax."
|
intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax."
|
||||||
dummy: "Sharkey expands the world of the Fediverse"
|
dummy: "Sharkey expands the world of the Fediverse"
|
||||||
mention: "Mention"
|
mention: "Mention"
|
||||||
|
@ -2542,10 +2543,16 @@ _mfm:
|
||||||
rotateDescription: "Turns content by a specified angle."
|
rotateDescription: "Turns content by a specified angle."
|
||||||
position: "Position"
|
position: "Position"
|
||||||
positionDescription: "Move content by a specified amount."
|
positionDescription: "Move content by a specified amount."
|
||||||
|
crop: "Crop"
|
||||||
|
cropDescription: "Crop content."
|
||||||
|
followMouse: "Follow Mouse"
|
||||||
|
followMouseDescription: "Content will follow the mouse. On mobile it will follow wherever the user taps."
|
||||||
scale: "Scale"
|
scale: "Scale"
|
||||||
scaleDescription: "Scale content by a specified amount."
|
scaleDescription: "Scale content by a specified amount."
|
||||||
foreground: "Foreground color"
|
foreground: "Foreground color"
|
||||||
foregroundDescription: "Change the foreground color of text."
|
foregroundDescription: "Change the foreground color of text."
|
||||||
|
fade: 'Fade'
|
||||||
|
fadeDescription: 'Fade text in and out.'
|
||||||
background: "Background color"
|
background: "Background color"
|
||||||
backgroundDescription: "Change the background color of text."
|
backgroundDescription: "Change the background color of text."
|
||||||
plain: "Plain"
|
plain: "Plain"
|
||||||
|
|
|
@ -404,7 +404,7 @@ antennaKeywordsDescription: "Separar con espacios es una declaración AND, separ
|
||||||
notifyAntenna: "Notificar nueva nota"
|
notifyAntenna: "Notificar nueva nota"
|
||||||
withFileAntenna: "Sólo notas con archivos adjuntados"
|
withFileAntenna: "Sólo notas con archivos adjuntados"
|
||||||
enableServiceworker: "Activar ServiceWorker"
|
enableServiceworker: "Activar ServiceWorker"
|
||||||
antennaUsersDescription: "Elegir nombres de usuarios separados por una linea nueva"
|
antennaUsersDescription: "Elegir nombres de usuarios separados por una linea nueva. Utilice \"*@instance.com\" para especificar todos los usuarios de una instancia."
|
||||||
caseSensitive: "Distinguir mayúsculas de minúsculas"
|
caseSensitive: "Distinguir mayúsculas de minúsculas"
|
||||||
withReplies: "Incluir respuestas"
|
withReplies: "Incluir respuestas"
|
||||||
connectedTo: "Estas cuentas están conectadas"
|
connectedTo: "Estas cuentas están conectadas"
|
||||||
|
|
|
@ -36,7 +36,6 @@ export async function jobQueue() {
|
||||||
});
|
});
|
||||||
|
|
||||||
jobQueue.get(QueueProcessorService).start();
|
jobQueue.get(QueueProcessorService).start();
|
||||||
jobQueue.get(ChartManagementService).start();
|
|
||||||
|
|
||||||
return jobQueue;
|
return jobQueue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ async function main() {
|
||||||
ev.mount();
|
ev.mount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cluster.isWorker || envOption.disableClustering) {
|
if (cluster.isWorker) {
|
||||||
await workerMain();
|
await workerMain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -212,6 +212,8 @@ export function loadConfig(): Config {
|
||||||
{} as Source,
|
{} as Source,
|
||||||
) as Source;
|
) as Source;
|
||||||
|
|
||||||
|
applyEnvOverrides(config);
|
||||||
|
|
||||||
const url = tryCreateUrl(config.url);
|
const url = tryCreateUrl(config.url);
|
||||||
const version = meta.version;
|
const version = meta.version;
|
||||||
const host = url.host;
|
const host = url.host;
|
||||||
|
@ -304,3 +306,123 @@ function convertRedisOptions(options: RedisOptionsSource, host: string): RedisOp
|
||||||
db: options.db ?? 0,
|
db: options.db ?? 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
this function allows overriding any string-valued config option with
|
||||||
|
a sensible-named environment variable
|
||||||
|
|
||||||
|
e.g. `MK_CONFIG_MEILISEARCH_APIKEY` sets `config.meilisearch.apikey`
|
||||||
|
|
||||||
|
you can also override a single `dbSlave` value,
|
||||||
|
e.g. `MK_CONFIG_DBSLAVES_1_PASS` sets the password for the 2nd
|
||||||
|
database replica (the first one would be
|
||||||
|
`MK_CONFIG_DBSLAVES_0_PASS`); in this case, `config.dbSlaves` must
|
||||||
|
be set to an array of the right size already in the file
|
||||||
|
|
||||||
|
values can be read from files, too: setting `MK_DB_PASS_FILE` to
|
||||||
|
`/some/file` would set the main database password to the contents of
|
||||||
|
`/some/file` (trimmed of whitespaces)
|
||||||
|
*/
|
||||||
|
function applyEnvOverrides(config: Source) {
|
||||||
|
// these inner functions recurse through the config structure, using
|
||||||
|
// the given steps, building the env variable name
|
||||||
|
|
||||||
|
function _apply_top(steps: (string | number)[]) {
|
||||||
|
_walk('', [], steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _walk(name: string, path: (string | number)[], steps: (string | number)[]) {
|
||||||
|
// are there more steps after this one? recurse
|
||||||
|
if (steps.length > 1) {
|
||||||
|
const thisStep = steps.shift();
|
||||||
|
if (thisStep === null || thisStep === undefined) return;
|
||||||
|
|
||||||
|
// if a step is not a simple value, iterate through it
|
||||||
|
if (typeof thisStep === 'object') {
|
||||||
|
for (const thisOneStep of thisStep) {
|
||||||
|
_descend(name, path, thisOneStep, steps);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_descend(name, path, thisStep, steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the actual override has happened at the bottom of the
|
||||||
|
// recursion, we're done
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the last step, same thing as above
|
||||||
|
const lastStep = steps[0];
|
||||||
|
|
||||||
|
if (typeof lastStep === 'object') {
|
||||||
|
for (const lastOneStep of lastStep) {
|
||||||
|
_lastBit(name, path, lastOneStep);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_lastBit(name, path, lastStep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _step2name(step: string|number): string {
|
||||||
|
return step.toString().replaceAll(/[^a-z0-9]+/gi,'').toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// this recurses down, bailing out if there's no config to override
|
||||||
|
function _descend(name: string, path: (string | number)[], thisStep: string | number, steps: (string | number)[]) {
|
||||||
|
name = `${name}${_step2name(thisStep)}_`;
|
||||||
|
path = [ ...path, thisStep ];
|
||||||
|
_walk(name, path, steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the bottom of the recursion: look at the environment and
|
||||||
|
// set the value
|
||||||
|
function _lastBit(name: string, path: (string | number)[], lastStep: string | number) {
|
||||||
|
name = `MK_CONFIG_${name}${_step2name(lastStep)}`;
|
||||||
|
|
||||||
|
const val = process.env[name];
|
||||||
|
if (val != null && val != undefined) {
|
||||||
|
_assign(path, lastStep, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = process.env[`${name}_FILE`];
|
||||||
|
if (file) {
|
||||||
|
_assign(path, lastStep, fs.readFileSync(file, 'utf-8').trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const alwaysStrings = { 'chmodSocket': 1 };
|
||||||
|
|
||||||
|
function _assign(path: (string | number)[], lastStep: string | number, value: string) {
|
||||||
|
let thisConfig = config;
|
||||||
|
for (const step of path) {
|
||||||
|
if (!thisConfig[step]) {
|
||||||
|
thisConfig[step] = {};
|
||||||
|
}
|
||||||
|
thisConfig = thisConfig[step];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alwaysStrings[lastStep]) {
|
||||||
|
if (value.match(/^[0-9]+$/)) {
|
||||||
|
value = parseInt(value);
|
||||||
|
} else if (value.match(/^(true|false)$/i)) {
|
||||||
|
value = !!value.match(/^true$/i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thisConfig[lastStep] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// these are all the settings that can be overridden
|
||||||
|
|
||||||
|
_apply_top([['url', 'port', 'socket', 'chmodSocket', 'disableHsts']]);
|
||||||
|
_apply_top(['db', ['host', 'port', 'db', 'user', 'pass']]);
|
||||||
|
_apply_top(['dbSlaves', config.dbSlaves?.keys(), ['host', 'port', 'db', 'user', 'pass']]);
|
||||||
|
_apply_top([
|
||||||
|
['redis', 'redisForPubsub', 'redisForJobQueue', 'redisForTimelines'],
|
||||||
|
['host','port','username','pass','db','prefix'],
|
||||||
|
]);
|
||||||
|
_apply_top(['meilisearch', ['host', 'port', 'apikey', 'ssl', 'index', 'scope']]);
|
||||||
|
_apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]);
|
||||||
|
_apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'videoThumbnailGenerator']]);
|
||||||
|
_apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]);
|
||||||
|
}
|
||||||
|
|
|
@ -133,13 +133,17 @@ export class AntennaService implements OnApplicationShutdown {
|
||||||
const { username, host } = Acct.parse(x);
|
const { username, host } = Acct.parse(x);
|
||||||
return this.utilityService.getFullApAccount(username, host).toLowerCase();
|
return this.utilityService.getFullApAccount(username, host).toLowerCase();
|
||||||
});
|
});
|
||||||
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
|
const matchUser = this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase();
|
||||||
|
const matchWildcard = this.utilityService.getFullApAccount('*', noteUser.host).toLowerCase();
|
||||||
|
if (!accts.includes(matchUser) && !accts.includes(matchWildcard)) return false;
|
||||||
} else if (antenna.src === 'users_blacklist') {
|
} else if (antenna.src === 'users_blacklist') {
|
||||||
const accts = antenna.users.map(x => {
|
const accts = antenna.users.map(x => {
|
||||||
const { username, host } = Acct.parse(x);
|
const { username, host } = Acct.parse(x);
|
||||||
return this.utilityService.getFullApAccount(username, host).toLowerCase();
|
return this.utilityService.getFullApAccount(username, host).toLowerCase();
|
||||||
});
|
});
|
||||||
if (accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
|
const matchUser = this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase();
|
||||||
|
const matchWildcard = this.utilityService.getFullApAccount('*', noteUser.host).toLowerCase();
|
||||||
|
if (accts.includes(matchUser) || accts.includes(matchWildcard)) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keywords = antenna.keywords
|
const keywords = antenna.keywords
|
||||||
|
|
|
@ -632,7 +632,8 @@ export class DriveService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
|
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
|
||||||
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
|
const profile = await this.userProfilesRepository.findOneBy({ userId: file.userId });
|
||||||
|
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw || (profile !== null && profile!.alwaysMarkNsfw);
|
||||||
|
|
||||||
if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) {
|
if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) {
|
||||||
throw new DriveService.InvalidFileNameError();
|
throw new DriveService.InvalidFileNameError();
|
||||||
|
|
|
@ -699,6 +699,24 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
dm.addFollowersRecipe();
|
dm.addFollowersRecipe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['public', 'home'].includes(note.visibility)) {
|
||||||
|
// Send edit event to all users who replied to,
|
||||||
|
// renoted a post or reacted to a note.
|
||||||
|
const noteId = note.id;
|
||||||
|
const users = await this.usersRepository.createQueryBuilder()
|
||||||
|
.where(
|
||||||
|
'id IN (SELECT "userId" FROM note WHERE "replyId" = :noteId OR "renoteId" = :noteId UNION SELECT "userId" FROM note_reaction WHERE "noteId" = :noteId)',
|
||||||
|
{ noteId },
|
||||||
|
)
|
||||||
|
.andWhere('host IS NOT NULL')
|
||||||
|
.getMany();
|
||||||
|
for (const u of users) {
|
||||||
|
// User was verified to be remote by checking
|
||||||
|
// whether host IS NOT NULL in SQL query.
|
||||||
|
dm.addDirectRecipe(u as MiRemoteUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (['public'].includes(note.visibility)) {
|
if (['public'].includes(note.visibility)) {
|
||||||
this.relayService.deliverToRelays(user, noteActivity);
|
this.relayService.deliverToRelays(user, noteActivity);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,12 @@ export const paramDef = {
|
||||||
sinceDate: { type: 'integer' },
|
sinceDate: { type: 'integer' },
|
||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
|
||||||
|
withRenotes: { type: 'boolean', default: true },
|
||||||
|
withFiles: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Only show notes that have attached files.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ['channelId'],
|
required: ['channelId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -89,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (me) this.activeUsersChart.read(me);
|
if (me) this.activeUsersChart.read(me);
|
||||||
|
|
||||||
if (!serverSettings.enableFanoutTimeline) {
|
if (!serverSettings.enableFanoutTimeline) {
|
||||||
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me);
|
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me), me);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.fanoutTimelineEndpointService.timeline({
|
return await this.fanoutTimelineEndpointService.timeline({
|
||||||
|
@ -100,9 +106,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
me,
|
me,
|
||||||
useDbFallback: true,
|
useDbFallback: true,
|
||||||
redisTimelines: [`channelTimeline:${channel.id}`],
|
redisTimelines: [`channelTimeline:${channel.id}`],
|
||||||
excludePureRenotes: false,
|
excludePureRenotes: !ps.withRenotes,
|
||||||
|
excludeNoFiles: ps.withFiles,
|
||||||
dbFallback: async (untilId, sinceId, limit) => {
|
dbFallback: async (untilId, sinceId, limit) => {
|
||||||
return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me);
|
return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -112,7 +119,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
untilId: string | null,
|
untilId: string | null,
|
||||||
sinceId: string | null,
|
sinceId: string | null,
|
||||||
limit: number,
|
limit: number,
|
||||||
channelId: string
|
channelId: string,
|
||||||
|
withFiles: boolean,
|
||||||
|
withRenotes: boolean,
|
||||||
}, me: MiLocalUser | null) {
|
}, me: MiLocalUser | null) {
|
||||||
//#region fallback to database
|
//#region fallback to database
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||||
|
@ -128,6 +137,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.withRenotes === false) {
|
||||||
|
query.andWhere(new Brackets(qb => {
|
||||||
|
qb.orWhere('note.renoteId IS NULL');
|
||||||
|
qb.orWhere(new Brackets(qb => {
|
||||||
|
qb.orWhere('note.text IS NOT NULL');
|
||||||
|
qb.orWhere('note.fileIds != \'{}\'');
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.withFiles) {
|
||||||
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
return await query.limit(ps.limit).getMany();
|
return await query.limit(ps.limit).getMany();
|
||||||
|
|
|
@ -146,8 +146,8 @@ export class MastoConverters {
|
||||||
display_name: user.name ?? user.username,
|
display_name: user.name ?? user.username,
|
||||||
locked: user.isLocked,
|
locked: user.isLocked,
|
||||||
created_at: this.idService.parse(user.id).date.toISOString(),
|
created_at: this.idService.parse(user.id).date.toISOString(),
|
||||||
followers_count: user.followersCount,
|
followers_count: profile?.followersVisibility === 'public' ? user.followersCount : 0,
|
||||||
following_count: user.followingCount,
|
following_count: profile?.followingVisibility === 'public' ? user.followingCount : 0,
|
||||||
statuses_count: user.notesCount,
|
statuses_count: user.notesCount,
|
||||||
note: profile?.description ?? '',
|
note: profile?.description ?? '',
|
||||||
url: user.uri ?? acctUrl,
|
url: user.uri ?? acctUrl,
|
||||||
|
|
|
@ -15,6 +15,8 @@ class ChannelChannel extends Channel {
|
||||||
public static shouldShare = false;
|
public static shouldShare = false;
|
||||||
public static requireCredential = false as const;
|
public static requireCredential = false as const;
|
||||||
private channelId: string;
|
private channelId: string;
|
||||||
|
private withFiles: boolean;
|
||||||
|
private withRenotes: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
@ -29,6 +31,8 @@ class ChannelChannel extends Channel {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async init(params: any) {
|
public async init(params: any) {
|
||||||
this.channelId = params.channelId as string;
|
this.channelId = params.channelId as string;
|
||||||
|
this.withFiles = params.withFiles ?? false;
|
||||||
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
|
|
||||||
// Subscribe stream
|
// Subscribe stream
|
||||||
this.subscriber.on('notesStream', this.onNote);
|
this.subscriber.on('notesStream', this.onNote);
|
||||||
|
@ -38,6 +42,10 @@ class ChannelChannel extends Channel {
|
||||||
private async onNote(note: Packed<'Note'>) {
|
private async onNote(note: Packed<'Note'>) {
|
||||||
if (note.channelId !== this.channelId) return;
|
if (note.channelId !== this.channelId) return;
|
||||||
|
|
||||||
|
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||||
|
|
||||||
|
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||||
|
|
||||||
if (this.isNoteMutedOrBlocked(note)) return;
|
if (this.isNoteMutedOrBlocked(note)) return;
|
||||||
|
|
||||||
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
|
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
|
||||||
|
|
81
packages/frontend/src/components/CkFollowMouse.vue
Normal file
81
packages/frontend/src/components/CkFollowMouse.vue
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<span ref="container" :class="$style.root">
|
||||||
|
<span ref="el" :class="$style.inner" style="position: absolute">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, onUnmounted, shallowRef } from 'vue';
|
||||||
|
const el = shallowRef<HTMLElement>();
|
||||||
|
const container = shallowRef<HTMLElement>();
|
||||||
|
const props = defineProps({
|
||||||
|
x: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
speed: {
|
||||||
|
type: String,
|
||||||
|
default: '0.1s',
|
||||||
|
},
|
||||||
|
rotateByVelocity: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastX = 0;
|
||||||
|
let lastY = 0;
|
||||||
|
let oldAngle = 0;
|
||||||
|
|
||||||
|
function lerp(a, b, alpha) {
|
||||||
|
return a + alpha * (b - a);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePosition = (mouseEvent: MouseEvent) => {
|
||||||
|
if (el.value && container.value) {
|
||||||
|
const containerRect = container.value.getBoundingClientRect();
|
||||||
|
const newX = mouseEvent.clientX - containerRect.left;
|
||||||
|
const newY = mouseEvent.clientY - containerRect.top;
|
||||||
|
let transform = `translate(calc(${props.x ? newX : 0}px - 50%), calc(${props.y ? newY : 0}px - 50%))`;
|
||||||
|
if (props.rotateByVelocity) {
|
||||||
|
const deltaX = newX - lastX;
|
||||||
|
const deltaY = newY - lastY;
|
||||||
|
const angle = lerp(
|
||||||
|
oldAngle,
|
||||||
|
Math.atan2(deltaY, deltaX) * (180 / Math.PI),
|
||||||
|
0.1,
|
||||||
|
);
|
||||||
|
transform += ` rotate(${angle}deg)`;
|
||||||
|
oldAngle = angle;
|
||||||
|
}
|
||||||
|
el.value.style.transform = transform;
|
||||||
|
el.value.style.transition = `transform ${props.speed}`;
|
||||||
|
lastX = newX;
|
||||||
|
lastY = newY;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('mousemove', updatePosition);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('mousemove', updatePosition);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.inner {
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -304,6 +304,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.crop }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.cropDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_crop" />
|
||||||
|
<MkTextarea v-model="preview_crop"><span>MFM</span></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="section _block">
|
<div class="section _block">
|
||||||
<div class="title">{{ i18n.ts._mfm.position }}</div>
|
<div class="title">{{ i18n.ts._mfm.position }}</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -314,6 +324,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section _block" style="overflow: hidden">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.followMouse }}</div>
|
||||||
|
<MkInfo warn>{{ i18n.ts._mfm.uncommonFeature }}</MkInfo>
|
||||||
|
<br/>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.followMouseDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_followmouse"/>
|
||||||
|
<MkTextarea v-model="preview_followmouse"><span>MFM</span></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="section _block">
|
<div class="section _block">
|
||||||
<div class="title">{{ i18n.ts._mfm.scale }}</div>
|
<div class="title">{{ i18n.ts._mfm.scale }}</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -324,6 +346,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section _block">
|
||||||
|
<div class="title">{{ i18n.ts._mfm.fade }}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ i18n.ts._mfm.fadeDescription }}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<Mfm :text="preview_fade" />
|
||||||
|
<MkTextarea v-model="preview_fade"><span>MFM</span></MkTextarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="section _block">
|
<div class="section _block">
|
||||||
<div class="title">{{ i18n.ts._mfm.foreground }}</div>
|
<div class="title">{{ i18n.ts._mfm.foreground }}</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -362,18 +394,19 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import MkInfo from './MkInfo.vue';
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import { i18n } from "@/i18n.js";
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const preview_mention = ref("@example");
|
const preview_mention = ref('@example');
|
||||||
const preview_hashtag = ref("#test");
|
const preview_hashtag = ref('#test');
|
||||||
const preview_link = ref(`[${i18n.ts._mfm.dummy}](https://joinsharkey.org)`);
|
const preview_link = ref(`[${i18n.ts._mfm.dummy}](https://joinsharkey.org)`);
|
||||||
const preview_emoji = ref(`:heart:`);
|
const preview_emoji = ref(':heart:');
|
||||||
const preview_bold = ref(`**${i18n.ts._mfm.dummy}**`);
|
const preview_bold = ref(`**${i18n.ts._mfm.dummy}**`);
|
||||||
const preview_small = ref(
|
const preview_small = ref(
|
||||||
`<small>${i18n.ts._mfm.dummy}</small>`,
|
`<small>${i18n.ts._mfm.dummy}</small>`,
|
||||||
|
@ -386,33 +419,33 @@ const preview_blockCode = ref(
|
||||||
'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
|
'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',
|
||||||
);
|
);
|
||||||
const preview_inlineMath = ref(
|
const preview_inlineMath = ref(
|
||||||
"\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)",
|
'\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)',
|
||||||
);
|
);
|
||||||
const preview_blockMath = ref("\\[x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\]");
|
const preview_blockMath = ref('\\[x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\]');
|
||||||
const preview_quote = ref(`> ${i18n.ts._mfm.dummy}`);
|
const preview_quote = ref(`> ${i18n.ts._mfm.dummy}`);
|
||||||
const preview_search = ref(
|
const preview_search = ref(
|
||||||
`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]`,
|
`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]`,
|
||||||
);
|
);
|
||||||
const preview_jelly = ref(
|
const preview_jelly = ref(
|
||||||
"$[jelly 🍮] $[jelly.speed=3s 🍮] $[jelly.delay=3s 🍮] $[jelly.loop=3 🍮]",
|
'$[jelly 🍮] $[jelly.speed=3s 🍮] $[jelly.delay=3s 🍮] $[jelly.loop=3 🍮]',
|
||||||
);
|
);
|
||||||
const preview_tada = ref(
|
const preview_tada = ref(
|
||||||
"$[tada 🍮] $[tada.speed=3s 🍮] $[tada.delay=3s 🍮] $[tada.loop=3 🍮]",
|
'$[tada 🍮] $[tada.speed=3s 🍮] $[tada.delay=3s 🍮] $[tada.loop=3 🍮]',
|
||||||
);
|
);
|
||||||
const preview_jump = ref(
|
const preview_jump = ref(
|
||||||
"$[jump 🍮] $[jump.speed=3s 🍮] $[jump.delay=3s 🍮] $[jump.loop=3 🍮]",
|
'$[jump 🍮] $[jump.speed=3s 🍮] $[jump.delay=3s 🍮] $[jump.loop=3 🍮]',
|
||||||
);
|
);
|
||||||
const preview_bounce = ref(
|
const preview_bounce = ref(
|
||||||
"$[bounce 🍮] $[bounce.speed=3s 🍮] $[bounce.delay=3s 🍮] $[bounce.loop=3 🍮]",
|
'$[bounce 🍮] $[bounce.speed=3s 🍮] $[bounce.delay=3s 🍮] $[bounce.loop=3 🍮]',
|
||||||
);
|
);
|
||||||
const preview_shake = ref(
|
const preview_shake = ref(
|
||||||
"$[shake 🍮] $[shake.speed=3s 🍮] $[shake.delay=3s 🍮] $[shake.loop=3 🍮]",
|
'$[shake 🍮] $[shake.speed=3s 🍮] $[shake.delay=3s 🍮] $[shake.loop=3 🍮]',
|
||||||
);
|
);
|
||||||
const preview_twitch = ref(
|
const preview_twitch = ref(
|
||||||
"$[twitch 🍮] $[twitch.speed=3s 🍮] $[twitch.delay=3s 🍮] $[twitch.loop=3 🍮]",
|
'$[twitch 🍮] $[twitch.speed=3s 🍮] $[twitch.delay=3s 🍮] $[twitch.loop=3 🍮]',
|
||||||
);
|
);
|
||||||
const preview_spin = ref(
|
const preview_spin = ref(
|
||||||
"$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=3s 🍮] $[spin.delay=3s 🍮] $[spin.loop=3 🍮]",
|
'$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=3s 🍮] $[spin.delay=3s 🍮] $[spin.loop=3 🍮]',
|
||||||
);
|
);
|
||||||
const preview_flip = ref(
|
const preview_flip = ref(
|
||||||
`$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`,
|
`$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`,
|
||||||
|
@ -420,26 +453,31 @@ const preview_flip = ref(
|
||||||
const preview_font = ref(
|
const preview_font = ref(
|
||||||
`$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]`,
|
`$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]`,
|
||||||
);
|
);
|
||||||
const preview_x2 = ref("$[x2 🍮]");
|
const preview_x2 = ref('$[x2 🍮]');
|
||||||
const preview_x3 = ref("$[x3 🍮]");
|
const preview_x3 = ref('$[x3 🍮]');
|
||||||
const preview_x4 = ref("$[x4 🍮]");
|
const preview_x4 = ref('$[x4 🍮]');
|
||||||
const preview_blur = ref(`$[blur ${i18n.ts._mfm.dummy}]`);
|
const preview_blur = ref(`$[blur ${i18n.ts._mfm.dummy}]`);
|
||||||
const preview_rainbow = ref(
|
const preview_rainbow = ref(
|
||||||
"$[rainbow 🍮] $[rainbow.speed=3s 🍮] $[rainbow.delay=3s 🍮] $[rainbow.loop=3 🍮]",
|
'$[rainbow 🍮] $[rainbow.speed=3s 🍮] $[rainbow.delay=3s 🍮] $[rainbow.loop=3 🍮]',
|
||||||
);
|
);
|
||||||
const preview_sparkle = ref("$[sparkle 🍮]");
|
const preview_sparkle = ref('$[sparkle 🍮]');
|
||||||
const preview_rotate = ref(
|
const preview_rotate = ref(
|
||||||
"$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]",
|
'$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]',
|
||||||
);
|
);
|
||||||
const preview_position = ref("$[position.y=-1 🍮]\n$[position.x=-1 🍮]");
|
const preview_position = ref('$[position.y=-1 🍮]\n$[position.x=-1 🍮]');
|
||||||
|
const preview_crop = ref(
|
||||||
|
"$[crop.top=50 🍮] $[crop.right=50 🍮] $[crop.bottom=50 🍮] $[crop.left=50 🍮]",
|
||||||
|
);
|
||||||
|
const preview_followmouse = ref('$[followmouse.x 🍮]\n$[followmouse.x,y,rotateByVelocity,speed=0.4 🍮]');
|
||||||
const preview_scale = ref(
|
const preview_scale = ref(
|
||||||
"$[scale.x=1.3 🍮]\n$[scale.x=1.5,y=3 🍮]\n$[scale.y=0.3 🍮]",
|
'$[scale.x=1.3 🍮]\n$[scale.x=1.5,y=3 🍮]\n$[scale.y=0.3 🍮]',
|
||||||
);
|
);
|
||||||
const preview_fg = ref("$[fg.color=eb6f92 Text color]");
|
const preview_fg = ref('$[fg.color=eb6f92 Text color]');
|
||||||
const preview_bg = ref("$[bg.color=31748f Background color]");
|
const preview_bg = ref('$[bg.color=31748f Background color]');
|
||||||
const preview_plain = ref(
|
const preview_plain = ref(
|
||||||
"<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>",
|
'<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>',
|
||||||
);
|
);
|
||||||
|
const preview_fade = ref(`$[fade 🍮] $[fade.out 🍮] $[fade.speed=3s 🍮] $[fade.delay=3s 🍮]`);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -154,6 +154,8 @@ function connectChannel() {
|
||||||
} else if (props.src === 'channel') {
|
} else if (props.src === 'channel') {
|
||||||
if (props.channel == null) return;
|
if (props.channel == null) return;
|
||||||
connection = stream.useChannel('channel', {
|
connection = stream.useChannel('channel', {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
channelId: props.channel,
|
channelId: props.channel,
|
||||||
});
|
});
|
||||||
} else if (props.src === 'role') {
|
} else if (props.src === 'role') {
|
||||||
|
@ -234,6 +236,8 @@ function updatePaginationQuery() {
|
||||||
} else if (props.src === 'channel') {
|
} else if (props.src === 'channel') {
|
||||||
endpoint = 'channels/timeline';
|
endpoint = 'channels/timeline';
|
||||||
query = {
|
query = {
|
||||||
|
withRenotes: props.withRenotes,
|
||||||
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
channelId: props.channel,
|
channelId: props.channel,
|
||||||
};
|
};
|
||||||
} else if (props.src === 'role') {
|
} else if (props.src === 'role') {
|
||||||
|
|
|
@ -235,6 +235,6 @@ onMounted(init);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
background-image: url(/client-assets/oneko.gif);
|
background-image: var(--oneko-image, url(/client-assets/oneko.gif));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { VNode, h, defineAsyncComponent, SetupContext, provide } from 'vue';
|
import { VNode, h, defineAsyncComponent, SetupContext, provide } from 'vue';
|
||||||
import * as mfm from '@transfem-org/sfm-js';
|
import * as mfm from '@transfem-org/sfm-js';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import CkFollowMouse from '../CkFollowMouse.vue';
|
||||||
import MkUrl from '@/components/global/MkUrl.vue';
|
import MkUrl from '@/components/global/MkUrl.vue';
|
||||||
import MkTime from '@/components/global/MkTime.vue';
|
import MkTime from '@/components/global/MkTime.vue';
|
||||||
import MkLink from '@/components/MkLink.vue';
|
import MkLink from '@/components/MkLink.vue';
|
||||||
|
@ -230,11 +231,49 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
||||||
}
|
}
|
||||||
return h(MkSparkle, {}, genEl(token.children, scale));
|
return h(MkSparkle, {}, genEl(token.children, scale));
|
||||||
}
|
}
|
||||||
|
case 'fade': {
|
||||||
|
// Dont run with reduced motion on
|
||||||
|
if (!defaultStore.state.animation) {
|
||||||
|
style = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = token.props.args.out
|
||||||
|
? 'alternate-reverse'
|
||||||
|
: 'alternate';
|
||||||
|
const speed = validTime(token.props.args.speed) ?? '1.5s';
|
||||||
|
const delay = validTime(token.props.args.delay) ?? '0s';
|
||||||
|
const loop = safeParseFloat(token.props.args.loop) ?? 'infinite';
|
||||||
|
style = `animation: mfm-fade ${speed} ${delay} linear ${loop}; animation-direction: ${direction};`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'rotate': {
|
case 'rotate': {
|
||||||
const degrees = safeParseFloat(token.props.args.deg) ?? 90;
|
const degrees = safeParseFloat(token.props.args.deg) ?? 90;
|
||||||
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
|
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'followmouse': {
|
||||||
|
// Make sure advanced MFM is on and that reduced motion is off
|
||||||
|
if (!useAnim) {
|
||||||
|
style = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = (!!token.props.args.x);
|
||||||
|
let y = (!!token.props.args.y);
|
||||||
|
|
||||||
|
if (!x && !y) {
|
||||||
|
x = true;
|
||||||
|
y = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(CkFollowMouse, {
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
speed: validTime(token.props.args.speed) ?? '0.1s',
|
||||||
|
rotateByVelocity: !!token.props.args.rotateByVelocity,
|
||||||
|
}, genEl(token.children, scale));
|
||||||
|
}
|
||||||
case 'position': {
|
case 'position': {
|
||||||
if (!defaultStore.state.advancedMfm) break;
|
if (!defaultStore.state.advancedMfm) break;
|
||||||
const x = safeParseFloat(token.props.args.x) ?? 0;
|
const x = safeParseFloat(token.props.args.x) ?? 0;
|
||||||
|
@ -242,6 +281,22 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
||||||
style = `transform: translateX(${x}em) translateY(${y}em);`;
|
style = `transform: translateX(${x}em) translateY(${y}em);`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'crop': {
|
||||||
|
const top = Number.parseFloat(
|
||||||
|
(token.props.args.top ?? '0').toString(),
|
||||||
|
);
|
||||||
|
const right = Number.parseFloat(
|
||||||
|
(token.props.args.right ?? '0').toString(),
|
||||||
|
);
|
||||||
|
const bottom = Number.parseFloat(
|
||||||
|
(token.props.args.bottom ?? '0').toString(),
|
||||||
|
);
|
||||||
|
const left = Number.parseFloat(
|
||||||
|
(token.props.args.left ?? '0').toString(),
|
||||||
|
);
|
||||||
|
style = `clip-path: inset(${top}% ${right}% ${bottom}% ${left}%);`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'scale': {
|
case 'scale': {
|
||||||
if (!defaultStore.state.advancedMfm) {
|
if (!defaultStore.state.advancedMfm) {
|
||||||
style = '';
|
style = '';
|
||||||
|
|
|
@ -162,7 +162,7 @@ export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://launcher.moe/error.png';
|
||||||
export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://launcher.moe/missingpage.webp';
|
export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://launcher.moe/missingpage.webp';
|
||||||
export const DEFAULT_INFO_IMAGE_URL = 'https://launcher.moe/nothinghere.png';
|
export const DEFAULT_INFO_IMAGE_URL = 'https://launcher.moe/nothinghere.png';
|
||||||
|
|
||||||
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
|
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime', 'crop', 'fade', 'followmouse'];
|
||||||
export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
|
export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
|
||||||
tada: ['speed=', 'delay='],
|
tada: ['speed=', 'delay='],
|
||||||
jelly: ['speed=', 'delay='],
|
jelly: ['speed=', 'delay='],
|
||||||
|
@ -186,4 +186,7 @@ export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
|
||||||
rotate: ['deg='],
|
rotate: ['deg='],
|
||||||
ruby: [],
|
ruby: [],
|
||||||
unixtime: [],
|
unixtime: [],
|
||||||
|
fade: ['speed=', 'delay=', 'loop=', 'out'],
|
||||||
|
crop: ['top=', 'bottom=', 'left=', 'right='],
|
||||||
|
followmouse: ['x', 'y', 'rotateByVelocity', 'speed='],
|
||||||
};
|
};
|
||||||
|
|
|
@ -111,7 +111,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ph-user-circle ph-bold ph-lg"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
|
<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ph-user-circle ph-bold ph-lg"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
|
||||||
<MkButton v-if="iAmModerator" inline danger @click="unsetUserBanner"><i class="ph-photo ph-bold ph-lg"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
|
<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserBanner"><i class="ph-image ph-bold ph-lg"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
|
||||||
|
<MkButton v-if="iAmModerator" inline danger @click="deleteAllFiles"><i class="ph-cloud ph-bold ph-lg"></i> {{ i18n.ts.deleteAllFiles }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
|
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -265,6 +266,7 @@ function createFetcher() {
|
||||||
moderator.value = info.value.isModerator;
|
moderator.value = info.value.isModerator;
|
||||||
silenced.value = info.value.isSilenced;
|
silenced.value = info.value.isSilenced;
|
||||||
approved.value = info.value.approved;
|
approved.value = info.value.approved;
|
||||||
|
markedAsNSFW.value = info.value.alwaysMarkNsfw;
|
||||||
suspended.value = info.value.isSuspended;
|
suspended.value = info.value.isSuspended;
|
||||||
moderationNote.value = info.value.moderationNote;
|
moderationNote.value = info.value.moderationNote;
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
|
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
|
||||||
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
|
<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
|
||||||
|
|
||||||
<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
|
<MkTimeline :key="channelId + withRenotes + onlyFiles" src="channel" :channel="channelId" :withRenotes="withRenotes" :onlyFiles="onlyFiles" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'featured'" key="featured">
|
<div v-else-if="tab === 'featured'" key="featured">
|
||||||
<MkNotes :pagination="featuredPagination"/>
|
<MkNotes :pagination="featuredPagination"/>
|
||||||
|
@ -95,6 +95,7 @@ import { isSupportShare } from '@/scripts/navigator.js';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { useRouter } from '@/router/supplier.js';
|
import { useRouter } from '@/router/supplier.js';
|
||||||
|
import { deepMerge } from '@/scripts/merge.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -116,6 +117,15 @@ const featuredPagination = computed(() => ({
|
||||||
channelId: props.channelId,
|
channelId: props.channelId,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
const withRenotes = computed<boolean>({
|
||||||
|
get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
|
||||||
|
set: (x) => saveTlFilter('withRenotes', x),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onlyFiles = computed<boolean>({
|
||||||
|
get: () => defaultStore.reactiveState.tl.value.filter.onlyFiles,
|
||||||
|
set: (x) => saveTlFilter('onlyFiles', x),
|
||||||
|
});
|
||||||
|
|
||||||
watch(() => props.channelId, async () => {
|
watch(() => props.channelId, async () => {
|
||||||
channel.value = await misskeyApi('channels/show', {
|
channel.value = await misskeyApi('channels/show', {
|
||||||
|
@ -136,6 +146,13 @@ watch(() => props.channelId, async () => {
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) {
|
||||||
|
if (key !== 'withReplies' || $i) {
|
||||||
|
const out = deepMerge({ filter: { [key]: newValue } }, defaultStore.state.tl);
|
||||||
|
defaultStore.set('tl', out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function edit() {
|
function edit() {
|
||||||
router.push(`/channels/${channel.value?.id}/edit`);
|
router.push(`/channels/${channel.value?.id}/edit`);
|
||||||
}
|
}
|
||||||
|
@ -192,7 +209,21 @@ async function search() {
|
||||||
|
|
||||||
const headerActions = computed(() => {
|
const headerActions = computed(() => {
|
||||||
if (channel.value && channel.value.userId) {
|
if (channel.value && channel.value.userId) {
|
||||||
const headerItems: PageHeaderItem[] = [];
|
const headerItems: PageHeaderItem[] = [{
|
||||||
|
icon: 'ph-dots-three ph-bold ph-lg',
|
||||||
|
text: i18n.ts.options,
|
||||||
|
handler: (ev) => {
|
||||||
|
os.popupMenu([{
|
||||||
|
type: 'switch',
|
||||||
|
text: i18n.ts.showRenotes,
|
||||||
|
ref: withRenotes,
|
||||||
|
}, {
|
||||||
|
type: 'switch',
|
||||||
|
text: i18n.ts.fileAttachedOnly,
|
||||||
|
ref: onlyFiles,
|
||||||
|
}], ev.currentTarget ?? ev.target);
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
headerItems.push({
|
headerItems.push({
|
||||||
icon: 'ph-share-network ph-bold ph-lg',
|
icon: 'ph-share-network ph-bold ph-lg',
|
||||||
|
|
|
@ -11,10 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
|
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
|
||||||
<div :class="$style.tl">
|
<div :class="$style.tl">
|
||||||
<MkTimeline
|
<MkTimeline
|
||||||
ref="tlEl" :key="listId"
|
ref="tlEl" :key="listId + withRenotes + onlyFiles"
|
||||||
src="list"
|
src="list"
|
||||||
:list="listId"
|
:list="listId"
|
||||||
:sound="true"
|
:sound="true"
|
||||||
|
:withRenotes="withRenotes"
|
||||||
|
:onlyFiles="onlyFiles"
|
||||||
@queue="queueUpdated"
|
@queue="queueUpdated"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,6 +34,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { useRouter } from '@/router/supplier.js';
|
import { useRouter } from '@/router/supplier.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { deepMerge } from '@/scripts/merge.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -43,6 +48,21 @@ const list = ref<Misskey.entities.UserList | null>(null);
|
||||||
const queue = ref(0);
|
const queue = ref(0);
|
||||||
const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
|
const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||||
const rootEl = shallowRef<HTMLElement>();
|
const rootEl = shallowRef<HTMLElement>();
|
||||||
|
const withRenotes = computed<boolean>({
|
||||||
|
get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
|
||||||
|
set: (x) => saveTlFilter('withRenotes', x),
|
||||||
|
});
|
||||||
|
const onlyFiles = computed<boolean>({
|
||||||
|
get: () => defaultStore.reactiveState.tl.value.filter.onlyFiles,
|
||||||
|
set: (x) => saveTlFilter('onlyFiles', x),
|
||||||
|
});
|
||||||
|
|
||||||
|
function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) {
|
||||||
|
if (key !== 'withReplies' || $i) {
|
||||||
|
const out = deepMerge({ filter: { [key]: newValue } }, defaultStore.state.tl);
|
||||||
|
defaultStore.set('tl', out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(() => props.listId, async () => {
|
watch(() => props.listId, async () => {
|
||||||
list.value = await misskeyApi('users/lists/show', {
|
list.value = await misskeyApi('users/lists/show', {
|
||||||
|
@ -63,6 +83,20 @@ function settings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerActions = computed(() => list.value ? [{
|
const headerActions = computed(() => list.value ? [{
|
||||||
|
icon: 'ph-dots-three ph-bold ph-lg',
|
||||||
|
text: i18n.ts.options,
|
||||||
|
handler: (ev) => {
|
||||||
|
os.popupMenu([{
|
||||||
|
type: 'switch',
|
||||||
|
text: i18n.ts.showRenotes,
|
||||||
|
ref: withRenotes,
|
||||||
|
}, {
|
||||||
|
type: 'switch',
|
||||||
|
text: i18n.ts.fileAttachedOnly,
|
||||||
|
ref: onlyFiles,
|
||||||
|
}], ev.currentTarget ?? ev.target);
|
||||||
|
},
|
||||||
|
}, {
|
||||||
icon: 'ph-gear ph-bold ph-lg',
|
icon: 'ph-gear ph-bold ph-lg',
|
||||||
text: i18n.ts.settings,
|
text: i18n.ts.settings,
|
||||||
handler: settings,
|
handler: settings,
|
||||||
|
|
|
@ -706,3 +706,12 @@ html[data-color-mode=dark] ._woodenFrame {
|
||||||
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
|
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
|
||||||
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
|
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes mfm-fade {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,13 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div style="padding: 8px; text-align: center;">
|
<div style="padding: 8px; text-align: center;">
|
||||||
<MkButton primary gradate rounded inline small @click="post"><i class="ph-pencil-simple ph-bold ph-lg"></i></MkButton>
|
<MkButton primary gradate rounded inline small @click="post"><i class="ph-pencil-simple ph-bold ph-lg"></i></MkButton>
|
||||||
</div>
|
</div>
|
||||||
<MkTimeline ref="timeline" src="channel" :channel="column.channelId"/>
|
<MkTimeline ref="timeline" src="channel" :channel="column.channelId" :key="column.channelId + column.withRenotes + column.onlyFiles" :withRenotes="withRenotes" :onlyFiles="onlyFiles"/>
|
||||||
</template>
|
</template>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { shallowRef } from 'vue';
|
import { watch, ref, shallowRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import XColumn from './column.vue';
|
import XColumn from './column.vue';
|
||||||
import { updateColumn, Column } from './deck-store.js';
|
import { updateColumn, Column } from './deck-store.js';
|
||||||
|
@ -36,6 +36,20 @@ const props = defineProps<{
|
||||||
|
|
||||||
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||||
const channel = shallowRef<Misskey.entities.Channel>();
|
const channel = shallowRef<Misskey.entities.Channel>();
|
||||||
|
const withRenotes = ref(props.column.withRenotes ?? true);
|
||||||
|
const onlyFiles = ref(props.column.onlyFiles ?? false);
|
||||||
|
|
||||||
|
watch(withRenotes, v => {
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
withRenotes: v,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(onlyFiles, v => {
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
onlyFiles: v,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (props.column.channelId == null) {
|
if (props.column.channelId == null) {
|
||||||
setChannel();
|
setChannel();
|
||||||
|
@ -75,5 +89,13 @@ const menu = [{
|
||||||
icon: 'ph-pencil-simple ph-bold ph-lg',
|
icon: 'ph-pencil-simple ph-bold ph-lg',
|
||||||
text: i18n.ts.selectChannel,
|
text: i18n.ts.selectChannel,
|
||||||
action: setChannel,
|
action: setChannel,
|
||||||
|
}, {
|
||||||
|
type: 'switch',
|
||||||
|
text: i18n.ts.showRenotes,
|
||||||
|
ref: withRenotes,
|
||||||
|
}, {
|
||||||
|
type: 'switch',
|
||||||
|
text: i18n.ts.fileAttachedOnly,
|
||||||
|
ref: onlyFiles,
|
||||||
}];
|
}];
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ph-list ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
<i class="ph-list ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :withRenotes="withRenotes"/>
|
<MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :key="column.listId + column.withRenotes + column.onlyFiles" :withRenotes="withRenotes" :onlyFiles="onlyFiles"/>
|
||||||
</XColumn>
|
</XColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||||
const withRenotes = ref(props.column.withRenotes ?? true);
|
const withRenotes = ref(props.column.withRenotes ?? true);
|
||||||
|
const onlyFiles = ref(props.column.onlyFiles ?? false);
|
||||||
|
|
||||||
if (props.column.listId == null) {
|
if (props.column.listId == null) {
|
||||||
setList();
|
setList();
|
||||||
|
@ -40,6 +41,12 @@ watch(withRenotes, v => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(onlyFiles, v => {
|
||||||
|
updateColumn(props.column.id, {
|
||||||
|
onlyFiles: v,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
async function setList() {
|
async function setList() {
|
||||||
const lists = await misskeyApi('users/lists/list');
|
const lists = await misskeyApi('users/lists/list');
|
||||||
const { canceled, result: list } = await os.select({
|
const { canceled, result: list } = await os.select({
|
||||||
|
@ -75,5 +82,10 @@ const menu = [
|
||||||
text: i18n.ts.showRenotes,
|
text: i18n.ts.showRenotes,
|
||||||
ref: withRenotes,
|
ref: withRenotes,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'switch',
|
||||||
|
text: i18n.ts.fileAttachedOnly,
|
||||||
|
ref: onlyFiles,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue