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"
|
||||
withFileAntenna: "Only notes with files"
|
||||
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"
|
||||
withReplies: "Include replies"
|
||||
connectedTo: "Following account(s) are connected"
|
||||
|
@ -1830,7 +1830,7 @@ _registry:
|
|||
domain: "Domain"
|
||||
createKey: "Create key"
|
||||
_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"
|
||||
allContributors: "All contributors"
|
||||
source: "Source code"
|
||||
|
@ -2478,6 +2478,7 @@ _moderationLogTypes:
|
|||
unsetUserAvatar: "Unset this user's avatar"
|
||||
unsetUserBanner: "Unset this user's banner"
|
||||
_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."
|
||||
dummy: "Sharkey expands the world of the Fediverse"
|
||||
mention: "Mention"
|
||||
|
@ -2542,10 +2543,16 @@ _mfm:
|
|||
rotateDescription: "Turns content by a specified angle."
|
||||
position: "Position"
|
||||
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"
|
||||
scaleDescription: "Scale content by a specified amount."
|
||||
foreground: "Foreground color"
|
||||
foregroundDescription: "Change the foreground color of text."
|
||||
fade: 'Fade'
|
||||
fadeDescription: 'Fade text in and out.'
|
||||
background: "Background color"
|
||||
backgroundDescription: "Change the background color of text."
|
||||
plain: "Plain"
|
||||
|
|
|
@ -404,7 +404,7 @@ antennaKeywordsDescription: "Separar con espacios es una declaración AND, separ
|
|||
notifyAntenna: "Notificar nueva nota"
|
||||
withFileAntenna: "Sólo notas con archivos adjuntados"
|
||||
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"
|
||||
withReplies: "Incluir respuestas"
|
||||
connectedTo: "Estas cuentas están conectadas"
|
||||
|
|
|
@ -36,7 +36,6 @@ export async function jobQueue() {
|
|||
});
|
||||
|
||||
jobQueue.get(QueueProcessorService).start();
|
||||
jobQueue.get(ChartManagementService).start();
|
||||
|
||||
return jobQueue;
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ async function main() {
|
|||
ev.mount();
|
||||
}
|
||||
}
|
||||
if (cluster.isWorker || envOption.disableClustering) {
|
||||
if (cluster.isWorker) {
|
||||
await workerMain();
|
||||
}
|
||||
|
||||
|
|
|
@ -212,6 +212,8 @@ export function loadConfig(): Config {
|
|||
{} as Source,
|
||||
) as Source;
|
||||
|
||||
applyEnvOverrides(config);
|
||||
|
||||
const url = tryCreateUrl(config.url);
|
||||
const version = meta.version;
|
||||
const host = url.host;
|
||||
|
@ -304,3 +306,123 @@ function convertRedisOptions(options: RedisOptionsSource, host: string): RedisOp
|
|||
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);
|
||||
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') {
|
||||
const accts = antenna.users.map(x => {
|
||||
const { username, host } = Acct.parse(x);
|
||||
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
|
||||
|
|
|
@ -632,7 +632,8 @@ export class DriveService {
|
|||
|
||||
@bindThis
|
||||
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)) {
|
||||
throw new DriveService.InvalidFileNameError();
|
||||
|
|
|
@ -699,6 +699,24 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
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)) {
|
||||
this.relayService.deliverToRelays(user, noteActivity);
|
||||
}
|
||||
|
|
|
@ -51,6 +51,12 @@ export const paramDef = {
|
|||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
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'],
|
||||
} as const;
|
||||
|
@ -89,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (me) this.activeUsersChart.read(me);
|
||||
|
||||
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({
|
||||
|
@ -100,9 +106,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
me,
|
||||
useDbFallback: true,
|
||||
redisTimelines: [`channelTimeline:${channel.id}`],
|
||||
excludePureRenotes: false,
|
||||
excludePureRenotes: !ps.withRenotes,
|
||||
excludeNoFiles: ps.withFiles,
|
||||
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,
|
||||
sinceId: string | null,
|
||||
limit: number,
|
||||
channelId: string
|
||||
channelId: string,
|
||||
withFiles: boolean,
|
||||
withRenotes: boolean,
|
||||
}, me: MiLocalUser | null) {
|
||||
//#region fallback to database
|
||||
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.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
|
||||
|
||||
return await query.limit(ps.limit).getMany();
|
||||
|
|
|
@ -146,8 +146,8 @@ export class MastoConverters {
|
|||
display_name: user.name ?? user.username,
|
||||
locked: user.isLocked,
|
||||
created_at: this.idService.parse(user.id).date.toISOString(),
|
||||
followers_count: user.followersCount,
|
||||
following_count: user.followingCount,
|
||||
followers_count: profile?.followersVisibility === 'public' ? user.followersCount : 0,
|
||||
following_count: profile?.followingVisibility === 'public' ? user.followingCount : 0,
|
||||
statuses_count: user.notesCount,
|
||||
note: profile?.description ?? '',
|
||||
url: user.uri ?? acctUrl,
|
||||
|
|
|
@ -15,6 +15,8 @@ class ChannelChannel extends Channel {
|
|||
public static shouldShare = false;
|
||||
public static requireCredential = false as const;
|
||||
private channelId: string;
|
||||
private withFiles: boolean;
|
||||
private withRenotes: boolean;
|
||||
|
||||
constructor(
|
||||
private noteEntityService: NoteEntityService,
|
||||
|
@ -29,6 +31,8 @@ class ChannelChannel extends Channel {
|
|||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.channelId = params.channelId as string;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
|
||||
// Subscribe stream
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
|
@ -38,6 +42,10 @@ class ChannelChannel extends Channel {
|
|||
private async onNote(note: Packed<'Note'>) {
|
||||
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.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 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="title">{{ i18n.ts._mfm.position }}</div>
|
||||
<div class="content">
|
||||
|
@ -314,6 +324,18 @@
|
|||
</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="title">{{ i18n.ts._mfm.scale }}</div>
|
||||
<div class="content">
|
||||
|
@ -324,6 +346,16 @@
|
|||
</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="title">{{ i18n.ts._mfm.foreground }}</div>
|
||||
<div class="content">
|
||||
|
@ -362,18 +394,19 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import MkInfo from './MkInfo.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import { i18n } from "@/i18n.js";
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const preview_mention = ref("@example");
|
||||
const preview_hashtag = ref("#test");
|
||||
const preview_mention = ref('@example');
|
||||
const preview_hashtag = ref('#test');
|
||||
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_small = ref(
|
||||
`<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```',
|
||||
);
|
||||
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_search = ref(
|
||||
`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]`,
|
||||
);
|
||||
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(
|
||||
"$[tada 🍮] $[tada.speed=3s 🍮] $[tada.delay=3s 🍮] $[tada.loop=3 🍮]",
|
||||
'$[tada 🍮] $[tada.speed=3s 🍮] $[tada.delay=3s 🍮] $[tada.loop=3 🍮]',
|
||||
);
|
||||
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(
|
||||
"$[bounce 🍮] $[bounce.speed=3s 🍮] $[bounce.delay=3s 🍮] $[bounce.loop=3 🍮]",
|
||||
'$[bounce 🍮] $[bounce.speed=3s 🍮] $[bounce.delay=3s 🍮] $[bounce.loop=3 🍮]',
|
||||
);
|
||||
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(
|
||||
"$[twitch 🍮] $[twitch.speed=3s 🍮] $[twitch.delay=3s 🍮] $[twitch.loop=3 🍮]",
|
||||
'$[twitch 🍮] $[twitch.speed=3s 🍮] $[twitch.delay=3s 🍮] $[twitch.loop=3 🍮]',
|
||||
);
|
||||
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(
|
||||
`$[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(
|
||||
`$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]`,
|
||||
);
|
||||
const preview_x2 = ref("$[x2 🍮]");
|
||||
const preview_x3 = ref("$[x3 🍮]");
|
||||
const preview_x4 = ref("$[x4 🍮]");
|
||||
const preview_x2 = ref('$[x2 🍮]');
|
||||
const preview_x3 = ref('$[x3 🍮]');
|
||||
const preview_x4 = ref('$[x4 🍮]');
|
||||
const preview_blur = ref(`$[blur ${i18n.ts._mfm.dummy}]`);
|
||||
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(
|
||||
"$[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(
|
||||
"$[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_bg = ref("$[bg.color=31748f Background color]");
|
||||
const preview_fg = ref('$[fg.color=eb6f92 Text color]');
|
||||
const preview_bg = ref('$[bg.color=31748f Background color]');
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -154,6 +154,8 @@ function connectChannel() {
|
|||
} else if (props.src === 'channel') {
|
||||
if (props.channel == null) return;
|
||||
connection = stream.useChannel('channel', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
channelId: props.channel,
|
||||
});
|
||||
} else if (props.src === 'role') {
|
||||
|
@ -234,6 +236,8 @@ function updatePaginationQuery() {
|
|||
} else if (props.src === 'channel') {
|
||||
endpoint = 'channels/timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
channelId: props.channel,
|
||||
};
|
||||
} else if (props.src === 'role') {
|
||||
|
|
|
@ -235,6 +235,6 @@ onMounted(init);
|
|||
pointer-events: none;
|
||||
image-rendering: pixelated;
|
||||
z-index: 2147483647;
|
||||
background-image: url(/client-assets/oneko.gif);
|
||||
background-image: var(--oneko-image, url(/client-assets/oneko.gif));
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { VNode, h, defineAsyncComponent, SetupContext, provide } from 'vue';
|
||||
import * as mfm from '@transfem-org/sfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import CkFollowMouse from '../CkFollowMouse.vue';
|
||||
import MkUrl from '@/components/global/MkUrl.vue';
|
||||
import MkTime from '@/components/global/MkTime.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));
|
||||
}
|
||||
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': {
|
||||
const degrees = safeParseFloat(token.props.args.deg) ?? 90;
|
||||
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
|
||||
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': {
|
||||
if (!defaultStore.state.advancedMfm) break;
|
||||
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);`;
|
||||
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': {
|
||||
if (!defaultStore.state.advancedMfm) {
|
||||
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_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[]> = {
|
||||
tada: ['speed=', 'delay='],
|
||||
jelly: ['speed=', 'delay='],
|
||||
|
@ -186,4 +186,7 @@ export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
|
|||
rotate: ['deg='],
|
||||
ruby: [],
|
||||
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>
|
||||
<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>
|
||||
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
|
||||
</div>
|
||||
|
@ -265,6 +266,7 @@ function createFetcher() {
|
|||
moderator.value = info.value.isModerator;
|
||||
silenced.value = info.value.isSilenced;
|
||||
approved.value = info.value.approved;
|
||||
markedAsNSFW.value = info.value.alwaysMarkNsfw;
|
||||
suspended.value = info.value.isSuspended;
|
||||
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'"/>
|
||||
|
||||
<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 v-else-if="tab === 'featured'" key="featured">
|
||||
<MkNotes :pagination="featuredPagination"/>
|
||||
|
@ -95,6 +95,7 @@ import { isSupportShare } from '@/scripts/navigator.js';
|
|||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { deepMerge } from '@/scripts/merge.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -116,6 +117,15 @@ const featuredPagination = computed(() => ({
|
|||
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 () => {
|
||||
channel.value = await misskeyApi('channels/show', {
|
||||
|
@ -136,6 +146,13 @@ watch(() => props.channelId, async () => {
|
|||
}
|
||||
}, { 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() {
|
||||
router.push(`/channels/${channel.value?.id}/edit`);
|
||||
}
|
||||
|
@ -192,7 +209,21 @@ async function search() {
|
|||
|
||||
const headerActions = computed(() => {
|
||||
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({
|
||||
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 :class="$style.tl">
|
||||
<MkTimeline
|
||||
ref="tlEl" :key="listId"
|
||||
ref="tlEl" :key="listId + withRenotes + onlyFiles"
|
||||
src="list"
|
||||
:list="listId"
|
||||
:sound="true"
|
||||
:withRenotes="withRenotes"
|
||||
:onlyFiles="onlyFiles"
|
||||
@queue="queueUpdated"
|
||||
/>
|
||||
</div>
|
||||
|
@ -32,6 +34,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.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();
|
||||
|
||||
|
@ -43,6 +48,21 @@ const list = ref<Misskey.entities.UserList | null>(null);
|
|||
const queue = ref(0);
|
||||
const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
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 () => {
|
||||
list.value = await misskeyApi('users/lists/show', {
|
||||
|
@ -63,6 +83,20 @@ function settings() {
|
|||
}
|
||||
|
||||
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',
|
||||
text: i18n.ts.settings,
|
||||
handler: settings,
|
||||
|
|
|
@ -706,3 +706,12 @@ html[data-color-mode=dark] ._woodenFrame {
|
|||
0% { filter: hue-rotate(0deg) 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;">
|
||||
<MkButton primary gradate rounded inline small @click="post"><i class="ph-pencil-simple ph-bold ph-lg"></i></MkButton>
|
||||
</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>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import { watch, ref, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XColumn from './column.vue';
|
||||
import { updateColumn, Column } from './deck-store.js';
|
||||
|
@ -36,6 +36,20 @@ const props = defineProps<{
|
|||
|
||||
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
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) {
|
||||
setChannel();
|
||||
|
@ -75,5 +89,13 @@ const menu = [{
|
|||
icon: 'ph-pencil-simple ph-bold ph-lg',
|
||||
text: i18n.ts.selectChannel,
|
||||
action: setChannel,
|
||||
}, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
ref: withRenotes,
|
||||
}, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
ref: onlyFiles,
|
||||
}];
|
||||
</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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
@ -29,6 +29,7 @@ const props = defineProps<{
|
|||
|
||||
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
const withRenotes = ref(props.column.withRenotes ?? true);
|
||||
const onlyFiles = ref(props.column.onlyFiles ?? false);
|
||||
|
||||
if (props.column.listId == null) {
|
||||
setList();
|
||||
|
@ -40,6 +41,12 @@ watch(withRenotes, v => {
|
|||
});
|
||||
});
|
||||
|
||||
watch(onlyFiles, v => {
|
||||
updateColumn(props.column.id, {
|
||||
onlyFiles: v,
|
||||
});
|
||||
});
|
||||
|
||||
async function setList() {
|
||||
const lists = await misskeyApi('users/lists/list');
|
||||
const { canceled, result: list } = await os.select({
|
||||
|
@ -75,5 +82,10 @@ const menu = [
|
|||
text: i18n.ts.showRenotes,
|
||||
ref: withRenotes,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
ref: onlyFiles,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
|
Loading…
Reference in a new issue