diff --git a/.config/ci.yml b/.config/ci.yml deleted file mode 100644 index 8730ccab3a..0000000000 --- a/.config/ci.yml +++ /dev/null @@ -1,236 +0,0 @@ -#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# Misskey configuration -#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -# ┌─────┐ -#───┘ URL └───────────────────────────────────────────────────── - -# Final accessible URL seen by a user. -url: http://misskey.local - -# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE -# URL SETTINGS AFTER THAT! - -# ┌───────────────────────┐ -#───┘ Port and TLS settings └─────────────────────────────────── - -# -# Misskey requires a reverse proxy to support HTTPS connections. -# -# +----- https://example.tld/ ------------+ -# +------+ |+-------------+ +----------------+| -# | User | ---> || Proxy (443) | ---> | Misskey (3000) || -# +------+ |+-------------+ +----------------+| -# +---------------------------------------+ -# -# You need to set up a reverse proxy. (e.g. nginx) -# An encrypted connection with HTTPS is highly recommended -# because tokens may be transferred in GET requests. - -# The port that your Misskey server should listen on. -port: 3000 - -# ┌──────────────────────────┐ -#───┘ PostgreSQL configuration └──────────────────────────────── - -db: - host: postgres - port: 5432 - - # Database name - db: postgres - - # Auth - user: postgres - pass: ci - - # Whether disable Caching queries - #disableCache: true - - # Extra Connection options - #extra: - # ssl: true - -dbReplications: false - -# You can configure any number of replicas here -#dbSlaves: -# - -# host: -# port: -# db: -# user: -# pass: -# - -# host: -# port: -# db: -# user: -# pass: - -# ┌─────────────────────┐ -#───┘ Redis configuration └───────────────────────────────────── - -redis: - host: redis - port: 6379 - #family: 0 # 0=Both, 4=IPv4, 6=IPv6 - #pass: example-pass - #prefix: example-prefix - #db: 1 - -#redisForPubsub: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForJobQueue: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForTimelines: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -# ┌───────────────────────────┐ -#───┘ MeiliSearch configuration └───────────────────────────── - -# You can set scope to local (default value) or global -# (include notes from remote). - -#meilisearch: -# host: meilisearch -# port: 7700 -# apiKey: '' -# ssl: true -# index: '' -# scope: global - -# ┌───────────────┐ -#───┘ ID generation └─────────────────────────────────────────── - -# You can select the ID generation method. -# You don't usually need to change this setting, but you can -# change it according to your preferences. - -# Available methods: -# aid ... Short, Millisecond accuracy -# aidx ... Millisecond accuracy -# meid ... Similar to ObjectID, Millisecond accuracy -# ulid ... Millisecond accuracy -# objectid ... This is left for backward compatibility - -# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE -# ID SETTINGS AFTER THAT! - -id: 'aidx' - -# ┌─────────────────────┐ -#───┘ Other configuration └───────────────────────────────────── - -# Whether disable HSTS -#disableHsts: true - -# Number of worker processes -#clusterLimit: 1 - -# Job concurrency per worker -# deliverJobConcurrency: 128 -# inboxJobConcurrency: 16 -# relashionshipJobConcurrency: 16 -# What's relashionshipJob?: -# Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations. - -# Job rate limiter -# deliverJobPerSec: 128 -# inboxJobPerSec: 32 -# relashionshipJobPerSec: 64 - -# Job attempts -# deliverJobMaxAttempts: 12 -# inboxJobMaxAttempts: 8 - -# Local address used for outgoing requests -#outgoingAddress: 127.0.0.1 - -# IP address family used for outgoing request (ipv4, ipv6 or dual) -#outgoingAddressFamily: ipv4 - -# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) -#maxNoteLength: 3000 -# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) -#maxRemoteNoteLength: 100000 -# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) -#maxCwLength: 500 -# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) -#maxRemoteCwLength: 5000 -# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) -#maxAltTextLength: 20000 -# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) -#maxRemoteAltTextLength: 100000 - -# Proxy for HTTP/HTTPS -#proxy: http://127.0.0.1:3128 - -proxyBypassHosts: - - api.deepl.com - - api-free.deepl.com - - www.recaptcha.net - - hcaptcha.com - - challenges.cloudflare.com - -# Proxy for SMTP/SMTPS -#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT -#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 -#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 - -# Media Proxy -#mediaProxy: https://example.com/proxy - -# Proxy remote files (default: true) -# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains. -proxyRemoteFiles: true - -# Movie Thumbnail Generation URL -# There is no reference implementation. -# For example, Misskey will point to the following URL: -# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 -#videoThumbnailGenerator: https://example.com - -# Sign outgoing ActivityPub GET request (default: true) -signToActivityPubGet: true -# Sign outgoing ActivityPub Activities (default: true) -# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. -# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. -# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. -attachLdSignatureForRelays: true -# check that inbound ActivityPub GET requests are signed ("authorized fetch") -checkActivityPubGetSignature: false - -# For security reasons, uploading attachments from the intranet is prohibited, -# but exceptions can be made from the following settings. Default value is "undefined". -# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). -#allowedPrivateNetworks: [ -# '127.0.0.1/32' -#] - -#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] - -# Upload or download file size limits (bytes) -#maxFileSize: 262144000 - -# CHMod-style permission bits to apply to uploaded files. -# Permission bits are specified as a base-8 string representing User/Group/Other permissions. -# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. -#filePermissionBits: '644' diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml deleted file mode 100644 index 342b0f43da..0000000000 --- a/.config/cypress-devcontainer.yml +++ /dev/null @@ -1,229 +0,0 @@ -#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# Misskey configuration -#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -# ┌─────┐ -#───┘ URL └───────────────────────────────────────────────────── - -# Final accessible URL seen by a user. -url: 'http://misskey.local' - -# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE -# URL SETTINGS AFTER THAT! - -# ┌───────────────────────┐ -#───┘ Port and TLS settings └─────────────────────────────────── - -# -# Misskey requires a reverse proxy to support HTTPS connections. -# -# +----- https://example.tld/ ------------+ -# +------+ |+-------------+ +----------------+| -# | User | ---> || Proxy (443) | ---> | Misskey (3000) || -# +------+ |+-------------+ +----------------+| -# +---------------------------------------+ -# -# You need to set up a reverse proxy. (e.g. nginx) -# An encrypted connection with HTTPS is highly recommended -# because tokens may be transferred in GET requests. - -# The port that your Misskey server should listen on. -port: 61812 - -# ┌──────────────────────────┐ -#───┘ PostgreSQL configuration └──────────────────────────────── - -db: - host: db - port: 5432 - - # Database name - db: misskey - - # Auth - user: postgres - pass: postgres - - # Whether disable Caching queries - #disableCache: true - - # Extra Connection options - #extra: - # ssl: true - -dbReplications: false - -# You can configure any number of replicas here -#dbSlaves: -# - -# host: -# port: -# db: -# user: -# pass: -# - -# host: -# port: -# db: -# user: -# pass: - -# ┌─────────────────────┐ -#───┘ Redis configuration └───────────────────────────────────── - -redis: - host: redis - port: 6379 - #family: 0 # 0=Both, 4=IPv4, 6=IPv6 - #pass: example-pass - #prefix: example-prefix - #db: 1 - -#redisForPubsub: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForJobQueue: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForTimelines: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForReactions: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -# ┌───────────────────────────┐ -#───┘ MeiliSearch configuration └───────────────────────────── - -#meilisearch: -# host: meilisearch -# port: 7700 -# apiKey: '' -# ssl: true -# index: '' - -# ┌───────────────┐ -#───┘ ID generation └─────────────────────────────────────────── - -# You can select the ID generation method. -# You don't usually need to change this setting, but you can -# change it according to your preferences. - -# Available methods: -# aid ... Short, Millisecond accuracy -# aidx ... Millisecond accuracy -# meid ... Similar to ObjectID, Millisecond accuracy -# ulid ... Millisecond accuracy -# objectid ... This is left for backward compatibility - -# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE -# ID SETTINGS AFTER THAT! - -id: 'aidx' - -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -# ┌─────────────────────┐ -#───┘ Other configuration └───────────────────────────────────── - -# Whether disable HSTS -#disableHsts: true - -# Number of worker processes -#clusterLimit: 1 - -# Job concurrency per worker -# deliverJobConcurrency: 128 -# inboxJobConcurrency: 16 - -# Job rate limiter -# deliverJobPerSec: 128 -# inboxJobPerSec: 32 - -# Job attempts -# deliverJobMaxAttempts: 12 -# inboxJobMaxAttempts: 8 - -# IP address family used for outgoing request (ipv4, ipv6 or dual) -#outgoingAddressFamily: ipv4 - -# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) -#maxNoteLength: 3000 -# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) -#maxRemoteNoteLength: 100000 -# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) -#maxCwLength: 500 -# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) -#maxRemoteCwLength: 5000 -# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) -#maxAltTextLength: 20000 -# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) -#maxRemoteAltTextLength: 100000 - -# Proxy for HTTP/HTTPS -#proxy: http://127.0.0.1:3128 - -proxyBypassHosts: - - api.deepl.com - - api-free.deepl.com - - www.recaptcha.net - - hcaptcha.com - - challenges.cloudflare.com - -# Proxy for SMTP/SMTPS -#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT -#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 -#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 - -# Media Proxy -#mediaProxy: https://example.com/proxy - -# Proxy remote files (default: true) -proxyRemoteFiles: true - -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - -allowedPrivateNetworks: [ - '127.0.0.1/32' -] - -# Upload or download file size limits (bytes) -#maxFileSize: 262144000 - -# CHMod-style permission bits to apply to uploaded files. -# Permission bits are specified as a base-8 string representing User/Group/Other permissions. -# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. -#filePermissionBits: '644' diff --git a/.config/docker_example.env b/.config/docker_example.env index c61248da2e..7a0261524b 100644 --- a/.config/docker_example.env +++ b/.config/docker_example.env @@ -1,11 +1,4 @@ -# misskey settings -# MISSKEY_URL=https://example.tld/ - # db settings POSTGRES_PASSWORD=example-misskey-pass -# DATABASE_PASSWORD=${POSTGRES_PASSWORD} POSTGRES_USER=example-misskey-user -# DATABASE_USER=${POSTGRES_USER} POSTGRES_DB=misskey -# DATABASE_DB=${POSTGRES_DB} -DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" diff --git a/.config/docker_example.yml b/.config/docker_example.yml index ce2daf3aec..0a21f20320 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -2,68 +2,10 @@ # Misskey configuration #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# ┌──────────────────────────────┐ -#───┘ a boring but important thing └──────────────────────────── - -# -# First of all, let me tell you a story that may possibly be -# boring to you and possibly important to you. -# -# Misskey is licensed under the AGPLv3 license. This license is -# known to be often misunderstood. Please read the following -# instructions carefully and select the appropriate option so -# that you do not negligently cause a license violation. -# - -# -------- -# Option 1: If you host Misskey AS-IS (without any changes to -# the source code. forks are not included). -# -# Step 1: Congratulations! You don't need to do anything. - -# -------- -# Option 2: If you have made changes to the source code (forks -# are included) and publish a Git repository of source -# code. There should be no access restrictions on -# this repository. Strictly speaking, it doesn't have -# to be a Git repository, but you'll probably use Git! -# -# Step 1: Build and run the Misskey server first. -# Step 2: Open in -# your browser with the administrator account. -# Step 3: Enter the URL of your Git repository in the -# "Repository URL" field. - -# -------- -# Option 3: If neither of the above applies to you. -# (In this case, the source code should be published -# on the Misskey interface. IT IS NOT ENOUGH TO -# DISCLOSE THE SOURCE CODE WEHN A USER REQUESTS IT BY -# E-MAIL OR OTHER MEANS. If you are not satisfied -# with this, it is recommended that you read the -# license again carefully. Anyway, enabling this -# option will automatically generate and publish a -# tarball at build time, protecting you from -# inadvertent license violations. (There is no legal -# guarantee, of course.) The tarball will generated -# from the root directory of your codebase. So it is -# also recommended to check directory -# once after building and before activating the server -# to avoid ACCIDENTAL LEAKING OF SENSITIVE INFORMATION. -# To prevent certain files from being included in the -# tarball, add a glob pattern after line 15 in -# . DO NOT FORGET TO BUILD AFTER -# ENABLING THIS OPTION!) -# -# Step 1: Uncomment the following line. -# -# publishTarballInsteadOfProvideRepositoryUrl: true - # ┌─────┐ #───┘ URL └───────────────────────────────────────────────────── # Final accessible URL seen by a user. -# You can set url from an environment variable instead. url: https://example.tld/ # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE @@ -96,11 +38,9 @@ db: port: 5432 # Database name - # You can set db from an environment variable instead. db: misskey # Auth - # You can set user and pass from environment variables instead. user: example-misskey-user pass: example-misskey-pass @@ -116,17 +56,17 @@ dbReplications: false # You can configure any number of replicas here #dbSlaves: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # ┌─────────────────────┐ #───┘ Redis configuration └───────────────────────────────────── @@ -163,20 +103,9 @@ redis: # #prefix: example-prefix # #db: 1 -#redisForReactions: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── -# You can set scope to local (default value) or global -# (include notes from remote). - #meilisearch: # host: meilisearch # port: 7700 @@ -204,21 +133,6 @@ redis: id: 'aidx' -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -231,14 +145,14 @@ id: 'aidx' # Job concurrency per worker # deliverJobConcurrency: 128 # inboxJobConcurrency: 16 -# relationshipJobConcurrency: 16 -# What's relationshipJob?: +# relashionshipJobConcurrency: 16 +# What's relashionshipJob?: # Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations. # Job rate limiter # deliverJobPerSec: 128 -# inboxJobPerSec: 32 -# relationshipJobPerSec: 64 +# inboxJobPerSec: 16 +# relashionshipJobPerSec: 64 # Job attempts # deliverJobMaxAttempts: 12 @@ -250,19 +164,6 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) -#maxNoteLength: 3000 -# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) -#maxRemoteNoteLength: 100000 -# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) -#maxCwLength: 500 -# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) -#maxRemoteCwLength: 5000 -# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) -#maxAltTextLength: 20000 -# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) -#maxRemoteAltTextLength: 100000 - # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 @@ -291,29 +192,12 @@ proxyRemoteFiles: true # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign outgoing ActivityPub GET request (default: true) +# Sign to ActivityPub GET request (default: true) signToActivityPubGet: true -# Sign outgoing ActivityPub Activities (default: true) -# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. -# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. -# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. -attachLdSignatureForRelays: true -# check that inbound ActivityPub GET requests are signed ("authorized fetch") -checkActivityPubGetSignature: false -# For security reasons, uploading attachments from the intranet is prohibited, -# but exceptions can be made from the following settings. Default value is "undefined". -# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). #allowedPrivateNetworks: [ # '127.0.0.1/32' #] -#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] - # Upload or download file size limits (bytes) #maxFileSize: 262144000 - -# CHMod-style permission bits to apply to uploaded files. -# Permission bits are specified as a base-8 string representing User/Group/Other permissions. -# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. -#filePermissionBits: '644' diff --git a/.config/example.yml b/.config/example.yml index 9debb3bf70..7cd710f250 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -2,63 +2,6 @@ # Misskey configuration #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# ┌──────────────────────────────┐ -#───┘ a boring but important thing └──────────────────────────── - -# -# First of all, let me tell you a story that may possibly be -# boring to you and possibly important to you. -# -# Misskey is licensed under the AGPLv3 license. This license is -# known to be often misunderstood. Please read the following -# instructions carefully and select the appropriate option so -# that you do not negligently cause a license violation. -# - -# -------- -# Option 1: If you host Misskey AS-IS (without any changes to -# the source code. forks are not included). -# -# Step 1: Congratulations! You don't need to do anything. - -# -------- -# Option 2: If you have made changes to the source code (forks -# are included) and publish a Git repository of source -# code. There should be no access restrictions on -# this repository. Strictly speaking, it doesn't have -# to be a Git repository, but you'll probably use Git! -# -# Step 1: Build and run the Misskey server first. -# Step 2: Open in -# your browser with the administrator account. -# Step 3: Enter the URL of your Git repository in the -# "Repository URL" field. - -# -------- -# Option 3: If neither of the above applies to you. -# (In this case, the source code should be published -# on the Misskey interface. IT IS NOT ENOUGH TO -# DISCLOSE THE SOURCE CODE WHEN A USER REQUESTS IT BY -# E-MAIL OR OTHER MEANS. If you are not satisfied -# with this, it is recommended that you read the -# license again carefully. Anyway, enabling this -# option will automatically generate and publish a -# tarball at build time, protecting you from -# inadvertent license violations. (There is no legal -# guarantee, of course.) The tarball will generated -# from the root directory of your codebase. So it is -# also recommended to check directory -# once after building and before activating the server -# to avoid ACCIDENTAL LEAKING OF SENSITIVE INFORMATION. -# To prevent certain files from being included in the -# tarball, add a glob pattern after line 15 in -# . DO NOT FORGET TO BUILD AFTER -# ENABLING THIS OPTION!) -# -# Step 1: Uncomment the following line. -# -# publishTarballInsteadOfProvideRepositoryUrl: true - # ┌─────┐ #───┘ URL └───────────────────────────────────────────────────── @@ -99,10 +42,10 @@ db: port: 5432 # Database name - db: sharkey + db: misskey # Auth - user: sharkey + user: example-misskey-user pass: example-misskey-pass # Whether disable Caching queries @@ -172,22 +115,9 @@ redis: # # You can specify more ioredis options... # #username: example-username -#redisForReactions: -# host: localhost -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 -# # You can specify more ioredis options... -# #username: example-username - # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── -# You can set scope to local (default value) or global -# (include notes from remote). - #meilisearch: # host: localhost # port: 7700 @@ -215,21 +145,6 @@ redis: id: 'aidx' -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -242,14 +157,14 @@ id: 'aidx' # Job concurrency per worker #deliverJobConcurrency: 128 #inboxJobConcurrency: 16 -#relationshipJobConcurrency: 16 -# What's relationshipJob?: +#relashionshipJobConcurrency: 16 +# What's relashionshipJob?: # Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations. # Job rate limiter #deliverJobPerSec: 128 -#inboxJobPerSec: 32 -#relationshipJobPerSec: 64 +#inboxJobPerSec: 16 +#relashionshipJobPerSec: 64 # Job attempts #deliverJobMaxAttempts: 12 @@ -261,19 +176,6 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) -#maxNoteLength: 3000 -# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) -#maxRemoteNoteLength: 100000 -# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) -#maxCwLength: 500 -# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) -#maxRemoteCwLength: 5000 -# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) -#maxAltTextLength: 20000 -# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) -#maxRemoteAltTextLength: 100000 - # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 @@ -305,37 +207,15 @@ proxyRemoteFiles: true # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign outgoing ActivityPub GET request (default: true) +# Sign to ActivityPub GET request (default: true) signToActivityPubGet: true -# Sign outgoing ActivityPub Activities (default: true) -# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. -# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. -# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. -attachLdSignatureForRelays: true -# check that inbound ActivityPub GET requests are signed ("authorized fetch") -checkActivityPubGetSignature: false -# For security reasons, uploading attachments from the intranet is prohibited, -# but exceptions can be made from the following settings. Default value is "undefined". -# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). #allowedPrivateNetworks: [ # '127.0.0.1/32' #] -#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] - # Upload or download file size limits (bytes) #maxFileSize: 262144000 -# timeout and maximum size for imports (e.g. note imports) -#import: -# downloadTimeout: 30 -# maxFileSize: 262144000 - # PID File of master process #pidFile: /tmp/misskey.pid - -# CHMod-style permission bits to apply to uploaded files. -# Permission bits are specified as a base-8 string representing User/Group/Other permissions. -# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. -#filePermissionBits: '644' diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fbf959d449..0583a66960 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,22 +1,25 @@ { "name": "Misskey", - "dockerComposeFile": "compose.yml", + "dockerComposeFile": "docker-compose.yml", "service": "app", "workspaceFolder": "/workspace", "features": { - "ghcr.io/devcontainers/features/node:1": { - "version": "20.16.0" + "ghcr.io/devcontainers-contrib/features/pnpm:2": { + "version": "8.9.2" }, - "ghcr.io/devcontainers-contrib/features/corepack:1": {} + "ghcr.io/devcontainers/features/node:1": { + "version": "20.5.1" + } }, "forwardPorts": [3000], - "postCreateCommand": "/bin/bash .devcontainer/init.sh", + "postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh", "customizations": { "vscode": { "extensions": [ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", "Vue.volar", + "Vue.vscode-typescript-vue-plugin", "Orta.vscode-jest", "dbaeumer.vscode-eslint", "mrmlnc.vscode-json5" diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 3eb4fc2879..3d57d1245d 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -56,17 +56,17 @@ dbReplications: false # You can configure any number of replicas here #dbSlaves: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # ┌─────────────────────┐ #───┘ Redis configuration └───────────────────────────────────── @@ -103,14 +103,6 @@ redis: # #prefix: example-prefix # #db: 1 -#redisForReactions: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── @@ -140,21 +132,6 @@ redis: id: 'aidx' -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -170,7 +147,7 @@ id: 'aidx' # Job rate limiter # deliverJobPerSec: 128 -# inboxJobPerSec: 32 +# inboxJobPerSec: 16 # Job attempts # deliverJobMaxAttempts: 12 diff --git a/.devcontainer/compose.yml b/.devcontainer/docker-compose.yml similarity index 93% rename from .devcontainer/compose.yml rename to .devcontainer/docker-compose.yml index d02d2a8f4a..2809cd2ca4 100644 --- a/.devcontainer/compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,3 +1,5 @@ +version: '3.8' + services: app: build: @@ -6,7 +8,6 @@ services: volumes: - ../:/workspace:cached - - node_modules:/workspace/node_modules command: sleep infinity @@ -45,7 +46,6 @@ services: volumes: postgres-data: redis-data: - node_modules: networks: internal_network: diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh index e02a533c15..bcad3e6d85 100755 --- a/.devcontainer/init.sh +++ b/.devcontainer/init.sh @@ -2,16 +2,10 @@ set -xe -sudo chown node node_modules -sudo apt-get update -sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb -git config --global --add safe.directory /workspace +sudo chown -R node /workspace git submodule update --init -corepack install -corepack enable pnpm config set store-dir /home/node/.local/share/pnpm/store pnpm install --frozen-lockfile cp .devcontainer/devcontainer.yml .config/default.yml pnpm build pnpm migrate -pnpm exec cypress install diff --git a/.dockerignore b/.dockerignore index f204349160..1de0c7982b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,11 +7,12 @@ Dockerfile build/ built/ db/ -.devcontainer/compose.yml +docker-compose.yml node_modules/ packages/*/node_modules redis/ files/ +misskey-assets/ fluent-emojis/ .pnp.* @@ -27,4 +28,4 @@ fluent-emojis/ .idea/ packages/*/.vscode/ -packages/backend/test/compose.yml +packages/backend/test/docker-compose.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..2383856700 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: transfem-org +ko-fi: transfem diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.yml b/.github/ISSUE_TEMPLATE/01_bug-report.yml new file mode 100644 index 0000000000..f74719989f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug-report.yml @@ -0,0 +1,91 @@ +name: 🐛 Bug Report +description: Create a report to help us improve +labels: ["⚠️bug?"] + +body: + - type: markdown + attributes: + value: | + Thanks for reporting! + First, in order to avoid duplicate Issues, please search to see if the problem you found has already been reported. + Also, If you are NOT owner/admin of server, PLEASE DONT REPORT SERVER SPECIFIC ISSUES TO HERE! (e.g. feature XXX is not working in misskey.example) Please try with another misskey servers, and if your issue is only reproducible with specific server, contact your server's owner/admin first. + + - type: textarea + attributes: + label: 💡 Summary + description: Tell us what the bug is + validations: + required: true + + - type: textarea + attributes: + label: 🥰 Expected Behavior + description: Tell us what should happen + validations: + required: true + + - type: textarea + attributes: + label: 🤬 Actual Behavior + description: | + Tell us what happens instead of the expected behavior. + Please include errors from the developer console and/or server log files if you have access to them. + validations: + required: true + + - type: textarea + attributes: + label: 📝 Steps to Reproduce + placeholder: | + 1. + 2. + 3. + validations: + required: false + + - type: textarea + attributes: + label: 💻 Frontend Environment + description: | + Tell us where on the platform it happens + DO NOT WRITE "latest". Please provide the specific version. + + Examples: + * Model and OS of the device(s): MacBook Pro (14inch, 2021), macOS Ventura 13.4 + * Browser: Chrome 113.0.5672.126 + * Server URL: misskey.io + * Misskey: 13.x.x + value: | + * Model and OS of the device(s): + * Browser: + * Server URL: + * Misskey: + render: markdown + validations: + required: false + + - type: textarea + attributes: + label: 🛰 Backend Environment (for server admin) + description: | + Tell us where on the platform it happens + DO NOT WRITE "latest". Please provide the specific version. + If you are using a managed service, put that after the version. + + Examples: + * Installation Method or Hosting Service: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment + * Misskey: 13.x.x + * Node: 20.x.x + * PostgreSQL: 15.x.x + * Redis: 7.x.x + * OS and Architecture: Ubuntu 22.04.2 LTS aarch64 + value: | + * Installation Method or Hosting Service: + * Misskey: + * Node: + * PostgreSQL: + * Redis: + * OS and Architecture: + render: markdown + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/02_feature-request.yml b/.github/ISSUE_TEMPLATE/02_feature-request.yml new file mode 100644 index 0000000000..8420475b3e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_feature-request.yml @@ -0,0 +1,17 @@ +name: ✨ Feature Request +description: Suggest an idea for this project +labels: ["✨Feature"] + +body: + - type: textarea + attributes: + label: Summary + description: Tell us what the suggestion is + validations: + required: true + - type: textarea + attributes: + label: Purpose + description: Describe the specific problem or need you think this feature will solve, and who it will help. + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..6c8c6f7a49 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: 💬 Transfem.org Discord + url: https://discord.gg/xTtXc7We + about: Chat freely about Sharkey diff --git a/.github/PULL_REQUEST_TEMPLATE/01_bug.md b/.github/PULL_REQUEST_TEMPLATE/01_bug.md new file mode 100644 index 0000000000..0739fee709 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/01_bug.md @@ -0,0 +1,23 @@ + + +## What + + + +## Why + + + +## Additional info (optional) + + + +## Checklist +- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) +- [ ] Test working in a local environment +- [ ] (If needed) Update CHANGELOG.md +- [ ] (If possible) Add tests diff --git a/.github/PULL_REQUEST_TEMPLATE/02_enhance.md b/.github/PULL_REQUEST_TEMPLATE/02_enhance.md new file mode 100644 index 0000000000..0739fee709 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/02_enhance.md @@ -0,0 +1,23 @@ + + +## What + + + +## Why + + + +## Additional info (optional) + + + +## Checklist +- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) +- [ ] Test working in a local environment +- [ ] (If needed) Update CHANGELOG.md +- [ ] (If possible) Add tests diff --git a/.github/PULL_REQUEST_TEMPLATE/03_release.md b/.github/PULL_REQUEST_TEMPLATE/03_release.md new file mode 100644 index 0000000000..b5b832e1dc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/03_release.md @@ -0,0 +1,20 @@ +## Summary +This is a release PR. + +For more information on the release instructions, please see: +https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md#release + +## For reviewers +- CHANGELOGに抜け漏れは無いか +- バージョンの上げ方は適切か +- 他にこのリリースに含めなければならない変更は無いか +- 全体的な変更内容を俯瞰し問題は無いか +- レビューされていないコミットがある場合は、それが問題ないか +- 最終的な動作確認を行い問題は無いか + +などを確認し、リリースする準備が整っていると思われる場合は approve してください。 + +## Checklist +- [ ] package.jsonのバージョンが正しく更新されている +- [ ] CHANGELOGが過不足無く更新されている +- [ ] CIが全て通っている diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..5955f6b5d9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 100 + +# Add only the root, not each workspace item +# https://github.com/dependabot/dependabot-core/issues/4993#issuecomment-1289133027 +- package-ecosystem: npm + directory: "/" + schedule: + interval: daily + # PNPM has an issue with dependabot. See: + # https://github.com/dependabot/dependabot-core/issues/7258 + # https://github.com/pnpm/pnpm/issues/6530 + # TODO: Restore this when the issue is solved + open-pull-requests-limit: 0 + groups: + swc: + patterns: + - "@swc/*" + storybook: + patterns: + - "storybook*" + - "@storybook/*" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..137be487c0 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,21 @@ +'packages/backend': +- packages/backend/**/* + +'packages/backend:test': +- packages/backend/test/**/* + +'packages/frontend': +- packages/frontend/**/* + +'packages/frontend:test': +- cypress/**/* + +'packages/sw': +- packages/sw/**/* + +'packages/misskey-js': +- packages/misskey-js/**/* + +'packages/misskey-js:test': +- packages/misskey-js/test/**/* +- packages/misskey-js/test-d/**/* diff --git a/.github/misskey/test.yml b/.github/misskey/test.yml new file mode 100644 index 0000000000..7a4aa4ae6c --- /dev/null +++ b/.github/misskey/test.yml @@ -0,0 +1,15 @@ +url: 'http://misskey.local' + +# ローカルでテストするときにポートを被らないようにするためデフォルトのものとは変える(以下同じ) +port: 61812 + +db: + host: 127.0.0.1 + port: 54312 + db: test-misskey + user: postgres + pass: '' +redis: + host: 127.0.0.1 + port: 56312 +id: aidx diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..e78b82c47c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,24 @@ + + +## What + + + +## Why + + + +## Additional info (optional) + + + +## Checklist +- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md) +- [ ] Test working in a local environment +- [ ] (If needed) Add story of storybook +- [ ] (If needed) Update CHANGELOG.md +- [ ] (If possible) Add tests diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml new file mode 100644 index 0000000000..7818370c0a --- /dev/null +++ b/.github/workflows/api-misskey-js.yml @@ -0,0 +1,36 @@ +name: API report (misskey.js) + +on: [push, pull_request] + +jobs: + report: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + + - run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.node-version' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm i --frozen-lockfile + + - name: Build + run: pnpm --filter misskey-js build + + - name: Check files + run: ls packages/misskey-js/built + + - name: API report + run: pnpm --filter misskey-js api-prod + + - name: Show report + if: always() + run: cat packages/misskey-js/temp/misskey-js.api.md diff --git a/.github/workflows/check_copyright_year.yml b/.github/workflows/check_copyright_year.yml new file mode 100644 index 0000000000..03dfcd0a0b --- /dev/null +++ b/.github/workflows/check_copyright_year.yml @@ -0,0 +1,18 @@ +name: Check copyright year + +on: + push: + branches: + - master + - develop + +jobs: + check_copyright_year: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + - run: | + if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then + echo "Please change copyright year!" + exit 1 + fi diff --git a/.github/workflows/clear-untagged-packages.yml b/.github/workflows/clear-untagged-packages.yml new file mode 100644 index 0000000000..5f94d54bcb --- /dev/null +++ b/.github/workflows/clear-untagged-packages.yml @@ -0,0 +1,18 @@ +name: Remove old package versions + +on: + workflow_dispatch: + +jobs: + remove-package-versions: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/delete-package-versions@v4 + with: + package-name: sharkey + package-type: container + min-versions-to-keep: 10 + delete-only-untagged-versions: 'true' diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml new file mode 100644 index 0000000000..e5e8392d78 --- /dev/null +++ b/.github/workflows/docker-develop.yml @@ -0,0 +1,66 @@ +name: Publish Docker image (develop) + +on: + push: + branches: + - develop + paths: + - packages/** + - locales/** + workflow_dispatch: + +env: + REGISTRY: ghcr.io + +jobs: + push_to_registry: + name: Push Docker image to GHCR + runs-on: ubuntu-latest + if: github.repository == 'transfem-org/Sharkey' + permissions: + contents: read + packages: write + steps: + - name: Remove unnecessary files + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + - name: Check out the repo + uses: actions/checkout@v4.1.1 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3.0.0 + with: + platforms: linux/amd64,linux/arm64 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/transfem-org/sharkey + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and Push to GHCR + id: build + uses: docker/build-push-action@v5 + with: + builder: ${{ steps.buildx.outputs.name }} + context: . + push: true + platforms: ${{ steps.buildx.outputs.platforms }} + provenance: false + tags: ${{ env.REGISTRY }}/transfem-org/sharkey:develop + labels: develop + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: NODE_ENV=development + - name: Push update to server + if: steps.build.outcome == 'success' + uses: indiesdev/curl@v1.1 + with: + url: ${{ secrets.AUTO_UPDATE_DEV_URL }} + method: POST + timeout: 600000 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..8eabaa2882 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,64 @@ +name: Publish Docker image + +on: + push: + branches: + - stable + paths: + - packages/** + - locales/** + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + +jobs: + push_to_registry: + name: Push Docker image to GHCR + runs-on: ubuntu-latest + if: github.repository == 'transfem-org/Sharkey' + permissions: + contents: read + packages: write + + steps: + - name: Check out the repo + uses: actions/checkout@v4.1.1 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3.0.0 + with: + platforms: linux/amd64,linux/arm64 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/transfem-org/sharkey + tags: | + type=edge + type=ref,event=pr + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and Push to GHCR + id: build + uses: docker/build-push-action@v5 + with: + builder: ${{ steps.buildx.outputs.name }} + context: . + push: true + platforms: ${{ steps.buildx.outputs.platforms }} + provenance: false + tags: ${{ env.REGISTRY }}/transfem-org/sharkey:stable + labels: stable + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml new file mode 100644 index 0000000000..edb18b04da --- /dev/null +++ b/.github/workflows/dockle.yml @@ -0,0 +1,30 @@ +--- +name: Dockle + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + dockle: + runs-on: ubuntu-latest + env: + DOCKER_CONTENT_TRUST: 1 + steps: + - uses: actions/checkout@v4.1.1 + - run: | + curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb" + sudo dpkg -i dockle.deb + - run: | + cp .config/docker_example.env .config/docker.env + cp ./docker-compose.yml.example ./docker-compose.yml + - run: | + docker compose up -d web + docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest + - run: | + cmd="dockle --exit-code 1 misskey-web:latest ${image_name}" + echo "> ${cmd}" + eval "${cmd}" diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml new file mode 100644 index 0000000000..8e3437ad86 --- /dev/null +++ b/.github/workflows/get-api-diff.yml @@ -0,0 +1,186 @@ +# this name is used in report-api-diff.yml so be careful when change name +name: Get api.json from Misskey + +on: + pull_request: + branches: + - master + - develop + +jobs: + get-base: + runs-on: ubuntu-latest + permissions: + contents: read + + strategy: + matrix: + node-version: [20.5.1] + + services: + db: + image: postgres:13 + ports: + - 5432:5432 + env: + POSTGRES_DB: misskey + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_USER: example-misskey-user + POSTGRESS_PASS: example-misskey-pass + redis: + image: redis:7 + ports: + - 6379:6379 + + steps: + - uses: actions/checkout@v4.1.1 + with: + repository: ${{ github.event.pull_request.base.repo.full_name }} + ref: ${{ github.base_ref }} + submodules: true + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4.0.0 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - name: Check pnpm-lock.yaml + run: git diff --exit-code pnpm-lock.yaml + - name: Copy Configure + run: cp .config/example.yml .config/default.yml + - name: Build + run: pnpm build + - name : Migrate + run: pnpm migrate + - name: Launch misskey + run: | + screen -S misskey -dm pnpm run dev + sleep 30s + - name: Wait for Misskey to be ready + run: | + MAX_RETRIES=12 + RETRY_DELAY=5 + count=0 + until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do + printf '.' + sleep $RETRY_DELAY + count=$((count + 1)) + done + + if [[ $count -eq $MAX_RETRIES ]]; then + echo "Failed to connect to Misskey after $MAX_RETRIES attempts." + exit 1 + fi + - id: fetch + name: Get api.json from Misskey + run: | + RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json) + echo $RESULT > api-base.json + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: api-artifact + path: api-base.json + - name: Kill Misskey Job + run: screen -S misskey -X quit + + get-head: + runs-on: ubuntu-latest + permissions: + contents: read + + strategy: + matrix: + node-version: [20.5.1] + + services: + db: + image: postgres:13 + ports: + - 5432:5432 + env: + POSTGRES_DB: misskey + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_USER: example-misskey-user + POSTGRESS_PASS: example-misskey-pass + redis: + image: redis:7 + ports: + - 6379:6379 + + steps: + - uses: actions/checkout@v4.1.1 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.head_ref }} + submodules: true + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4.0.0 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - name: Check pnpm-lock.yaml + run: git diff --exit-code pnpm-lock.yaml + - name: Copy Configure + run: cp .config/example.yml .config/default.yml + - name: Build + run: pnpm build + - name : Migrate + run: pnpm migrate + - name: Launch misskey + run: | + screen -S misskey -dm pnpm run dev + sleep 30s + - name: Wait for Misskey to be ready + run: | + MAX_RETRIES=12 + RETRY_DELAY=5 + count=0 + until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do + printf '.' + sleep $RETRY_DELAY + count=$((count + 1)) + done + + if [[ $count -eq $MAX_RETRIES ]]; then + echo "Failed to connect to Misskey after $MAX_RETRIES attempts." + exit 1 + fi + - id: fetch + name: Get api.json from Misskey + run: | + RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json) + echo $RESULT > api-head.json + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: api-artifact + path: api-head.json + - name: Kill Misskey Job + run: screen -S misskey -X quit + + save-pr-number: + runs-on: ubuntu-latest + steps: + - name: Save PR number + env: + PR_NUMBER: ${{ github.event.number }} + run: | + echo "$PR_NUMBER" > ./pr_number + - uses: actions/upload-artifact@v3 + with: + name: api-artifact + path: pr_number diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000000..fa4a58c3a9 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,16 @@ +name: "Pull Request Labeler" +on: + pull_request_target: + branches-ignore: + - 'l10n_develop' + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000000..c63ccc87fc --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,85 @@ +name: Lint + +on: + push: + branches: + - master + - develop + paths: + - packages/** + pull_request: + branches-ignore: + - weblate + +jobs: + pnpm_install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + submodules: true + - uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + - uses: actions/setup-node@v4 + with: + node-version-file: '.node-version' + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + + lint: + needs: [pnpm_install] + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + workspace: + - backend + - frontend + - sw + - misskey-js + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + submodules: true + - uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - uses: actions/setup-node@v4 + with: + node-version-file: '.node-version' + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - run: pnpm --filter ${{ matrix.workspace }} run eslint + + typecheck: + needs: [pnpm_install] + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + workspace: + - backend + - misskey-js + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 0 + submodules: true + - uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - uses: actions/setup-node@v4 + with: + node-version-file: '.node-version' + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - run: pnpm --filter ${{ matrix.workspace }} run typecheck diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml new file mode 100644 index 0000000000..c02b980e4d --- /dev/null +++ b/.github/workflows/ok-to-test.yml @@ -0,0 +1,36 @@ +# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event +name: Ok To Test + +on: + issue_comment: + types: [created] + +jobs: + ok-to-test: + runs-on: ubuntu-latest + # Only run for PRs, not issue comments + if: ${{ github.event.issue.pull_request }} + steps: + # Generate a GitHub App installation access token from an App ID and private key + # To create a new GitHub App: + # https://developer.github.com/apps/building-github-apps/creating-a-github-app/ + # See app.yml for an example app manifest + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.DEPLOYBOT_APP_ID }} + private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }} + + - name: Slash Command Dispatch + uses: peter-evans/slash-command-dispatch@v3 + env: + TOKEN: ${{ steps.generate_token.outputs.token }} + with: + token: ${{ env.TOKEN }} # GitHub App installation access token + # token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work + reaction-token: ${{ secrets.GITHUB_TOKEN }} + issue-type: pull-request + commands: deploy + named-args: true + permission: write diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000000..d10d0fcafd --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,74 @@ +name: Publish prebuild + +on: + push: + branches: + - stable + release: + types: [published] + workflow_dispatch: + +jobs: + build_binaries: + name: Build & ship binaries + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] + python-version: [3.11.x] + if: github.repository == 'transfem-org/Sharkey' + permissions: + contents: read + packages: write + + steps: + - name: Check out the repo + uses: actions/checkout@v4.1.1 + with: + lfs: true + submodules: 'recursive' + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Setup Python + uses: actions/setup-python@v4.7.1 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache APT Packages + uses: awalsh128/cache-apt-pkgs-action@v1.3.1 + with: + packages: "build-essential binfmt-support qemu-user-static ffmpeg tini curl libjemalloc-dev libjemalloc2 uuid-dev libx11-dev libxkbfile-dev execstack libgconf-2-4 libsecret-1-dev" + + - name: Set pnpm store path + run: echo "PNPM_STORE_PATH=$(pnpm store path)" >> $GITHUB_ENV + + - name: Cache node modules + uses: actions/cache@v3 + with: + path: ${{ env.PNPM_STORE_PATH }} + key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + pnpm-${{ runner.os }}- + + - name: Build + run: | + corepack enable + corepack prepare pnpm@latest --activate + CI=true pnpm install + CI=true pnpm run build + rm -rdf packages/backend/node_modules + rm -rdf packages/frontend/node_modules + rm -rdf packages/megalodon/node_modules + rm -rdf packages/misskey-js/node_modules + rm -rdf node_modules + CI=true pnpm --prod --no-optional install + tar -czf /tmp/workspace.tar.gz . + - name: Upload linux x64 + uses: actions/upload-artifact@v3.1.3 + with: + name: sharkey-linux-x64 + path: /tmp/workspace.tar.gz diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml new file mode 100644 index 0000000000..cb9a4ebfc8 --- /dev/null +++ b/.github/workflows/pr-preview-deploy.yml @@ -0,0 +1,92 @@ +# Run secret-dependent integration tests only after /deploy approval +on: + repository_dispatch: + types: [deploy-command] + +name: Deploy preview environment + +jobs: + # Repo owner has commented /deploy on a (fork-based) pull request + deploy-preview-environment: + runs-on: ubuntu-latest + if: + github.event.client_payload.slash_command.sha != '' && + contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha) + steps: + - uses: actions/github-script@v7 + id: check-id + env: + number: ${{ github.event.client_payload.pull_request.number }} + job: ${{ github.job }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string + script: | + const { data: pull } = await github.rest.pulls.get({ + ...context.repo, + pull_number: process.env.number + }); + const ref = pull.head.sha; + + const { data: checks } = await github.rest.checks.listForRef({ + ...context.repo, + ref + }); + + const check = checks.check_runs.filter(c => c.name === process.env.job); + + return check[0].id; + + - uses: actions/github-script@v7 + env: + check_id: ${{ steps.check-id.outputs.result }} + details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.checks.update({ + ...context.repo, + check_run_id: process.env.check_id, + status: 'in_progress', + details_url: process.env.details_url + }); + + # Check out merge commit + - name: Fork based /deploy checkout + uses: actions/checkout@v4.1.1 + with: + ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' + + # + - name: Context + uses: okteto/context@latest + with: + token: ${{ secrets.OKTETO_TOKEN }} + + - name: Deploy preview environment + uses: ikuradon/deploy-preview@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo + timeout: 15m + + # Update check run called "integration-fork" + - uses: actions/github-script@v7 + id: update-check-run + if: ${{ always() }} + env: + # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run + conclusion: ${{ job.status }} + check_id: ${{ steps.check-id.outputs.result }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: result } = await github.rest.checks.update({ + ...context.repo, + check_run_id: process.env.check_id, + status: 'completed', + conclusion: process.env.conclusion + }); + + return result; diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml new file mode 100644 index 0000000000..47d9eb313a --- /dev/null +++ b/.github/workflows/pr-preview-destroy.yml @@ -0,0 +1,54 @@ +# file: .github/workflows/preview-closed.yaml +on: + pull_request: + types: + - closed + +name: Destroy preview environment + +jobs: + destroy-preview-environment: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + id: check-conclusion + env: + number: ${{ github.event.number }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string + script: | + const { data: pull } = await github.rest.pulls.get({ + ...context.repo, + pull_number: process.env.number + }); + const ref = pull.head.sha; + + const { data: checks } = await github.rest.checks.listForRef({ + ...context.repo, + ref + }); + + const check = checks.check_runs.filter(c => c.name === 'deploy-preview-environment'); + + if (check.length === 0) { + return; + } + + const { data: result } = await github.rest.checks.get({ + ...context.repo, + check_run_id: check[0].id, + }); + + return result.conclusion; + - name: Context + if: steps.check-conclusion.outputs.result == 'success' + uses: okteto/context@latest + with: + token: ${{ secrets.OKTETO_TOKEN }} + + - name: Destroy preview environment + if: steps.check-conclusion.outputs.result == 'success' + uses: okteto/destroy-preview@latest + with: + name: pr-${{ github.event.number }}-syuilo diff --git a/.github/workflows/report-api-diff.yml b/.github/workflows/report-api-diff.yml new file mode 100644 index 0000000000..2868d6cc09 --- /dev/null +++ b/.github/workflows/report-api-diff.yml @@ -0,0 +1,85 @@ +name: Report API Diff + +on: + workflow_run: + types: [completed] + workflows: + - Get api.json from Misskey # get-api-diff.yml + +jobs: + compare-diff: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + permissions: + pull-requests: write + +# api-artifact + steps: + - name: Download artifact + uses: actions/github-script@v7 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "api-artifact" + })[0]; + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + let fs = require('fs'); + fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/api-artifact.zip`, Buffer.from(download.data)); + - name: Extract artifact + run: unzip api-artifact.zip -d artifacts + - name: Load PR Number + id: load-pr-num + run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT" + + - name: Output base + run: cat ./artifacts/api-base.json + - name: Output head + run: cat ./artifacts/api-head.json + - name: Arrange json files + run: | + jq '.' ./artifacts/api-base.json > ./api-base.json + jq '.' ./artifacts/api-head.json > ./api-head.json + - name: Get diff of 2 files + run: diff -u --label=base --label=head ./api-base.json ./api-head.json | cat > api.json.diff + - name: Get full diff + run: diff --label=base --label=head --new-line-format='+%L' --old-line-format='-%L' --unchanged-line-format=' %L' ./api-base.json ./api-head.json | cat > api-full.json.diff + - name: Echo full diff + run: cat ./api-full.json.diff + - name: Upload full diff to Artifact + uses: actions/upload-artifact@v3 + with: + name: api-artifact + path: | + api-full.json.diff + api-base.json + api-head.json + - id: out-diff + name: Build diff Comment + run: | + cat <<- EOF > ./output.md + このPRによるapi.jsonの差分 +
+ 差分はこちら + + \`\`\`diff + $(cat ./api.json.diff) + \`\`\` +
+ + [Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) + EOF + - uses: thollander/actions-comment-pull-request@v2 + with: + pr_number: ${{ steps.load-pr-num.outputs.pr-number }} + comment_tag: show_diff + filePath: ./output.md diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml new file mode 100644 index 0000000000..711e8bb7f5 --- /dev/null +++ b/.github/workflows/test-backend.yml @@ -0,0 +1,59 @@ +name: Test (backend) + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + jest: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.5.1] + + services: + postgres: + image: postgres:13 + ports: + - 54312:5432 + env: + POSTGRES_DB: test-misskey + POSTGRES_HOST_AUTH_METHOD: trust + redis: + image: redis:7 + ports: + - 56312:6379 + + steps: + - uses: actions/checkout@v4.1.1 + with: + submodules: true + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - name: Check pnpm-lock.yaml + run: git diff --exit-code pnpm-lock.yaml + - name: Copy Configure + run: cp .github/misskey/test.yml .config + - name: Build + run: pnpm build + - name: Test + run: pnpm jest-and-coverage + - name: Upload Coverage + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./packages/backend/coverage/coverage-final.json diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml new file mode 100644 index 0000000000..62b2fe3e38 --- /dev/null +++ b/.github/workflows/test-frontend.yml @@ -0,0 +1,120 @@ +name: Test (frontend) + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + vitest: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.5.1] + + steps: + - uses: actions/checkout@v4.1.1 + with: + submodules: true + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - name: Check pnpm-lock.yaml + run: git diff --exit-code pnpm-lock.yaml + - name: Copy Configure + run: cp .github/misskey/test.yml .config + - name: Build + run: pnpm build + - name: Test + run: pnpm --filter frontend test-and-coverage + - name: Upload Coverage + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./packages/frontend/coverage/coverage-final.json + + e2e: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node-version: [20.5.1] + browser: [chrome] + + services: + postgres: + image: postgres:13 + ports: + - 54312:5432 + env: + POSTGRES_DB: test-misskey + POSTGRES_HOST_AUTH_METHOD: trust + redis: + image: redis:7 + ports: + - 56312:6379 + + steps: + - uses: actions/checkout@v4.1.1 + with: + submodules: true + # https://github.com/cypress-io/cypress-docker-images/issues/150 + #- name: Install mplayer for FireFox + # run: sudo apt install mplayer -y + # if: ${{ matrix.browser == 'firefox' }} + #- uses: browser-actions/setup-firefox@latest + # if: ${{ matrix.browser == 'firefox' }} + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 7 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - name: Copy Configure + run: cp .github/misskey/test.yml .config + - name: Build + run: pnpm build + # https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091 + - name: ALSA Env + run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc + # XXX: This tries reinstalling Cypress if the binary is not cached + # Remove this when the cache issue is fixed + - name: Cypress install + run: pnpm exec cypress install + - name: Cypress run + uses: cypress-io/github-action@v6 + with: + install: false + start: pnpm start:test + wait-on: 'http://localhost:61812' + headed: true + browser: ${{ matrix.browser }} + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: ${{ matrix.browser }}-cypress-screenshots + path: cypress/screenshots + - uses: actions/upload-artifact@v3 + if: always() + with: + name: ${{ matrix.browser }}-cypress-videos + path: cypress/videos diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml new file mode 100644 index 0000000000..36816c78b8 --- /dev/null +++ b/.github/workflows/test-misskey-js.yml @@ -0,0 +1,52 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Test (misskey.js) + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + +jobs: + test: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.5.1] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + + - run: corepack enable + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - name: Install dependencies + run: pnpm i --frozen-lockfile + + - name: Check pnpm-lock.yaml + run: git diff --exit-code pnpm-lock.yaml + + - name: Build + run: pnpm --filter misskey-js build + + - name: Test + run: pnpm --filter misskey-js test + env: + CI: true + + - name: Upload Coverage + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./packages/misskey-js/coverage/coverage-final.json diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml new file mode 100644 index 0000000000..6b98fc51e6 --- /dev/null +++ b/.github/workflows/test-production.yml @@ -0,0 +1,42 @@ +name: Test (production install and build) + +on: + push: + branches: + - master + - develop + pull_request: + +env: + NODE_ENV: production + +jobs: + production: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.5.1] + + steps: + - uses: actions/checkout@v4.1.1 + with: + submodules: true + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - run: corepack enable + - run: pnpm i --frozen-lockfile + - name: Check pnpm-lock.yaml + run: git diff --exit-code pnpm-lock.yaml + - name: Copy Configure + run: cp .github/misskey/test.yml .config/default.yml + - name: Build + run: pnpm build diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml new file mode 100644 index 0000000000..fb6c9ebdff --- /dev/null +++ b/.github/workflows/welcome.yml @@ -0,0 +1,25 @@ +name: Welcome +on: + pull_request: + types: [opened] + issues: + types: [opened] +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: actions/first-interaction@v1.3.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: | + 👋 @{{ author }} + Thanks for opening your first issue here! If you are reporting a bug, please make sure to include steps on how to reproduce it! :D + + pr-message: | + 👋 @{{ author }} + Thanks for opening this pull request! We will review it as soon as we can :3 + Please check out our contributing guidelines in the meantime. + + # FIRST_PR_MERGED: | + # 🎉 @{{ author }} + # Congrats on getting your first pull request merged! We are proud of you :3 ❤️ diff --git a/.gitignore b/.gitignore index 7cc7354a4a..11e69b2621 100644 --- a/.gitignore +++ b/.gitignore @@ -23,9 +23,6 @@ packages/sw/.yarn/cache # pnpm .pnpm-store -# eslint -**/.eslintcache - # Cypress cypress/screenshots cypress/videos @@ -38,17 +35,12 @@ coverage !/.config/example.yml !/.config/docker_example.yml !/.config/docker_example.env -!/.config/cypress-devcontainer.yml docker-compose.yml -compose.yml -.devcontainer/compose.yml -!/.devcontainer/compose.yml +!/.devcontainer/docker-compose.yml # misskey /build built -built-test -js-built /data /.cache-loader /db @@ -65,12 +57,9 @@ api-docs.json ormconfig.json temp /packages/frontend/src/**/*.stories.ts -tsdoc-metadata.json -misskey-assets -# Vite temporary files -vite.config.js.timestamp-* -vite.config.ts.timestamp-* +# Sharkey +/packages/megalodon/lib # blender backups *.blend1 @@ -81,6 +70,3 @@ vite.config.ts.timestamp-* # VSCode addon .favorites.json - -# Sharkey -/packages/megalodon/lib diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 4db8bda32e..0000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,114 +0,0 @@ -stages: - - test - - deploy - -testCommit: - stage: test - image: node:iron - services: - - postgres:15 - - redis - variables: - POSTGRES_PASSWORD: ci - script: - - apt-get update && apt-get install -y git wget curl build-essential python3 ffmpeg - - cp .config/ci.yml .config/default.yml - - cp .config/ci.yml .config/test.yml - - corepack enable - - corepack prepare pnpm@latest --activate - - git submodule update --init - - pnpm install --frozen-lockfile - - pnpm run build - - pnpm run migrate - - pnpm run --filter='!megalodon' test - - pnpm run --filter=backend --filter=misskey-js lint - - pnpm run --filter=frontend --filter=frontend-embed eslint - cache: - key: test - policy: pull-push - when: on_success - paths: - - node_modules/ - - packages/*/node_modules/ - only: - - develop - - merge_requests - - stable - -getImageTag: - stage: deploy - image: ubuntu:latest - script: - - apt-get update && apt-get install -y jq - - | - if test -n "$CI_COMMIT_TAG"; then - tag="$CI_COMMIT_TAG" - elif test "$CI_COMMIT_BRANCH" == "stable"; then - tag="latest" - elif test "$CI_COMMIT_BRANCH" == "develop"; then - tag="develop" - else - tag="$CI_COMMIT_BRANCH" - fi - version=$(cat package.json | jq -r '.version') - - echo "REGISTRY_PUSH_TAG=$tag" >> build.env - - echo "REGISTRY_PUSH_VERSION=$version" >> build.env - artifacts: - reports: - dotenv: build.env - only: - - stable - - develop - - tags - -buildDocker: - stage: deploy - needs: - - job: getImageTag - artifacts: true - parallel: - matrix: - - ARCH: amd64 - - ARCH: arm64 - tags: - - ${ARCH} - image: - name: gcr.io/kaniko-project/executor:debug - entrypoint: [""] - script: - - >- - /kaniko/executor - --context "${CI_PROJECT_DIR}" - --dockerfile "${CI_PROJECT_DIR}/Dockerfile" - --destination "${CI_REGISTRY_IMAGE}:${REGISTRY_PUSH_VERSION}-${ARCH}" - only: - - stable - - develop - - tags - -mergeManifests: - stage: deploy - needs: - - job: buildDocker - artifacts: false - - job: getImageTag - artifacts: true - tags: - - docker - image: - name: mplatform/manifest-tool:alpine - entrypoint: [""] - script: - - >- - manifest-tool - --username=${CI_REGISTRY_USER} - --password=${CI_REGISTRY_PASSWORD} - push from-args - --platforms linux/amd64,linux/arm64 - --tags ${REGISTRY_PUSH_VERSION} - --template ${CI_REGISTRY_IMAGE}:${REGISTRY_PUSH_VERSION}-ARCH - --target ${CI_REGISTRY_IMAGE}:${REGISTRY_PUSH_TAG} - only: - - stable - - develop - - tags diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md deleted file mode 100644 index a909067269..0000000000 --- a/.gitlab/issue_templates/bug.md +++ /dev/null @@ -1,35 +0,0 @@ - - -# **What happened?** - - -# **What did you expect to happen?** - - -# **Version** - - -# **Instance** - - -# **What type of issue is this?** - - -# **What browser are you using? (Client-side issues only)** - -# **What operating system are you using? (Client-side issues only)** - -# **How do you deploy Sharkey on your server? (Server-side issues only)** - -# **What operating system are you using? (Server-side issues only)** - -# **Relevant log output** - - -# **Contribution Guidelines** -By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) -- [ ] I agree to follow this project's Contribution Guidelines -- [ ] I have searched the issue tracker for similar issues, and this is not a duplicate. diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md deleted file mode 100644 index a77f9335fe..0000000000 --- a/.gitlab/issue_templates/feature.md +++ /dev/null @@ -1,21 +0,0 @@ - - -# **What feature would you like implemented?** - - -# **Why should we add this feature?** - - -# **Version** - - -# **Instance** - - -# **Contribution Guidelines** -By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) -- [ ] I agree to follow this project's Contribution Guidelines -- [ ] I have searched the issue tracker for similar requests, and this is not a duplicate. diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md deleted file mode 100644 index e6977def70..0000000000 --- a/.gitlab/merge_request_templates/default.md +++ /dev/null @@ -1,12 +0,0 @@ - - -# **What does this MR do?** - - -# **Contribution Guidelines** -By submitting this merge request, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) -- [ ] I agree to follow this project's Contribution Guidelines -- [ ] I have made sure to test this merge request - - - diff --git a/.gitmodules b/.gitmodules index 1a68b48180..225a69a652 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ +[submodule "misskey-assets"] + path = misskey-assets + url = https://github.com/misskey-dev/assets.git [submodule "fluent-emojis"] path = fluent-emojis url = https://github.com/misskey-dev/emojis.git -[submodule "tossface-emojis"] - path = tossface-emojis - url = https://activitypub.software/TransFem-org/tossface-emojis.git diff --git a/.node-version b/.node-version index 8ce7030825..7cc2069986 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.16.0 +20.5.1 diff --git a/.npmrc b/.npmrc deleted file mode 100644 index b949431c77..0000000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -@transfem-org:registry=https://activitypub.software/api/v4/packages/npm/ -engine-strict = true diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 3cdf81e339..baca8db246 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,7 +3,9 @@ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", "Vue.volar", + "Vue.vscode-typescript-vue-plugin", "Orta.vscode-jest", + "dbaeumer.vscode-eslint", "mrmlnc.vscode-json5" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ceec23acd..71fb02a59d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,11 @@ { - "search.exclude": { - "**/node_modules": true - }, - "typescript.tsdk": "node_modules/typescript/lib", - "files.associations": { - "*.test.ts": "typescript" - }, - "jest.jestCommandLine": "pnpm run jest", - "jest.runMode": "on-demand", - "editor.codeActionsOnSave": { - "source.fixAll": "explicit" - }, - "editor.formatOnSave": false -} + "search.exclude": { + "**/node_modules": true + }, + "typescript.tsdk": "node_modules/typescript/lib", + "files.associations": { + "*.test.ts": "typescript" + }, + "jest.jestCommandLine": "pnpm run jest", + "jest.autoRun": "off" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cf0437e51a..92e02508fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,609 +1,19 @@ -## 2024.9.0 - -### General -- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能 - - 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください -- Feat: パスキーでログインボタンを実装 (#14574) -- Feat: フォローされた際のメッセージを設定できるように -- Feat: 連合をホワイトリスト制にできるように -- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445) -- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680) -- Feat: データエクスポートが完了した際に通知を発行するように -- Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように -- Enhance: 依存関係の更新 -- Enhance: l10nの更新 - -### Client -- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように -- Enhance: アイコンデコレーション管理画面にプレビューを追加 -- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく -- Enhance: ScratchpadにUIインスペクターを追加 -- Enhance: Play編集画面の項目の並びを少しリデザイン -- Enhance: 各種メニューをドロワー表示するかどうか設定可能に -- Enhance: AiScriptのMk:C:containerのオプションに`borderStyle`と`borderRadius`を追加 -- Enhance: CWでも絵文字をクリックしてメニューを表示できるように -- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正 -- Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正 -- Fix: 月の違う同じ日はセパレータが表示されないのを修正 -- Fix: タッチ画面でレンジスライダーを操作するとツールチップが複数表示される問題を修正 - (Cherry-picked from https://github.com/taiyme/misskey/pull/265) -- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725) -- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正 -- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/bde6bb0bd2e8b0d027e724d2acdb8ae0585a8110) -- Fix: 一部画面のページネーションが動作しにくくなっていたのを修正 ( #12766 , #11449 ) - -### Server -- Feat: Misskey® Reactions Boost Technology™ (RBT)により、リアクションの作成負荷を低減することが可能に -- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように - - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます -- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 -- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8) -- Fix: Continue importing from file if single emoji import fails -- Fix: `Retry-After`ヘッダーが送信されなかった問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/8a982c61c01909e7540ff1be9f019df07c3f0624) -- Fix: サーバーサイドのDOM解析完了時にリソースを開放するように - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/634) -- Fix: ``を追って照会するのはOKレスポンスが返却された場合のみに - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/633) -- Fix: メールにスタイルが適用されていなかった問題を修正 - -## 2024.8.0 - -### General -- Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように -- Enhance: アカウントの削除のモデレーションログを残すように -- Enhance: 不適切なページ、ギャラリー、Playを管理者権限で削除できるように -- Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正 - -### Client -- Enhance: 「自分のPlay」ページにおいてPlayが非公開かどうかが一目でわかるように -- Enhance: 不適切なページ、ギャラリー、Playを通報できるように -- Fix: Play編集時に公開範囲が「パブリック」にリセットされる問題を修正 -- Fix: ページ遷移に失敗することがある問題を修正 -- Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制 -- Fix: mCaptchaを使用していてもbotプロテクションに関する警告が消えないのを修正 -- Fix: ユーザーのモデレーションページにおいてユーザー名にドットが入っているとシステムアカウントとして表示されてしまう問題を修正 -- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正 - -### Server -- Enhance: 照会時にURLがhtmlかつheadタグ内に`rel="alternate"`, `type="application/activity+json"`の`link`タグがある場合に追ってリンク先を照会できるように -- Enhance: 凍結されたアカウントのフォローリクエストを表示しないように -- Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374 - - 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。 - - これにより、プッシュ通知が有効な同条件下の環境において、プッシュ通知が常に発生してしまう問題も修正されます。 -- Fix: Play各種エンドポイントの返り値に`visibility`が含まれていない問題を修正 -- Fix: サーバー情報取得の際にモデレーター限定の情報が取得できないことがあるのを修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/582) -- Fix: 公開範囲がダイレクトのノートをユーザーアクティビティのチャート生成に使用しないように - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/679) -- Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように - - キュー処理のつまりが改善される可能性があります -- Fix: リバーシの対局設定の変更が反映されないのを修正 -- Fix: 無制限にストリーミングのチャンネルに接続できる問題を修正 -- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700) -- Fix: Prevent memory leak from memory caches (#14310) -- Fix: More reliable memory cache eviction (#14311) - -## 2024.7.0 - -### Note -- デッキUIの新着ノートをサウンドで通知する機能の追加(v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。 -- Streaming APIにて入力が不正な場合にはそのメッセージを無視するようになりました。 #14251 - -### General -- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705 -- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に - - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます -- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281 -- Feat: メディアサイレンスを実装 #13842 - - メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。 -- Enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように -- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正 -- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題 -- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正 -- 翻訳の更新 -- 依存関係の更新 - -### Client -- Feat: ユーザーページから「このユーザーのノートを検索」できるように (#14128) -- Feat: 検索ページはクエリを受け付けるようになりました (#14128) -- Enhance: 検索ページのUI改善 (#14128) -- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善 -- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加 -- Enhance: 非ログイン時のハイライトTLのデザインを改善 -- Enhance: フロントエンドのアクセシビリティ改善 - (Based on https://github.com/taiyme/misskey/pull/226) -- Enhance: サーバー情報ページ・お問い合わせページを改善 - (Cherry-picked from https://github.com/taiyme/misskey/pull/238) -- Enhance: AiScriptを0.19.0にアップデート -- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`) -- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように -- Enhance: 検索(ノート/ユーザー)で `#` から始まる文字列を入力すると、そのハッシュタグのノート/ユーザー一覧ページが表示できるように -- Enhance: 検索(ノート/ユーザー)において、入力に空白が含まれている場合は照会を行わないように -- Enhance: 検索(ノート/ユーザー)において、照会を行うかどうか、ハッシュタグのノート/ユーザー一覧ページを表示するかどうかの確認ダイアログを出すように -- Enhance: 検索(ノート/ユーザー)で `@` から始まる文字列(`@user@host`など)を入力すると、そのユーザーを照会できるように -- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように - (Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99) -- Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように -- Enhance: ブラウザのコンテキストメニューを使用できるように -- Enhance: 連合の「連合中」,「購読中」,「配信中」に対してブロックしているサーバー、配信停止しているサーバーを含めないように -- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正 -- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) -- Fix: リバーシの対局を正しく共有できないことがある問題を修正 -- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正 -- Fix: アンテナの編集画面のボタンに隙間を追加 -- Fix: テーマプレビューが見れない問題を修正 -- Fix: ショートカットキーが連打できる問題を修正 - (Cherry-picked from https://github.com/taiyme/misskey/pull/234) -- Fix: MkSignin.vueのcredentialRequestからReactivityを削除(ProxyがPasskey認証処理に渡ることを避けるため) -- Fix: 「アニメーション画像を再生しない」がオンのときでもサーバーのバナー画像・背景画像がアニメーションしてしまう問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/574) -- Fix: Twitchの埋め込みが開けない問題を修正 -- Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正 -- Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正 -- Fix: 一部の画像がセンシティブ指定されているときに画面に何も表示されないことがあるのを修正 -- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/672) -- Fix: `/share`ページにおいて絵文字ピッカーを開くことができない問題を修正 -- Fix: deck uiの通知音が重なる問題 (#14029) -- Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正 -- Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正 -- Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正 -- Fix: 照会に `#` から始まる文字列を入力してそのハッシュタグのページを表示する際、入力が `#` のみの場合に「指定されたURLに該当するページはありませんでした。」が表示されてしまう問題を修正 -- Fix: 照会に `@` から始まる文字列を入力してユーザーを照会する際、入力が `@` のみの場合に「問題が発生しました」が表示されてしまう問題を修正 -- Fix: 投稿フォームにノートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正 -- Fix: フォロー中のユーザーに関する"TLに他の人への返信を含める"の設定が分かりづらい問題を修正 -- Fix: タイムラインページを開いた時、`TLに他の人への返信を含める`がオフのときに`ファイル付きのみ`をオンにできない問題を修正 -- Fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されず、`TLに他の人への返信を含める`のトグルが表示されない問題を修正 -- Fix: ウィジェットのタイムライン選択欄に無効化されたタイムラインが表示される問題を修正 -- Fix: サウンドにドライブの音声を使用している際にドライブの音声が再生できなくなると設定が変更できなくなる問題を修正 - -### Server -- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) -- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに -- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに -- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに -- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに -- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに -- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに -- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように -- Enhance: エンドポイント`api/meta`にプロパティ`noteSearchableScope`が増え、`string`値`local`または`global`を返却します -- Fix: チャート生成時にinstance.suspensionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 -- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006) -- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) -- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) -- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正 -- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正 -- Fix: 空文字列のリアクションはフォールバックされるように -- Fix: リノートにリアクションできないように -- Fix: ユーザー名の前後に空白文字列がある場合は省略するように -- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正 -- Fix: ユーザ名のサジェスト時に表示される内容と順番を調整(以下の順番になります) #14149 - 1. フォロー中かつアクティブなユーザ - 2. フォロー中かつ非アクティブなユーザ - 3. フォローしていないアクティブなユーザ - 4. フォローしていない非アクティブなユーザ - - また、自分自身のアカウントもサジェストされるようになりました。 -- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652) -- Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正 -- Fix: FTT有効時にリモートユーザーのノートがHTLにキャッシュされる問題を修正 -- Fix: 一部の通知がローカル上のリモートユーザーに対して行われていた問題を修正 -- Fix: エラーメッセージの誤字を修正 (#14213) -- Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正 -- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正 - (Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1) -- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251 -- Fix: `users/search`において `@` から始まる文字列が与えられた際の処理が正しくなかった問題を修正 - - 名前や自己紹介に `@` から始まる文言が含まれるユーザーも検索できるようになります -- Fix: 一部のMisskey以外のソフトウェアからファイルを受け取れない問題 - (Cherry-picked from https://github.com/Secineralyr/misskey.dream/pull/73/commits/652eaff1e8aa00b890d71d2e1e52c263c1e67c76) - - NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます - Migrationではカラム定義の変更のみが行われます。 - サーバー管理者は各サーバーの必要に応じ`drive_file` `("uri")`に対するインデックスを張りなおすことでより安定しDBの探索が行われる可能性があります。詳細 は [GitHub](https://github.com/misskey-dev/misskey/pull/14323#issuecomment-2257562228)で確認可能です -- Fix: 自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正 -- Fix: フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題を修正 -- Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように - - キュー処理のつまりが改善される可能性があります - -### Misskey.js -- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) -- Feat: `/admin/role/create` のロールポリシーの型を修正 - -## 2024.5.0 - -### Note -- コントロールパネル内にあるサマリープロキシの設定個所がセキュリティから全般へ変更となります。 -- 悪意のある第三者がリモートユーザーになりすましたアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-2vxv-pv3m-3wvj)をご覧ください。 -- 管理者向け権限 `read:admin:show-users` は `read:admin:show-user` に統合されました。必要に応じてAPIトークンを再発行してください。 - -### General -- Feat: エラートラッキングにSentryを使用できるようになりました -- Enhance: URLプレビューの有効化・無効化を設定できるように #13569 -- Enhance: アンテナでBotによるノートを除外できるように - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545) -- Enhance: クリップのノート数を表示するように -- Enhance: コンディショナルロールの条件として以下を新たに追加 (#13667) - - 猫ユーザーか - - botユーザーか - - サスペンド済みユーザーか - - 鍵アカウントユーザーか - - 「アカウントを見つけやすくする」が有効なユーザーか -- Enhance: Goneを出さずに終了したサーバーへの配信停止を自動的に行うように - - もしそのようなサーバーからから配信が届いた場合には自動的に配信を再開します -- Enhance: 配信停止の理由を表示するように -- Enhance: サーバーのお問い合わせ先URLを設定できるようになりました -- Fix: Play作成時に設定した公開範囲が機能していない問題を修正 -- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正 -- Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正 - -### Client -- Feat: アップロードするファイルの名前をランダム文字列にできるように -- Feat: 個別のお知らせにリンクで飛べるように - (Based on https://github.com/MisskeyIO/misskey/pull/639) -- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように -- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように -- Enhance: リアクション・いいねの総数を表示するように -- Enhance: リアクション受け入れが「いいねのみ」の場合はリアクション絵文字一覧を表示しないように -- Enhance: 設定>プラグインのページからプラグインの簡易的なログやエラーを見られるように - - 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました -- Enhance: ページのデザインを変更 -- Enhance: 2要素認証(ワンタイムパスワード)の入力欄を改善 -- Enhance: 「今日誕生日のフォロー中ユーザー」ウィジェットを手動でリロードできるように -- Enhance: 映像・音声の再生にブラウザのネイティブプレイヤーを使用できるように -- Enhance: 映像・音声の再生メニューに「再生速度」「ループ再生」「ピクチャインピクチャ」を追加 -- Enhance: 映像・音声の再生にキーボードショートカットが使えるように -- Enhance: ノートについているリアクションの「もっと!」から、リアクションの一覧を表示できるように -- Enhance: リプライにて引用がある場合テキストが空でもノートできるように - - 引用したいノートのURLをコピーしリプライ投稿画面にペーストして添付することで達成できます -- Enhance: フォローするかどうかの確認ダイアログを出せるように -- Enhance: Playを手動でリロードできるように -- Enhance: 通報のコメント内のリンクをクリックした際、ウィンドウで開くように -- Enhance: `Ui:C:postForm` および `Ui:C:postFormButton` に `localOnly` と `visibility` を設定できるように -- Enhance: AiScriptを0.18.0にバージョンアップ -- Enhance: 通常のノートでも、お気に入りに登録したチャンネルにリノートできるように -- Enhance: 長いテキストをペーストした際にテキストファイルとして添付するかどうかを選択できるように -- Enhance: 新着ノートをサウンドで通知する機能をdeck UIに追加しました -- Enhance: コントロールパネルのクイックアクションからファイルを照会できるように -- Enhance: コントロールパネルのクイックアクションから通常の照会を行えるように -- Fix: 一部のページ内リンクが正しく動作しない問題を修正 -- Fix: 周年の実績が閏年を考慮しない問題を修正 -- Fix: ローカルURLのプレビューポップアップが左上に表示される -- Fix: WebGL2をサポートしないブラウザで「季節に応じた画面の演出」が有効になっているとき、Misskeyが起動できなくなる問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/459) -- Fix: ページタイトルでローカルユーザーとリモートユーザーの区別がつかない問題を修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/528) -- Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177 - - CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。 -- Fix: タイムゾーンによっては、「今日誕生日のフォロー中ユーザー」ウィジェットが正しく動作しない問題を修正 -- Fix: CWのみの引用リノートが詳細ページで純粋なリノートとして誤って扱われてしまう問題を修正 -- Fix: ノート詳細ページにおいてCW付き引用リノートのCWボタンのラベルに「引用」が含まれていない問題を修正 -- Fix: ダイアログの入力で字数制限に違反していてもEnterキーが押せてしまう問題を修正 -- Fix: ダイレクト投稿の宛先が保存されない問題を修正 -- Fix: Playのページを離れたときに、Playが正常に初期化されない問題を修正 -- Fix: ページのOGP URLが間違っているのを修正 -- Fix: リバーシの対局を正しく共有できないことがある問題を修正 -- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正 -- Fix: 連合なしの状態の読み書きができない問題を修正 -- Fix: `/share` で日本語等を含むurlがurlエンコードされない問題を修正 -- Fix: ファイルを5つ以上添付してもテキストがないとノートが折りたたまれない問題を修正 - -### Server -- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに -- Enhance: misskey-dev/summaly@5.1.0の取り込み(プレビュー生成処理の効率化) -- Enhance: ドライブのファイルがNSFWかどうか個別に連合されるように (#13756) - - 可能な場合、ノートの添付ファイルのセンシティブ判定がファイル単位になります -- Fix: リモートから配送されたアクティビティにJSON-LD compactionをかける -- Fix: フォローリクエストを作成する際に既存のものは削除するように - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/440) -- Fix: エンドポイント`notes/translate`のエラーを改善 -- Fix: CleanRemoteFilesProcessorService report progress from 100% (#13632) -- Fix: 一部の音声ファイルが映像ファイルとして扱われる問題を修正 -- Fix: リプライのみの引用リノートと、CWのみの引用リノートが純粋なリノートとして誤って扱われてしまう問題を修正 -- Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/606) -- Fix: Add Cache-Control to Bull Board -- Fix: nginx経由で/files/にRangeリクエストされた場合に正しく応答できないのを修正 -- Fix: 一部のタイムラインのストリーミングでインスタンスミュートが効かない問題を修正 -- Fix: グローバルタイムラインで返信が表示されないことがある問題を修正 -- Fix: リノートをミュートしたユーザの投稿のリノートがミュートされる問題を修正 -- Fix: AP Link等は添付ファイル扱いしないようになど (#13754) -- Fix: FTTが有効かつsinceIdのみを指定した場合に帰って来るレスポンスが逆順である問題を修正 -- Fix: `/i/notifications`に `includeTypes`か`excludeTypes`を指定しているとき、通知が存在するのに空配列を返すことがある問題を修正 -- Fix: 複数idを指定する`users/show`が関係ないユーザを返すことがある問題を修正 -- Fix: `/tags` と `/user-tags` が検索エンジンにインデックスされないように -- Fix: もともとセンシティブではないと連合されていたファイルがセンシティブとして連合された場合にセンシティブとしてそのファイルを扱うように - - センシティブとして連合したファイルは非センシティブとして連合されてもセンシティブとして扱われます - -## 2024.3.1 + ## 2023.11.1 -### Note -- 悪意のある第三者がリモートユーザーになりすました任意のアクティビティを受け取れてしまう問題を修正しました。詳しくは[GitHub security advisory](https://github.com/misskey-dev/misskey/security/advisories/GHSA-3f39-6537-3cgc)をご覧ください。 - ### General - Feat: 管理者がコントロールパネルからメールアドレスの照会を行えるようになりました - Enhance: ローカリゼーションの更新 @@ -643,7 +53,7 @@ ### General - Feat: アイコンデコレーション機能 - サーバーで用意された画像をアイコンに重ねることができます - - 画像のテンプレートはこちらです: https://misskey-hub.net/brand-assets/ + - 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png - 最大でも黄色いエリア内にデコレーションを収めることを推奨します。 - 画像は512x512pxを推奨します。 - Feat: チャンネル設定にリノート/引用リノートの可否を設定できる項目を追加 @@ -660,7 +70,7 @@ ### Client - Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました - 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください - https://misskey-hub.net/docs/for-developers/publish-on-your-website/ + https://misskey-hub.net/docs/advanced/publish-on-your-website.html - Feat: 通知をグルーピングして表示するオプション(オプトアウト) - Feat: Misskeyの基本的なチュートリアルを実装 - Feat: スワイプしてタイムラインを再読込できるように diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2e48ec61d..13e0656041 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,63 +1,50 @@ # Contribution guide -We're glad you're interested in contributing to Sharkey! In this document you will find the information you need to contribute to the project. +We're glad you're interested in contributing Misskey! In this document you will find the information you need to contribute to the project. + +> **Note** +> This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.** +> Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\ +> The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. +> It will also allow the reader to use the translation tool of their preference if necessary. ## Roadmap -See [ROADMAP.md](./ROADMAP.md) for the upstream Misskey roadmap. +See [ROADMAP.md](./ROADMAP.md) ## Issues Before creating an issue, please check the following: - To avoid duplication, please search for similar issues before creating a new issue. - Do not use Issues to ask questions or troubleshooting. - Issues should only be used to feature requests, suggestions, and bug tracking. - - Please ask questions or troubleshooting in [Discord](https://discord.gg/6VgKmEqHNk). + - Please ask questions or troubleshooting in [GitHub Discussions](https://github.com/misskey-dev/misskey/discussions) or [Discord](https://discord.gg/Wp8gVStHW3). -> [!WARNING] +> **Warning** > Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged. -### Recommended discussing before implementation -We welcome your proposal. +## Before implementation +When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented. -When you want to add a feature or fix a bug, *please open an issue*, -don't just start writing code. We may suggest different approaches, or -show that the "bug" is actually intended behaviour (and offer -work-arounds), or maybe we won't be able to merge your new feature -because it would make it too hard to incorporate future changes from -Misskey. Each of these examples have actually happened! +At this point, you also need to clarify the goals of the PR you will create, and make sure that the other members of the team are aware of them. +PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review. -On the other hand, it's very likely that we'll tell you "go -ahead!". We try our best to incorporate improvements from our users! - -Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask Committer to assign you). -By expressing your intention to work on the Issue, you can prevent conflicts in the work. - -To the Committers: you should not assign someone on it before the Final Decision. - -### How issues are triaged - -The Committers may: -* close an issue that is not reproducible on latest stable release, -* merge an issue into another issue, -* split an issue into multiple issues, -* or re-open that has been closed for some reason which is not applicable anymore. - -@syuilo reserves the Final Decision rights including whether the project will implement feature and how to implement, these rights are not always exercised. +Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work. ## Well-known branches -- **`stable`** branch is tracking the latest release and used for production purposes. +- **`master`** branch is tracking the latest release and used for production purposes. - **`develop`** branch is where we work for the next release. - When you create a PR, basically target it to this branch. +- **`l10n_develop`** branch is reserved for localization management. ## Creating a PR Thank you for your PR! Before creating a PR, please check the following: - If possible, prefix the title with a keyword that identifies the type of this PR, as shown below. - - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc - - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR. + - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc + - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR. - If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text. -- Please add the summary of the changes to [`CHANGELOG.md`](CHANGELOG.md). However, this is not necessary for changes that do not affect the users, such as refactoring. +- Please add the summary of the changes to [`CHANGELOG.md`](/CHANGELOG.md). However, this is not necessary for changes that do not affect the users, such as refactoring. - Check if there are any documents that need to be created or updated due to this change. - If you have added a feature or fixed a bug, please add a test case if possible. - Please make sure that tests and Lint are passed in advance. - - You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing) + - You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing) - If this PR includes UI changes, please attach a screenshot in the text. Thanks for your cooperation 🤗 @@ -67,8 +54,8 @@ Be willing to comment on the good points and not just the things you want fixed ### Review perspective - Scope - - Are the goals of the PR clear? - - Is the granularity of the PR appropriate? + - Are the goals of the PR clear? + - Is the granularity of the PR appropriate? - Security - Does merging this PR create a vulnerability? - Performance @@ -79,22 +66,29 @@ Be willing to comment on the good points and not just the things you want fixed - Are there any omissions or gaps? - Does it check for anomalies? +## Deploy +The `/deploy` command by issue comment can be used to deploy the contents of a PR to the preview environment. +``` +/deploy sha= +``` +An actual domain will be assigned so you can test the federation. + ## Merge ## Release ### Release Instructions -1. Commit version changes in the `develop` branch ([package.json](package.json)) +1. Commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json)) 2. Create a release PR. - - Into `stable` from `develop` branch. + - Into `master` from `develop` branch. - The title must be in the format `Release: x.y.z`. - `x.y.z` is the new version you are trying to release. 3. Deploy and perform a simple QA check. Also verify that the tests passed. 4. Merge it. (Do not squash commit) -5. Create a [release](https://activitypub.software/TransFem-org/Sharkey/-/releases) - - The target branch must be `stable` +5. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases) + - The target branch must be `master` - The tag name must be the version -> [!NOTE] +> **Note** > Why this instruction is necessary: > - To perform final QA checks > - To distribute responsibility @@ -111,34 +105,22 @@ If your language is not listed in Crowdin, please open an issue. ![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg) -## Icon Font (Shark Font) -Sharkey has its own Icon Font called Shark Font which can be found at https://activitypub.software/TransFem-org/shark-font -Build Instructions can all be found over there in the `README`. - -If you have an Icon Suggestion or want to add an Icon please open an issue/merge request over at that repo. - -When Updating the Font make sure to copy **all generated files** from the `dest` folder into `packages/backend/assets/fonts/sharkey-icons` -For the CSS simply copy the file content and replace the old content in `style.css` and for the WOFF, TTF and SVG simply replace them. - ## Development -### Setup -Before developing, you have to set up environment. Misskey requires Redis, PostgreSQL, and FFmpeg. +During development, it is useful to use the -You would want to install Meilisearch to experiment related features. Technically, meilisearch is not strict requirement, but some features and tests require it. +``` +pnpm dev +``` -There are a few ways to proceed. +command. -#### Use system-wide software -You could install them in system-wide (such as from package manager). - -#### Use `docker compose` -You could obtain middleware container by typing `docker compose -f $PROJECT_ROOT/compose.local-db.yml up -d`. - -#### Use Devcontainer -Devcontainer also has necessary setting. This method can be done by connecting from VSCode. +- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es). +- Vite HMR (just the `vite` command) is available. The behavior may be different from production. +- Service Worker is watched by esbuild. +### Dev Container Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. -To use Dev Container, open the project directory on VSCode with Dev Containers installed. +To use Dev Container, open the project directory on VSCode with Dev Containers installed. **Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled. It will run the following command automatically inside the container. @@ -150,38 +132,14 @@ pnpm build pnpm migrate ``` -After finishing the migration, you can proceed. +After finishing the migration, run the `pnpm dev` command to start the development server. -### Start developing -During development, it is useful to use the -``` +``` bash pnpm dev ``` -command. - -- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es). -- Vite HMR (just the `vite` command) is available. The behavior may be different from production. -- Service Worker is watched by esbuild. -- The front end can be viewed by accessing `http://localhost:5173`. -- The backend listens on the port configured with `port` in .config/default.yml. -If you have not changed it from the default, it will be "http://localhost:3000". -If "port" in .config/default.yml is set to something other than 3000, you need to change the proxy settings in packages/frontend/vite.config.local-dev.ts. - -### `MK_DEV_PREFER=backend pnpm dev` -pnpm dev has another mode with `MK_DEV_PREFER=backend`. - -``` -MK_DEV_PREFER=backend pnpm dev -``` - -- This mode is closer to the production environment than the default mode. -- Vite runs behind the backend (the backend will proxy Vite at /vite). -- You can see Misskey by accessing `http://localhost:3000` (Replace `3000` with the port configured with `port` in .config/default.yml). -- To change the port of Vite, specify with `VITE_PORT` environment variable. -- HMR may not work in some environments such as Windows. ## Testing -- Test codes are located in [`/packages/backend/test`](packages/backend/test). +- Test codes are located in [`/packages/backend/test`](/packages/backend/test). ### Run test Create a config file. @@ -190,7 +148,7 @@ cp .github/misskey/test.yml .config/ ``` Prepare DB/Redis for testing. ``` -docker compose -f packages/backend/test/compose.yml up +docker compose -f packages/backend/test/docker-compose.yml up ``` Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. @@ -213,8 +171,8 @@ TODO - `MISSKEY_WEBFINGER_USE_HTTP`: If it's set true, WebFinger requests will be http instead of https, useful for testing federation between servers in localhost. NEVER USE IN PRODUCTION. ## Continuous integration -Sharkey uses GitLab CI for executing automated tests. -Configuration files are located in [`/.gitlab-ci.yml`](.gitlab-ci.yml). +Misskey uses GitHub Actions for executing automated tests. +Configuration files are located in [`/.github/workflows`](/.github/workflows). ## Vue Misskey uses Vue(v3) as its front-end framework. @@ -229,7 +187,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド ### ルート定義 ルート定義は、以下の形式のオブジェクトの配列です。 -```ts +``` ts { name?: string; path: string; @@ -242,7 +200,7 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド } ``` -> [!WARNING] +> **Warning** > 現状、ルートは定義された順に評価されます。 > たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。 @@ -304,7 +262,7 @@ export const Default = { parameters: { layout: 'centered', }, -} satisfies StoryObj; +} satisfies StoryObj; ``` If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file. @@ -324,115 +282,24 @@ export const argTypes = { min: 1, max: 4, }, - }, }; ``` Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers. ```ts -import { HttpResponse, http } from 'msw'; +import { rest } from 'msw'; export const handlers = [ - http.post('/api/notes/timeline', ({ request }) => { - return HttpResponse.json([]); + rest.post('/api/notes/timeline', (req, res, ctx) => { + return res( + ctx.json([]), + ); }), ]; ``` Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files. -## Nest - -### Nest Service Circular dependency / Nestでサービスの循環参照でエラーが起きた場合 - -#### forwardRef -まずは簡単に`forwardRef`を試してみる - -```typescript -export class FooService { - constructor( - @Inject(forwardRef(() => BarService)) - private barService: BarService - ) { - } -} -``` - -#### OnModuleInit -できなければ`OnModuleInit`を使う - -```typescript -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; -import { BarService } from '@/core/BarService'; - -@Injectable() -export class FooService implements OnModuleInit { - private barService: BarService // constructorから移動してくる - - constructor( - private moduleRef: ModuleRef, - ) { - } - - async onModuleInit() { - this.barService = this.moduleRef.get(BarService.name); - } - - public async niceMethod() { - return await this.barService.incredibleMethod({ hoge: 'fuga' }); - } -} -``` - -##### Service Unit Test -テストで`onModuleInit`を呼び出す必要がある - -```typescript -// import ... - -describe('test', () => { - let app: TestingModule; - let fooService: FooService; // for test case - let barService: BarService; // for test case - - beforeEach(async () => { - app = await Test.createTestingModule({ - imports: ..., - providers: [ - FooService, - { // mockする (mockは必須ではないかもしれない) - provide: BarService, - useFactory: () => ({ - incredibleMethod: jest.fn(), - }), - }, - { // Provideにする - provide: BarService.name, - useExisting: BarService, - }, - ], - }) - .useMocker(... - .compile(); - - fooService = app.get(FooService); - barService = app.get(BarService) as jest.Mocked; - - // onModuleInitを実行する - await fooService.onModuleInit(); - }); - - test('nice', () => { - await fooService.niceMethod(); - - expect(barService.incredibleMethod).toHaveBeenCalled(); - expect(barService.incredibleMethod.mock.lastCall![0]) - .toEqual({ hoge: 'fuga' }); - }); -}) -``` - ## Notes ### Misskeyのドメイン固有の概念は`Mi`をprefixする @@ -529,21 +396,20 @@ enumの列挙の内容の削除は、その値をもつレコードを全て削 ### Migration作成方法 packages/backendで: ```sh -pnpm run build -pnpm dlx typeorm migration:generate -d ormconfig.js -o migration/ +pnpm dlx typeorm migration:generate -d ormconfig.js -o ``` - 生成後、ファイルをmigration下に移してください - 作成されたスクリプトは不必要な変更を含むため除去してください ### JSON SchemaのobjectでanyOfを使うとき -JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。 -バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます) +JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。 +バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます) https://github.com/misskey-dev/misskey/pull/10082 テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合: -```ts +``` export const paramDef = { type: 'object', properties: { @@ -573,84 +439,3 @@ marginはそのコンポーネントを使う側が設定する ### indexというファイル名を使うな ESMではディレクトリインポートは廃止されているのと、ディレクトリインポートせずともファイル名が index だと何故か一部のライブラリ?でディレクトリインポートだと見做されてエラーになる - -## CSS Recipe - -### Lighten CSS vars - -``` css -color: hsl(from var(--accent) h s calc(l + 10)); -``` - -### Darken CSS vars - -``` css -color: hsl(from var(--accent) h s calc(l - 10)); -``` - -### Add alpha to CSS vars - -``` css -color: color(from var(--accent) srgb r g b / 0.5); -``` - -## Merging from Misskey into Sharkey - -Make sure you have both remotes in the same clone (`git remote add misskey -https://github.com/misskey-dev/misskey.git`), then: - - git remote update - git checkout develop # this is Sharkey's develop - git checkout -m merge/$(date +%Y-%m-%d) # or whatever - git merge --no-ff misskey/develop - -fix conflicts and *commit*! (conflicts in `pnpm-lock.yaml` can usually -be fixed by running `pnpm install`, it detects conflict markers and -seems to do a decent job) - -*after that commit*, do all the extra work, on the same branch: - -* copy all changes (commit after each step): - * in - `packages/backend/src/core/activitypub/models/ApNoteService.ts`, - from `createNote` to `updateNote` - * from `packages/backend/src/core/NoteCreateService.ts` to - `packages/backend/src/core/NoteEditService.vue` - * from `packages/backend/src/server/api/endpoints/notes/create.ts` - to `packages/backend/src/server/api/endpoints/notes/edit.ts` - * from `packages/frontend/src/components/MkNote*.vue` to - `packages/frontend/src/components/SkNote*.vue` (if sensible) - * from the global timeline to the bubble timeline - (`packages/backend/src/server/api/stream/channels/global-timeline.ts`, - `packages/backend/src/server/api/stream/channels/bubble-timeline.ts`, - `packages/frontend/src/timelines.ts`, - `packages/frontend/src/components/MkTimeline.vue`, - `packages/frontend/src/pages/timeline.vue`, - `packages/frontend/src/ui/deck/tl-column.vue`, - `packages/frontend/src/widgets/WidgetTimeline.vue`) -* check the changes against our `develop` (`git diff develop`) and - against Misskey (`git diff misskey/develop`) -* re-generate `misskey-js` (`pnpm build-misskey-js-with-types`) and commit -* build the frontend: `rm -rf built/; NODE_ENV=development pnpm - --filter=frontend --filter=frontend-embed build` (the `development` - tells it to keep some of the original filenames in the built files) -* make sure there aren't any new `ti-*` classes (Tabler Icons), and - replace them with appropriate `ph-*` ones (Phosphor Icons): - `grep -rP '["'\'']ti[ -](?!fw)' -- built/` should show you what to change. - NOTE: `ti-fw` is a special class that's defined by Misskey, leave it - alone - - after every change, re-build the frontend and check again, until - there are no more `ti-*` classes in the built files - - commit! -* double-check the new migration, that they won't conflict with our db - changes: `git diff develop -- packages/backend/migration/` -* `pnpm clean; pnpm build` -* run tests `pnpm --filter='!megalodon' test` (requires a test - database, [see above](#testing)) and fix as much as you can - * right now `megalodon` doesn't pass its tests, so we skip them -* run lint `pnpm --filter=backend lint` + `pnpm --filter=frontend - eslint` and fix as much as you can - -Then push and open a Merge Request. diff --git a/COPYING b/COPYING index 6a5f3ca1d5..c218443d42 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ Unless otherwise stated this repository is -Copyright © 2014-2024 syuilo and contributors +Copyright © 2014-2023 syuilo and contributers And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. diff --git a/Dockerfile b/Dockerfile index abee7fb098..66018a01c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,86 +1,101 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=20.16.0-alpine3.20 +ARG NODE_VERSION=20.5.1-bullseye -FROM node:${NODE_VERSION} as build +# build assets & compile TypeScript -RUN apk add git linux-headers build-base +FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS native-builder -ENV PYTHONUNBUFFERED=1 -RUN apk add --update python3 && ln -sf python3 /usr/bin/python -RUN apk add py3-pip py3-setuptools +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean \ + ; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \ + && apt-get update \ + && apt-get install -yqq --no-install-recommends \ + build-essential curl ca-certificates + +ARG TARGETARCH + +RUN curl -L https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-$TARGETARCH-static.tar.xz -o /ffmpeg.tar.xz \ + && tar xvf /ffmpeg.tar.xz -C / --strip-components 1 --wildcards 'ffmpeg-*-static/ffmpeg' 'ffmpeg-*-static/ffprobe' RUN corepack enable WORKDIR /sharkey -COPY --link . ./ +COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] +COPY --link ["scripts", "./scripts"] +COPY --link ["packages/megalodon/package.json", "./packages/megalodon/"] +COPY --link ["packages/backend/package.json", "./packages/backend/"] +COPY --link ["packages/frontend/package.json", "./packages/frontend/"] +COPY --link ["packages/sw/package.json", "./packages/sw/"] +COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] -RUN git submodule update --init --recursive -RUN pnpm config set fetch-retries 5 RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --frozen-lockfile --aggregate-output -RUN pnpm build -RUN node scripts/trim-deps.mjs -RUN mv packages/frontend/assets sharkey-assets -RUN mv packages/frontend-embed/assets sharkey-embed-assets -RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ - pnpm prune -RUN rm -r node_modules packages/frontend packages/frontend-shared packages/frontend-embed packages/sw -RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ - pnpm i --prod --frozen-lockfile --aggregate-output -RUN rm -rf .git -FROM node:${NODE_VERSION} +COPY --link . ./ + +ARG NODE_ENV=production + +RUN git submodule update --init +RUN pnpm build +RUN rm -rf .git/ + +# build native dependencies for target platform + +FROM --platform=$TARGETPLATFORM node:${NODE_VERSION} AS target-builder + +RUN apt-get update \ + && apt-get install -yqq --no-install-recommends \ + build-essential + +RUN corepack enable + +WORKDIR /sharkey + +COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"] +COPY --link ["scripts", "./scripts"] +COPY --link ["packages/megalodon/package.json", "./packages/megalodon/"] +COPY --link ["packages/backend/package.json", "./packages/backend/"] + +RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ + pnpm i --frozen-lockfile --aggregate-output + +FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner ARG UID="991" ARG GID="991" -RUN apk add ffmpeg tini jemalloc \ +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + tini curl libjemalloc-dev libjemalloc2 \ + && ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \ && corepack enable \ - && addgroup -g "${GID}" sharkey \ - && adduser -D -u "${UID}" -G sharkey -h /sharkey sharkey \ - && mkdir /sharkey/files \ - && chown sharkey:sharkey /sharkey/files \ - && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -exec chmod u-s {} \; \ - && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /g+s -exec chmod g-s {} \; + && groupadd -g "${GID}" sharkey \ + && useradd -l -u "${UID}" -g "${GID}" -m -d /sharkey sharkey \ + && find / -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ + && find / -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists USER sharkey WORKDIR /sharkey -# add package.json to add pnpm -COPY --chown=sharkey:sharkey ./package.json ./package.json -RUN corepack install +COPY --chown=sharkey:sharkey --from=target-builder /sharkey/node_modules ./node_modules +COPY --chown=sharkey:sharkey --from=target-builder /sharkey/packages/megalodon/node_modules ./packages/megalodon/node_modules +COPY --chown=sharkey:sharkey --from=target-builder /sharkey/packages/backend/node_modules ./packages/backend/node_modules +COPY --chown=sharkey:sharkey --from=native-builder /ffmpeg /usr/local/bin/ +COPY --chown=sharkey:sharkey --from=native-builder /ffprobe /usr/local/bin/ +COPY --chown=sharkey:sharkey --from=native-builder /sharkey/built ./built +COPY --chown=sharkey:sharkey --from=native-builder /sharkey/packages/megalodon/lib ./packages/megalodon/lib +COPY --chown=sharkey:sharkey --from=native-builder /sharkey/packages/backend/built ./packages/backend/built +COPY --chown=sharkey:sharkey --from=native-builder /sharkey/fluent-emojis /sharkey/fluent-emojis +COPY --chown=sharkey:sharkey . ./ -COPY --chown=sharkey:sharkey --from=build /sharkey/node_modules ./node_modules -COPY --chown=sharkey:sharkey --from=build /sharkey/packages/backend/node_modules ./packages/backend/node_modules -COPY --chown=sharkey:sharkey --from=build /sharkey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules -COPY --chown=sharkey:sharkey --from=build /sharkey/packages/misskey-reversi/node_modules ./packages/misskey-reversi/node_modules -COPY --chown=sharkey:sharkey --from=build /sharkey/packages/misskey-bubble-game/node_modules ./packages/misskey-bubble-game/node_modules -COPY --chown=sharkey:sharkey --from=build /sharkey/packages/megalodon/node_modules ./packages/megalodon/node_modules -COPY --chown=sharkey:sharkey --from=build /sharkey/built ./built -COPY --chown=sharkey:sharkey --from=build /sharkey/packages/misskey-js/built ./packages/misskey-js/built -COPY --chown=sharkey:sharkey --from=build /sharkey/packages/misskey-reversi/built ./packages/misskey-reversi/built -COPY --chown=sharkey:sharkey --from=build /sharkey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built -COPY --chown=sharkey:sharkey --from=build /sharkey/packages/backend/built ./packages/backend/built -COPY --chown=sharkey:sharkey --from=build /sharkey/packages/megalodon/lib ./packages/megalodon/lib -COPY --chown=sharkey:sharkey --from=build /sharkey/fluent-emojis ./fluent-emojis -COPY --chown=sharkey:sharkey --from=build /sharkey/tossface-emojis/dist ./tossface-emojis/dist -COPY --chown=sharkey:sharkey --from=build /sharkey/sharkey-assets ./packages/frontend/assets -COPY --chown=sharkey:sharkey --from=build /sharkey/sharkey-embed-assets ./packages/frontend-embed/assets - -COPY --chown=sharkey:sharkey pnpm-workspace.yaml ./pnpm-workspace.yaml -COPY --chown=sharkey:sharkey packages/backend/package.json ./packages/backend/package.json -COPY --chown=sharkey:sharkey packages/backend/scripts/check_connect.js ./packages/backend/scripts/check_connect.js -COPY --chown=sharkey:sharkey packages/backend/ormconfig.js ./packages/backend/ormconfig.js -COPY --chown=sharkey:sharkey packages/backend/migration ./packages/backend/migration -COPY --chown=sharkey:sharkey packages/backend/assets ./packages/backend/assets -COPY --chown=sharkey:sharkey packages/megalodon/package.json ./packages/megalodon/package.json -COPY --chown=sharkey:sharkey packages/misskey-js/package.json ./packages/misskey-js/package.json -COPY --chown=sharkey:sharkey packages/misskey-reversi/package.json ./packages/misskey-reversi/package.json -COPY --chown=sharkey:sharkey packages/misskey-bubble-game/package.json ./packages/misskey-bubble-game/package.json - -ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2 +ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so ENV NODE_ENV=production -ENTRYPOINT ["/sbin/tini", "--"] +VOLUME "/sharkey/files" +HEALTHCHECK --interval=5s --retries=20 CMD ["/bin/bash", "/sharkey/healthcheck.sh"] +ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["pnpm", "run", "migrateandstart"] diff --git a/IMPORTANT_NOTES.md b/IMPORTANT_NOTES.md deleted file mode 100644 index 54d0440bea..0000000000 --- a/IMPORTANT_NOTES.md +++ /dev/null @@ -1,16 +0,0 @@ -# Basic Precautions - -When using a service with Sharkey, there are several important points to keep in mind. - -1. Because it is decentralized, there is no guarantee that data you upload will be deleted from all other servers even if you delete it once. (However, this applies to the internet in general.) - -2. Even for posts made in private, there is no guarantee that the recipient's server will treat them as private in the same way. Please exercise caution when posting personal or confidential information. (Again, this applies to the internet in general.) - -3. The "Drive" feature is NOT secure cloud storage. This feature exists for easier managing of your uploaded files. -Any data uploaded, whether shared via post or not, will be publicly accessible. Please use 3rd party cloud storage providers if you need to upload data with sensitive information of any kind. - -4. Account deletion can be a resource-intensive process and may take a long time. In cases with a lot of uploaded data, it may even be impossible to delete an account. - -5. Please disable ad blockers. Some servers may rely on advertising revenue to cover operating costs. Additionally, ad blockers can mistakenly block content and features unrelated to ads, potentially causing issues with the client's functionality and preventing normal use of Sharkey. Therefore, we recommend turning off ad blockers and similar features when using Sharkey. - -Please understand these points and enjoy using the service. diff --git a/README.md b/README.md index f9198c06c0..8b9da7e712 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@
- Sharkey logo + Sharkey logo **🌎 **[Sharkey](https://joinsharkey.org/)** is an open source, decentralized social media platform that's free forever! 🚀** --- - - create an instance + + find an instance become a contributor @@ -16,8 +16,10 @@ join the community - - donate + + donate + + Translate Sharkey --- @@ -25,13 +27,13 @@
- + ## ✨ Features - **ActivityPub support**\ Not on Sharkey? No problem! Not only can Sharkey instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed! - **Federated Backgrounds and Music status**\ -You can add a background to your profile as well as a music status via ListenBrainz, show everyone what music you are currently listening to +You can add a background to your profile as well as a music status via ListenBrainz, show everyone what music you are currently listening too - **Mastodon API**\ Sharkey implements the Mastodon API unlike normal Misskey - **UI/UX Improvements**\ @@ -39,15 +41,11 @@ Sharkey makes some UI/UX improvements to make it easier to navigate - **Sign-Up Approval**\ With Sharkey, you can enable sign-ups, subject to manual moderator approval and mandatory user-provided reasons for joining. - **Rich Web UI**\ - Sharkey has a rich and easy to use Web UI! - It is highly customizable, from changing the layout and adding widgets to making custom themes. - Furthermore, plugins can be created using AiScript, an original programming language. + Sharkey has a rich and easy to use Web UI! + It is highly customizable, from changing the layout and adding widgets to making custom themes. + Furthermore, plugins can be created using AiScript, an original programming language. - And much more...
- -## Documentation - -Sharkey Documentation can be found at [Sharkey Documentation](https://docs.joinsharkey.org/docs/install/fresh/) diff --git a/ROADMAP.md b/ROADMAP.md index 509ecb9fe7..3077c41e73 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,7 +6,6 @@ Also, the later tasks are more indefinite and are subject to change as developme This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development. - ~~Make the number of type errors zero (backend)~~ → Done ✔️ -- Make the number of type errors zero (frontend) - Improve CI - ~~Fix tests~~ → Done ✔️ - Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986 diff --git a/SECURITY.md b/SECURITY.md index cfc0614dd6..fa00b700e9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,6 +4,6 @@ If you discover a security issue in Sharkey, please report it by sending an email to [admin@transfem.org](mailto:admin@transfem.org). This will allow us to assess the risk, and make a fix available before we add a -bug report to the GitLab repository. +bug report to the GitHub repository. Thanks for helping make Sharkey safe for everyone. diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md deleted file mode 100644 index c941de6643..0000000000 --- a/UPGRADE_NOTES.md +++ /dev/null @@ -1,74 +0,0 @@ -# Upgrade Notes - -## 2024.10.0 - -### Hellspawns - -Sharkey versions before 2024.10 suffered from a bug in the "Mark instance as NSFW" feature. -When a user from such an instance boosted a note, the boost would be converted to a hellspawn (pure renote with Content Warning). -Hellspawns are buggy and do not properly federate, so it may be desirable to correct any that already exist in the database. -The following script will correct any local or remote hellspawns in the database. - -```postgresql -/* Remove "instance is marked as NSFW" hellspawns */ -UPDATE "note" -SET "cw" = null -WHERE - "renoteId" IS NOT NULL - AND "text" IS NULL - AND "cw" = 'Instance is marked as NSFW' - AND "replyId" IS NULL - AND "hasPoll" = false - AND "fileIds" = '{}'; - -/* Fix legacy / user-created hellspawns */ -UPDATE "note" -SET "text" = '.' -WHERE - "renoteId" IS NOT NULL - AND "text" IS NULL - AND "cw" IS NOT NULL - AND "replyId" IS NULL - AND "hasPoll" = false - AND "fileIds" = '{}'; -``` - -## 2024.9.0 - -### Following Feed - -When upgrading an existing instance to version 2024.9.0, the Following Feed will initially be empty. -The feed will gradually fill as new posts federate, but it may be desirable to back-fill the feed with existing data. -This database script will populate the feed with the latest post of each type for all users, ensuring that data is fully populated after the update. -Run this after migrations but before starting the instance. -Warning: the script may take a long time to execute! - -```postgresql -INSERT INTO latest_note (user_id, note_id, is_public, is_reply, is_quote) -SELECT - "userId" as user_id, - id as note_id, - visibility = 'public' AS is_public, - "replyId" IS NOT NULL AS is_reply, - ( - "renoteId" IS NOT NULL - AND ( - text IS NOT NULL - OR cw IS NOT NULL - OR "replyId" IS NOT NULL - OR "hasPoll" - OR "fileIds" != '{}' - ) - ) AS is_quote -FROM note -WHERE ( -- Exclude pure renotes (boosts) - "renoteId" IS NULL - OR text IS NOT NULL - OR cw IS NOT NULL - OR "replyId" IS NOT NULL - OR "hasPoll" - OR "fileIds" != '{}' - ) -ORDER BY id DESC -- This part is very important: it ensures that we only load the *latest* notes of each type. Do not remove it! -ON CONFLICT DO NOTHING; -- Any conflicts are guaranteed to be older notes that we can ignore. -``` diff --git a/chart/files/default.yml b/chart/files/default.yml index 97201aad66..87b2f677eb 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -77,17 +77,17 @@ dbReplications: false # You can configure any number of replicas here #dbSlaves: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # - -# host: -# port: -# db: -# user: -# pass: +# host: +# port: +# db: +# user: +# pass: # ┌─────────────────────┐ #───┘ Redis configuration └───────────────────────────────────── @@ -124,14 +124,6 @@ redis: # #prefix: example-prefix # #db: 1 -#redisForReactions: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── @@ -160,22 +152,6 @@ redis: # ID SETTINGS AFTER THAT! id: "aidx" - -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - # ┌─────────────────────┐ #───┘ Other configuration └───────────────────────────────────── @@ -191,7 +167,7 @@ id: "aidx" # Job rate limiter # deliverJobPerSec: 128 -# inboxJobPerSec: 32 +# inboxJobPerSec: 16 # Job attempts # deliverJobMaxAttempts: 12 @@ -216,15 +192,8 @@ id: "aidx" # Media Proxy #mediaProxy: https://example.com/proxy -# Sign outgoing ActivityPub GET request (default: true) +# Sign to ActivityPub GET request (default: true) signToActivityPubGet: true -# Sign outgoing ActivityPub Activities (default: true) -# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. -# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. -# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. -attachLdSignatureForRelays: true -# check that inbound ActivityPub GET requests are signed ("authorized fetch") -checkActivityPubGetSignature: false #allowedPrivateNetworks: [ # '127.0.0.1/32' diff --git a/chart/templates/Deployment.yml b/chart/templates/Deployment.yml index 3c73837801..d5dd14f59e 100644 --- a/chart/templates/Deployment.yml +++ b/chart/templates/Deployment.yml @@ -27,7 +27,7 @@ spec: ports: - containerPort: 3000 - name: postgres - image: postgres:15-alpine + image: postgres:14-alpine env: - name: POSTGRES_USER value: "example-misskey-user" @@ -38,7 +38,7 @@ spec: ports: - containerPort: 5432 - name: redis - image: redis:7-alpine + image: redis:alpine ports: - containerPort: 6379 volumes: diff --git a/compose_example.yml b/compose_example.yml deleted file mode 100644 index 0db8b04dc6..0000000000 --- a/compose_example.yml +++ /dev/null @@ -1,97 +0,0 @@ -services: - web: -# image: registry.activitypub.software/transfem-org/sharkey:latest - build: . - restart: always - links: - - db - - redis -# - mcaptcha -# - meilisearch - depends_on: - db: - condition: service_healthy - redis: - condition: service_healthy - ports: - - "3000:3000" - networks: - - shonk - # env_file: - # - .config/docker.env - volumes: - - ./files:/sharkey/files - - ./.config:/sharkey/.config:ro - - redis: - restart: always - image: redis:7-alpine - networks: - - shonk - volumes: - - ./redis:/data - healthcheck: - test: "redis-cli ping" - interval: 5s - retries: 20 - - db: - restart: always - image: postgres:15-alpine - networks: - - shonk - env_file: - - .config/docker.env - volumes: - - ./db:/var/lib/postgresql/data - healthcheck: - test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" - interval: 5s - retries: 20 - -# mcaptcha: -# restart: always -# image: mcaptcha/mcaptcha:latest -# networks: -# shonk: -# aliases: -# - localhost -# ports: -# - 7493:7493 -# env_file: -# - .config/docker.env -# environment: -# PORT: 7493 -# MCAPTCHA_redis_URL: "redis://mcaptcha_redis/" -# MCAPTCHA_allow_registration: true -# MCAPTCHA_server_DOMAIN: "example.tld" -# depends_on: -# db: -# condition: service_healthy -# mcaptcha_redis: -# condition: service_healthy -# -# mcaptcha_redis: -# image: mcaptcha/cache:latest -# networks: -# - shonk -# healthcheck: -# test: "redis-cli ping" -# interval: 5s -# retries: 20 - -# meilisearch: -# restart: always -# image: getmeili/meilisearch:v1.3.4 -# environment: -# - MEILI_NO_ANALYTICS=true -# - MEILI_ENV=production -# - MEILI_MASTER_KEY=ChangeThis -# networks: -# - shonk -# volumes: -# - ./meili_data:/meili_data - - -networks: - shonk: diff --git a/crowdin.yml b/crowdin.yml index 0525ac7b0b..774ddc7a63 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,4 +1,4 @@ files: - - source: /sharkey-locales/en-US.yml - translation: /sharkey-locales/%locale%.yml + - source: /locales/ja-JP.yml + translation: /locales/%locale%.yml update_option: update_as_unapproved diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.js similarity index 93% rename from cypress/e2e/basic.cy.ts rename to cypress/e2e/basic.cy.js index d2525e0a7d..5ab07c7480 100644 --- a/cypress/e2e/basic.cy.ts +++ b/cypress/e2e/basic.cy.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - describe('Before setup instance', () => { beforeEach(() => { cy.resetState(); @@ -166,13 +161,11 @@ describe('After user signed in', () => { }); it('successfully loads', () => { - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).should('be.visible'); + cy.get('[data-cy-user-setup-continue]').should('be.visible'); }); it('account setup wizard', () => { - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup-continue]', { timeout: 30000 }).click(); + cy.get('[data-cy-user-setup-continue]').click(); cy.get('[data-cy-user-setup-user-name] input').type('ありす'); cy.get('[data-cy-user-setup-user-description] textarea').type('ほげ'); @@ -209,8 +202,7 @@ describe('After user setup', () => { cy.login('alice', 'alice1234'); // アカウント初期設定ウィザード - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); + cy.get('[data-cy-user-setup] [data-cy-modal-window-close]').click(); cy.get('[data-cy-modal-dialog-ok]').click(); }); diff --git a/cypress/e2e/router.cy.ts b/cypress/e2e/router.cy.ts deleted file mode 100644 index 8d8fb3af31..0000000000 --- a/cypress/e2e/router.cy.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -describe('Router transition', () => { - describe('Redirect', () => { - // サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い) - before(() => { - cy.resetState(); - - // インスタンス初期セットアップ - cy.registerUser('admin', 'pass', true); - - // ユーザー作成 - cy.registerUser('alice', 'alice1234'); - - cy.login('alice', 'alice1234'); - - // アカウント初期設定ウィザード - // 表示に時間がかかるのでデフォルト秒数だとタイムアウトする - cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 30000 }).click(); - cy.wait(500); - cy.get('[data-cy-modal-dialog-ok]').click(); - }); - - it('redirect to user profile', () => { - // テストのためだけに用意されたリダイレクト用ルートに飛ぶ - cy.visit('/redirect-test'); - - // プロフィールページのURLであることを確認する - cy.url().should('include', '/@alice') - }); - }); -}); diff --git a/cypress/e2e/widgets.cy.ts b/cypress/e2e/widgets.cy.js similarity index 95% rename from cypress/e2e/widgets.cy.ts rename to cypress/e2e/widgets.cy.js index 847801a69f..df6ec8357d 100644 --- a/cypress/e2e/widgets.cy.ts +++ b/cypress/e2e/widgets.cy.js @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - /* flaky describe('After user signed in', () => { beforeEach(() => { diff --git a/cypress/support/commands.ts b/cypress/support/commands.js similarity index 87% rename from cypress/support/commands.ts rename to cypress/support/commands.js index 281f2e6ccd..91a4d7abe6 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.js @@ -30,13 +30,9 @@ Cypress.Commands.add('visitHome', () => { }) Cypress.Commands.add('resetState', () => { - // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 - // see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123 - /* - cy.window().then(win => { + cy.window(win => { win.indexedDB.deleteDatabase('keyval-store'); }); - */ cy.request('POST', '/api/reset-db', {}).as('reset'); cy.get('@reset').its('status').should('equal', 204); cy.reload(true); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.js similarity index 100% rename from cypress/support/e2e.ts rename to cypress/support/e2e.js diff --git a/cypress/support/index.ts b/cypress/support/index.ts deleted file mode 100644 index c1bed21979..0000000000 --- a/cypress/support/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare global { - namespace Cypress { - interface Chainable { - login(username: string, password: string): Chainable; - - registerUser( - username: string, - password: string, - isAdmin?: boolean - ): Chainable; - - resetState(): Chainable; - - visitHome(): Chainable; - } - } -} - -export {} diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json deleted file mode 100644 index 6fe7f32cc4..0000000000 --- a/cypress/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "es5"], - "target": "es5", - "types": ["cypress", "node"] - }, - "include": ["./**/*.ts"] -} diff --git a/compose.local-db.yml b/docker-compose.yml.example similarity index 58% rename from compose.local-db.yml rename to docker-compose.yml.example index 3835cb23db..1e94c35d47 100644 --- a/compose.local-db.yml +++ b/docker-compose.yml.example @@ -1,11 +1,32 @@ -# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します +version: "3" services: + web: +# image: ghcr.io/transfem-org/sharkey:stable + build: . + restart: always + links: + - db + - redis +# - meilisearch + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + ports: + - "3000:3000" + networks: + - shonk + volumes: + - ./files:/sharkey/files + - ./.config:/sharkey/.config:ro + redis: restart: always image: redis:7-alpine - ports: - - "6379:6379" + networks: + - shonk volumes: - ./redis:/data healthcheck: @@ -16,8 +37,8 @@ services: db: restart: always image: postgres:15-alpine - ports: - - "5432:5432" + networks: + - shonk env_file: - .config/docker.env volumes: @@ -35,6 +56,11 @@ services: # - MEILI_ENV=production # env_file: # - .config/meilisearch.env +# networks: +# - shonk # volumes: # - ./meili_data:/meili_data + +networks: + shonk: diff --git a/eslint/locale.js b/eslint/locale.js deleted file mode 100644 index dbb807b714..0000000000 --- a/eslint/locale.js +++ /dev/null @@ -1,251 +0,0 @@ -/* - * SPDX-FileCopyrightText: dakkar and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only -*/ - -/* This is a ESLint rule to report use of the `i18n.ts` and `i18n.tsx` - * objects that reference translation items that don't actually exist - * in the lexicon (the `locale/` files) - */ - -/* given a MemberExpression node, collects all the member names - * - * e.g. for a bit of code like `foo=one.two.three`, `collectMembers` - * called on the node for `three` would return `['one', 'two', - * 'three']` - */ -function collectMembers(node) { - if (!node) return []; - if (node.type !== 'MemberExpression') return []; - // this is something like `foo[bar]` - if (node.computed) return []; - return [ node.property.name, ...collectMembers(node.parent) ]; -} - -/* given an object and an array of names, recursively descends the - * object via those names - * - * e.g. `walkDown({one:{two:{three:15}}},['one','two','three'])` would - * return 15 - */ -function walkDown(locale, path) { - if (!locale) return null; - if (!path || path.length === 0 || !path[0]) return locale; - return walkDown(locale[path[0]], path.slice(1)); -} - -/* given a MemberExpression node, returns its attached CallExpression - * node if present - * - * e.g. for a bit of code like `foo=one.two.three()`, - * `findCallExpression` called on the node for `three` would return - * the node for function call (which is the parent of the `one` and - * `two` nodes, and holds the nodes for the argument list) - * - * if the code had been `foo=one.two.three`, `findCallExpression` - * would have returned null, because there's no function call attached - * to the MemberExpressions - */ -function findCallExpression(node) { - if (!node.parent) return null; - - // the second half of this guard protects from cases like - // `foo(one.two.three)` where the CallExpression is parent of the - // MemberExpressions, but via `arguments`, not `callee` - if (node.parent.type === 'CallExpression' && node.parent.callee === node) return node.parent; - if (node.parent.type === 'MemberExpression') return findCallExpression(node.parent); - return null; -} - -// same, but for Vue expressions (``) -function findVueExpression(node) { - if (!node.parent) return null; - - if (node.parent.type.match(/^VExpr/) && node.parent.expression === node) return node.parent; - if (node.parent.type === 'MemberExpression') return findVueExpression(node.parent); - return null; -} - -function areArgumentsOneObject(node) { - return node.arguments.length === 1 && - node.arguments[0].type === 'ObjectExpression'; -} - -// only call if `areArgumentsOneObject(node)` is true -function getArgumentObjectProperties(node) { - return new Set(node.arguments[0].properties.map( - p => { - if (p.key && p.key.type === 'Identifier') return p.key.name; - return null; - }, - )); -} - -function getTranslationParameters(translation) { - return new Set(Array.from(translation.matchAll(/\{(\w+)\}/g)).map( m => m[1] )); -} - -function setDifference(a,b) { - const result = []; - for (const element of a.values()) { - if (!b.has(element)) { - result.push(element); - } - } - - return result; -} - -/* the actual rule body - */ -function theRuleBody(context,node) { - // we get the locale/translations via the options; it's the data - // that goes into a specific language's JSON file, see - // `scripts/build-assets.mjs` - const locale = context.options[0]; - - // sometimes we get MemberExpression nodes that have a - // *descendent* with the right identifier: skip them, we'll get - // the right ones as well - if (node.object?.name !== 'i18n') { - return; - } - - // `method` is going to be `'ts'` or `'tsx'`, `path` is going to - // be the various translation steps/names - const [ method, ...path ] = collectMembers(node); - const pathStr = `i18n.${method}.${path.join('.')}`; - - // does that path point to a real translation? - const translation = walkDown(locale, path); - if (!translation) { - context.report({ - node, - message: `translation missing for ${pathStr}`, - }); - return; - } - - // we hit something weird, assume the programmers know what - // they're doing (this is usually some complicated slicing of - // the translation structure) - if (typeof(translation) !== 'string') return; - - const callExpression = findCallExpression(node); - const vueExpression = findVueExpression(node); - - // some more checks on how the translation is called - if (method === 'ts') { - // the ` component gets parametric translations via - // `i18n.ts.*`, but we error out elsewhere - if (translation.match(/\{/) && !vueExpression) { - context.report({ - node, - message: `translation for ${pathStr} is parametric, but called via 'ts'`, - }); - return; - } - - if (callExpression) { - context.report({ - node, - message: `translation for ${pathStr} is not parametric, but is called as a function`, - }); - } - } - - if (method === 'tsx') { - if (!translation.match(/\{/)) { - context.report({ - node, - message: `translation for ${pathStr} is not parametric, but called via 'tsx'`, - }); - return; - } - - if (!callExpression && !vueExpression) { - context.report({ - node, - message: `translation for ${pathStr} is parametric, but not called as a function`, - }); - return; - } - - // we're not currently checking arguments when used via the - // `` component, because it's too complicated (also, it - // would have to be done inside the `if (method === 'ts')`) - if (!callExpression) return; - - if (!areArgumentsOneObject(callExpression)) { - context.report({ - node, - message: `translation for ${pathStr} should be called with a single object as argument`, - }); - return; - } - - const translationParameters = getTranslationParameters(translation); - const parameterCount = translationParameters.size; - const callArguments = getArgumentObjectProperties(callExpression); - const argumentCount = callArguments.size; - - if (parameterCount !== argumentCount) { - context.report({ - node, - message: `translation for ${pathStr} has ${parameterCount} parameters, but is called with ${argumentCount} arguments`, - }); - } - - // node 20 doesn't have `Set.difference`... - const extraArguments = setDifference(callArguments, translationParameters); - const missingArguments = setDifference(translationParameters, callArguments); - - if (extraArguments.length > 0) { - context.report({ - node, - message: `translation for ${pathStr} passes unused arguments ${extraArguments.join(' ')}`, - }); - } - - if (missingArguments.length > 0) { - context.report({ - node, - message: `translation for ${pathStr} does not pass arguments ${missingArguments.join(' ')}`, - }); - } - } -} - -function theRule(context) { - // we get the locale/translations via the options; it's the data - // that goes into a specific language's JSON file, see - // `scripts/build-assets.mjs` - const locale = context.options[0]; - - // for all object member access that have an identifier 'i18n'... - return context.getSourceCode().parserServices.defineTemplateBodyVisitor( - { - // this is for
- {{ c.text }} - + {{ c.text }} + {{ c.text }}
{{ button.text }} @@ -20,19 +20,19 @@ SPDX-License-Identifier: AGPL-3.0-only - + - + - + - + @@ -42,10 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -54,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
@@ -63,14 +60,14 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -135,10 +131,6 @@ function onMousedown(evt: MouseEvent): void { box-sizing: border-box; transition: background 0.1s ease; - &:hover { - text-decoration: none; - } - &:not(:disabled):hover { background: var(--buttonHoverBg); } @@ -171,11 +163,11 @@ function onMousedown(evt: MouseEvent): void { background: var(--accent); &:not(:disabled):hover { - background: hsl(from var(--accent) h s calc(l + 5)); + background: var(--X8); } &:not(:disabled):active { - background: hsl(from var(--accent) h s calc(l + 5)); + background: var(--X8); } } @@ -220,16 +212,15 @@ function onMousedown(evt: MouseEvent): void { background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + background: linear-gradient(90deg, var(--X8), var(--X8)); } &:not(:disabled):active { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + background: linear-gradient(90deg, var(--X8), var(--X8)); } } &.danger { - font-weight: bold; color: #ff2a2a; &.primary { @@ -247,10 +238,11 @@ function onMousedown(evt: MouseEvent): void { } &:disabled { - opacity: 0.5; + opacity: 0.7; } &:focus-visible { + outline: solid 2px var(--focus); outline-offset: 2px; } diff --git a/packages/frontend/src/components/MkCaptcha.stories.impl.ts b/packages/frontend/src/components/MkCaptcha.stories.impl.ts index 475257cc45..fb50e50b18 100644 --- a/packages/frontend/src/components/MkCaptcha.stories.impl.ts +++ b/packages/frontend/src/components/MkCaptcha.stories.impl.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index ab00ea9930..14e59acad2 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -1,22 +1,19 @@ - diff --git a/packages/frontend/src/components/MkCode.stories.impl.ts b/packages/frontend/src/components/MkCode.stories.impl.ts deleted file mode 100644 index b7e53e8e35..0000000000 --- a/packages/frontend/src/components/MkCode.stories.impl.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import MkCode from './MkCode.vue'; -const code = `for (let i, 100) { - <: if (i % 15 == 0) "FizzBuzz" - elif (i % 3 == 0) "Fizz" - elif (i % 5 == 0) "Buzz" - else i -}`; -export const Default = { - render(args) { - return { - components: { - MkCode, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - template: '', - }; - }, - args: { - code, - lang: 'is', - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index 05cde89dd9..b39e6ff23c 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -1,105 +1,39 @@ diff --git a/packages/frontend/src/components/MkCodeEditor.stories.impl.ts b/packages/frontend/src/components/MkCodeEditor.stories.impl.ts deleted file mode 100644 index 5c410c4886..0000000000 --- a/packages/frontend/src/components/MkCodeEditor.stories.impl.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import { action } from '@storybook/addon-actions'; -import MkCodeEditor from './MkCodeEditor.vue'; -const code = `for (let i, 100) { - <: if (i % 15 == 0) "FizzBuzz" - elif (i % 3 == 0) "Fizz" - elif (i % 5 == 0) "Buzz" - else i -}`; -export const Default = { - render(args) { - return { - components: { - MkCodeEditor, - }, - data() { - return { - code, - }; - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - 'change': action('change'), - 'keydown': action('keydown'), - 'enter': action('enter'), - 'update:modelValue': action('update:modelValue'), - }; - }, - }, - template: '', - }; - }, - args: { - lang: 'aiscript', - }, - parameters: { - layout: 'fullscreen', - }, - decorators: [ - () => ({ - template: '
', - }), - ], -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index b233189ab0..5434042684 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -1,41 +1,33 @@ diff --git a/packages/frontend/src/components/MkCodeInline.stories.impl.ts b/packages/frontend/src/components/MkCodeInline.stories.impl.ts deleted file mode 100644 index 51d4d106ff..0000000000 --- a/packages/frontend/src/components/MkCodeInline.stories.impl.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import MkCodeInline from './MkCodeInline.vue'; -export const Default = { - render(args) { - return { - components: { - MkCodeInline, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - template: '', - }; - }, - args: { - code: '<: "Hello, world!"', - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue deleted file mode 100644 index 6add80d1bc..0000000000 --- a/packages/frontend/src/components/MkCodeInline.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkColorInput.stories.impl.ts b/packages/frontend/src/components/MkColorInput.stories.impl.ts deleted file mode 100644 index 61383e2cae..0000000000 --- a/packages/frontend/src/components/MkColorInput.stories.impl.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import { action } from '@storybook/addon-actions'; -import MkColorInput from './MkColorInput.vue'; -export const Default = { - render(args) { - return { - components: { - MkColorInput, - }, - data() { - return { - color: '#cccccc', - }; - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - 'update:modelValue': action('update:modelValue'), - }; - }, - }, - template: '', - }; - }, - parameters: { - layout: 'fullscreen', - }, - decorators: [ - () => ({ - template: '
', - }), - ], -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue index 99aa46d561..79b1949640 100644 --- a/packages/frontend/src/components/MkColorInput.vue +++ b/packages/frontend/src/components/MkColorInput.vue @@ -1,5 +1,5 @@ @@ -24,7 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkContainer.stories.impl.ts b/packages/frontend/src/components/MkContainer.stories.impl.ts deleted file mode 100644 index 72a7659521..0000000000 --- a/packages/frontend/src/components/MkContainer.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkContainer from './MkContainer.vue'; -void MkContainer; diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index 5f71e289b8..42c6cc1075 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -1,5 +1,5 @@ @@ -13,8 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -216,7 +216,7 @@ onUnmounted(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + background: linear-gradient(0deg, var(--panel), var(--X15)); > .fadeLabel { display: inline-block; diff --git a/packages/frontend/src/components/MkContextMenu.stories.impl.ts b/packages/frontend/src/components/MkContextMenu.stories.impl.ts deleted file mode 100644 index 1ff0f51bd4..0000000000 --- a/packages/frontend/src/components/MkContextMenu.stories.impl.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import { userEvent, within } from '@storybook/test'; -import MkContextMenu from './MkContextMenu.vue'; -import * as os from '@/os.js'; -export const Empty = { - render(args) { - return { - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - methods: { - onContextmenu(ev: MouseEvent) { - os.contextMenu(args.items, ev); - }, - }, - template: '
Right Click Here
', - }; - }, - args: { - items: [], - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - const target = canvas.getByText('Right Click Here'); - await userEvent.pointer({ keys: '[MouseRight>]', target }); - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; -export const SomeTabs = { - ...Empty, - args: { - items: [ - { - text: 'Home', - icon: 'ti ti-home', - action() {}, - }, - ], - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index f51fefa0c0..6cca7fc353 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -1,5 +1,5 @@ @@ -12,15 +12,15 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" >
- +
diff --git a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts deleted file mode 100644 index ce13093975..0000000000 --- a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; -import { action } from '@storybook/addon-actions'; -import { file } from '../../.storybook/fakes.js'; -import { commonHandlers } from '../../.storybook/mocks.js'; -import MkCropperDialog from './MkCropperDialog.vue'; -export const Default = { - render(args) { - return { - components: { - MkCropperDialog, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - 'ok': action('ok'), - 'cancel': action('cancel'), - 'closed': action('closed'), - }; - }, - }, - template: '', - }; - }, - args: { - file: file(), - aspectRatio: NaN, - }, - parameters: { - chromatic: { - // NOTE: ロードが終わるまで待つ - delay: 3000, - }, - layout: 'centered', - msw: { - handlers: [ - ...commonHandlers, - http.get('/proxy/image.webp', async ({ request }) => { - const url = new URL(request.url).searchParams.get('url'); - if (url === 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true') { - const image = await (await fetch('client-assets/fedi.jpg')).blob(); - return new HttpResponse(image, { - headers: { - 'Content-Type': 'image/jpeg', - }, - }); - } else { - return new HttpResponse(null, { status: 404 }); - } - }), - http.post('/api/drive/files/create', async ({ request }) => { - action('POST /api/drive/files/create')(await request.formData()); - return HttpResponse.json(file()); - }), - ], - }, - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 2e1e92cbdf..81f3936600 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -1,5 +1,5 @@ @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/components/MkCwButton.stories.impl.ts b/packages/frontend/src/components/MkCwButton.stories.impl.ts deleted file mode 100644 index 5d6ea56da9..0000000000 --- a/packages/frontend/src/components/MkCwButton.stories.impl.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable import/no-default-export */ -import { StoryObj } from '@storybook/vue3'; -import { action } from '@storybook/addon-actions'; -import { expect, userEvent, within } from '@storybook/test'; -import { file } from '../../.storybook/fakes.js'; -import MkCwButton from './MkCwButton.vue'; -import { i18n } from '@/i18n.js'; - -export const Default = { - render(args) { - return { - components: { - MkCwButton, - }, - data() { - return { - showContent: false, - }; - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - 'update:modelValue': action('update:modelValue'), - }; - }, - }, - template: '', - }; - }, - args: { - text: 'Some CW content', - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - const buttonElement = canvas.getByRole('button'); - await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show); - await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 })); - await userEvent.click(buttonElement); - await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide); - await userEvent.click(buttonElement); - }, - parameters: { - chromatic: { - // NOTE: テストが終わるまで待つ - delay: 5000, - }, - layout: 'centered', - }, -} satisfies StoryObj; -export const IncludesTextAndDriveFile = { - ...Default, - args: { - text: 'Some CW content', - files: [file()], - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - const buttonElement = canvas.getByRole('button'); - await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 })); - await expect(buttonElement).toHaveTextContent(' / '); - await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.files({ count: 1 })); - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index b5f6e78b6c..0cdaf7c9bd 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -1,5 +1,5 @@ @@ -10,17 +10,13 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkDialog.stories.impl.ts b/packages/frontend/src/components/MkDialog.stories.impl.ts deleted file mode 100644 index 2d8d3661f2..0000000000 --- a/packages/frontend/src/components/MkDialog.stories.impl.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { action } from '@storybook/addon-actions'; -import { expect, userEvent, waitFor, within } from '@storybook/test'; -import { StoryObj } from '@storybook/vue3'; -import { i18n } from '@/i18n.js'; -import MkDialog from './MkDialog.vue'; -const Base = { - render(args) { - return { - components: { - MkDialog, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - done: action('done'), - closed: action('closed'), - }; - }, - }, - template: '', - }; - }, - args: { - text: 'Hello, world!', - }, - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; -export const Success = { - ...Base, - args: { - ...Base.args, - type: 'success', - }, -} satisfies StoryObj; -export const Error = { - ...Base, - args: { - ...Base.args, - type: 'error', - }, -} satisfies StoryObj; -export const Warning = { - ...Base, - args: { - ...Base.args, - type: 'warning', - }, -} satisfies StoryObj; -export const Info = { - ...Base, - args: { - ...Base.args, - type: 'info', - }, -} satisfies StoryObj; -export const Question = { - ...Base, - args: { - ...Base.args, - type: 'question', - }, -} satisfies StoryObj; -export const Waiting = { - ...Base, - args: { - ...Base.args, - type: 'waiting', - }, -} satisfies StoryObj; -export const DialogWithActions = { - ...Question, - args: { - ...Question.args, - text: i18n.ts.areYouSure, - actions: [ - { - text: i18n.ts.yes, - primary: true, - callback() { - action('YES')(); - }, - }, - { - text: i18n.ts.no, - callback() { - action('NO')(); - }, - }, - ], - }, -} satisfies StoryObj; -export const DialogWithDangerActions = { - ...Warning, - args: { - ...Warning.args, - text: i18n.ts.resetAreYouSure, - actions: [ - { - text: i18n.ts.yes, - danger: true, - primary: true, - callback() { - action('YES')(); - }, - }, - { - text: i18n.ts.no, - callback() { - action('NO')(); - }, - }, - ], - }, -} satisfies StoryObj; -export const DialogWithInput = { - ...Question, - args: { - ...Question.args, - title: 'Hello, world!', - text: undefined, - input: { - placeholder: i18n.ts.inputMessageHere, - type: 'text', - default: null, - minLength: 2, - maxLength: 3, - }, - }, - async play({ canvasElement }) { - const canvas = within(canvasElement); - await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 0, min: 2 })); - const okButton = canvas.getByRole('button', { name: i18n.ts.ok }); - await expect(okButton).toBeDisabled(); - const input = canvas.getByRole('combobox'); - await waitFor(() => userEvent.hover(input)); - await waitFor(() => userEvent.click(input)); - await waitFor(() => userEvent.type(input, 'M')); - await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 1, min: 2 })); - await waitFor(() => userEvent.type(input, 'i')); - await expect(okButton).toBeEnabled(); - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 7dc381b662..e0692eb383 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -1,10 +1,10 @@ diff --git a/packages/frontend/src/components/MkDonation.stories.impl.ts b/packages/frontend/src/components/MkDonation.stories.impl.ts deleted file mode 100644 index 27d6b7df6c..0000000000 --- a/packages/frontend/src/components/MkDonation.stories.impl.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { action } from '@storybook/addon-actions'; -import { StoryObj } from '@storybook/vue3'; -import { onBeforeUnmount } from 'vue'; -import MkDonation from './MkDonation.vue'; -import { instance } from '@/instance.js'; -export const Default = { - render(args) { - return { - components: { - MkDonation, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - events() { - return { - closed: action('closed'), - }; - }, - }, - template: '', - }; - }, - args: { - // @ts-expect-error name is used for mocking instance - name: 'Misskey Hub', - }, - decorators: [ - (_, { args }) => ({ - setup() { - // @ts-expect-error name is used for mocking instance - instance.name = args.name; - onBeforeUnmount(() => instance.name = null); - }, - template: '', - }), - ], - parameters: { - layout: 'centered', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue index 1dfdebf0d4..a77ff42f94 100644 --- a/packages/frontend/src/components/MkDonation.vue +++ b/packages/frontend/src/components/MkDonation.vue @@ -1,5 +1,5 @@ @@ -23,17 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.learnMore }} -
-
-
- - - -
- {{ i18n.ts.learnMore }} + {{ i18n.ts.learnMore }}
@@ -41,14 +31,14 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.neverShow }}
- + diff --git a/packages/frontend/src/components/MkDriveWindow.stories.impl.ts b/packages/frontend/src/components/MkDriveWindow.stories.impl.ts deleted file mode 100644 index faa1f7fd5f..0000000000 --- a/packages/frontend/src/components/MkDriveWindow.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkDriveWindow from './MkDriveWindow.vue'; -void MkDriveWindow; diff --git a/packages/frontend/src/components/MkDriveWindow.vue b/packages/frontend/src/components/MkDriveWindow.vue index c0142ec76e..72aa79b153 100644 --- a/packages/frontend/src/components/MkDriveWindow.vue +++ b/packages/frontend/src/components/MkDriveWindow.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue deleted file mode 100644 index c060c3a659..0000000000 --- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue +++ /dev/null @@ -1,414 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts b/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts deleted file mode 100644 index 69aef577de..0000000000 --- a/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import MkEmojiPicker_section from './MkEmojiPicker.section.vue'; -void MkEmojiPicker_section; diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 151843b18c..00e0a0e042 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -1,14 +1,13 @@ -
- @@ -79,24 +68,19 @@ import MkSelect from './MkSelect.vue'; import MkRange from './MkRange.vue'; import MkButton from './MkButton.vue'; import MkRadios from './MkRadios.vue'; -import XFile from './MkFormDialog.file.vue'; -import type { Form } from '@/scripts/form.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; -import { infoImageUrl } from '@/instance.js'; const props = defineProps<{ title: string; - form: Form; + form: any; }>(); const emit = defineEmits<{ (ev: 'done', v: { - canceled: true; - } | { - result: Record; + canceled?: boolean; + result?: any; }): void; - (ev: 'closed'): void; }>(); const dialog = shallowRef>(); @@ -110,13 +94,13 @@ function ok() { emit('done', { result: values, }); - dialog.value?.close(); + dialog.value.close(); } function cancel() { emit('done', { canceled: true, }); - dialog.value?.close(); + dialog.value.close(); } diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue deleted file mode 100644 index 1e88d59d8e..0000000000 --- a/packages/frontend/src/components/MkFormFooter.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts index a433ad680b..29e27e1373 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts +++ b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts @@ -1,12 +1,13 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { expect, userEvent, waitFor, within } from '@storybook/test'; +import { expect } from '@storybook/jest'; +import { userEvent, waitFor, within } from '@storybook/testing-library'; import { StoryObj } from '@storybook/vue3'; -import { galleryPost } from '../../.storybook/fakes.js'; +import { galleryPost } from '../../.storybook/fakes'; import MkGalleryPostPreview from './MkGalleryPostPreview.vue'; export const Default = { render(args) { diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue index 2bb5b8762a..316632b1a6 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.vue +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -1,5 +1,5 @@ @@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only leaveActiveClass: $style.transition_toggle_leaveActive, leaveToClass: $style.transition_toggle_leaveTo, }" - :src="post.files?.[0]?.thumbnailUrl" - :hash="post.files?.[0]?.blurhash" + :src="post.files[0].thumbnailUrl" + :hash="post.files[0].blurhash" :forceBlurhash="!show" /> @@ -83,7 +83,7 @@ function leaveHover(): void { > article { > footer { - &::before { + &:before { opacity: 1; } } @@ -139,7 +139,7 @@ function leaveHover(): void { text-shadow: 0 0 8px #000; background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); - &::before { + &:before { content: ""; display: block; position: absolute; diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue index 54c7585f18..185a49b5a9 100644 --- a/packages/frontend/src/components/MkGoogle.vue +++ b/packages/frontend/src/components/MkGoogle.vue @@ -1,19 +1,18 @@ diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue index 0cc0df9911..0022531e58 100644 --- a/packages/frontend/src/components/MkHeatmap.vue +++ b/packages/frontend/src/components/MkHeatmap.vue @@ -1,5 +1,5 @@ @@ -13,10 +13,9 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index c04d0864fb..4fb573fdbc 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -1,5 +1,5 @@ @@ -14,17 +14,18 @@ SPDX-License-Identifier: AGPL-3.0-only :enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined" :leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined" > - - + +
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index 46d42248d3..f0650e48f1 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -1,5 +1,5 @@ @@ -11,16 +11,16 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -101,8 +101,6 @@ function close() { vertical-align: bottom; height: 100px; border-radius: var(--radius); - padding: 10px; - box-sizing: border-box; &:hover { color: var(--accent); @@ -119,7 +117,6 @@ function close() { margin-top: 12px; font-size: 0.8em; line-height: 1.5em; - text-align: center; } > .indicatorWithValue { @@ -139,7 +136,7 @@ function close() { left: 32px; color: var(--indicator); font-size: 8px; - animation: global-blink 1s infinite; + animation: blink 1s infinite; @media (max-width: 500px) { top: 16px; diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index 263cd95eb1..114b9b4faf 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -1,34 +1,27 @@ diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 77a86ff2fb..42a709ae26 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -1,53 +1,54 @@ diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 0502bdd401..33a9b0fbf9 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -1,505 +1,64 @@ diff --git a/packages/frontend/src/components/SkMfmWindow.vue b/packages/frontend/src/components/MkMfmWindow.vue similarity index 74% rename from packages/frontend/src/components/SkMfmWindow.vue rename to packages/frontend/src/components/MkMfmWindow.vue index c599531ec5..314808dcf6 100644 --- a/packages/frontend/src/components/SkMfmWindow.vue +++ b/packages/frontend/src/components/MkMfmWindow.vue @@ -1,8 +1,3 @@ - - - +
{{ i18n.ts._mfm.intro }}
-
+
{{ i18n.ts._mfm.mention }}

{{ i18n.ts._mfm.mentionDescription }}

- +
@@ -34,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.hashtagDescription }}

- +
@@ -44,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.linkDescription }}

- +
@@ -54,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.emojiDescription }}

- +
@@ -64,7 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.boldDescription }}

- +
@@ -74,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.smallDescription }}

- +
@@ -84,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.quoteDescription }}

- +
@@ -94,7 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.centerDescription }}

- +
@@ -104,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.inlineCodeDescription }}

- +
@@ -114,7 +109,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.blockCodeDescription }}

- +
@@ -124,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.inlineMathDescription }}

- +
@@ -134,7 +129,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.blockMathDescription }}

- +
@@ -144,7 +139,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.searchDescription }}

- +
@@ -154,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.flipDescription }}

- +
@@ -164,7 +159,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.fontDescription }}

- +
@@ -174,7 +169,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.x2Description }}

- +
@@ -184,7 +179,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.x3Description }}

- +
@@ -194,7 +189,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.x4Description }}

- +
@@ -204,7 +199,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.blurDescription }}

- +
@@ -214,7 +209,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.jellyDescription }}

- +
@@ -224,7 +219,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.tadaDescription }}

- +
@@ -234,7 +229,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.jumpDescription }}

- +
@@ -244,7 +239,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.bounceDescription }}

- +
@@ -254,7 +249,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.spinDescription }}

- +
@@ -264,7 +259,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.shakeDescription }}

- +
@@ -274,7 +269,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.twitchDescription }}

- +
@@ -284,7 +279,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.rainbowDescription }}

- +
@@ -294,7 +289,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.sparkleDescription }}

- + MFM
@@ -304,69 +299,37 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.rotateDescription }}

- + MFM
-
-
{{ i18n.ts._mfm.crop }}
-
-

{{ i18n.ts._mfm.cropDescription }}

-
- - MFM -
-
-
{{ i18n.ts._mfm.position }}

{{ i18n.ts._mfm.positionDescription }}

- + MFM
-
-
{{ i18n.ts._mfm.followMouse }}
- {{ i18n.ts._mfm.uncommonFeature }} -
-
-

{{ i18n.ts._mfm.followMouseDescription }}

-
- - MFM -
-
-
{{ i18n.ts._mfm.scale }}

{{ i18n.ts._mfm.scaleDescription }}

- + MFM
-
-
{{ i18n.ts._mfm.fade }}
-
-

{{ i18n.ts._mfm.fadeDescription }}

-
- - MFM -
-
-
{{ i18n.ts._mfm.foreground }}

{{ i18n.ts._mfm.foregroundDescription }}

- + MFM
@@ -376,7 +339,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.backgroundDescription }}

- + MFM
@@ -386,7 +349,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._mfm.plainDescription }}

- + MFM
@@ -399,19 +362,18 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index c766a33823..ec5039c504 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -1,5 +1,5 @@ @@ -30,9 +30,9 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.transition_modal_leaveTo]: transitionName === 'modal', [$style.transition_send_leaveTo]: transitionName === 'send', })" - :duration="transitionDuration" appear @afterLeave="onClosed" @enter="emit('opening')" @afterEnter="onOpened" + :duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened" > -
+
@@ -42,14 +42,11 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -65,14 +53,6 @@ const props = defineProps<{ min-width: 0; } -.cw { - cursor: default; - display: block; - margin: 0; - padding: 0; - overflow-wrap: break-word; -} - .header { margin-bottom: 2px; font-weight: bold; diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index 542e3e79ea..b1d4ed3f7e 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -1,5 +1,5 @@ @@ -10,11 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only

- - + +

- +
@@ -22,12 +22,12 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 45276839ad..3e33c7aa69 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -1,10 +1,10 @@ diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index f90fcfef33..b2597d090b 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -1,5 +1,5 @@ @@ -7,17 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -31,9 +24,7 @@ import { defineAsyncComponent, inject } from 'vue'; import * as Misskey from 'misskey-js'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -64,30 +55,13 @@ function detachMedia(id: string) { } } -async function detachAndDeleteMedia(file: Misskey.entities.DriveFile) { - if (mock) return; - - detachMedia(file.id); - - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }), - }); - - if (canceled) return; - - os.apiWithDialog('drive/files/delete', { - fileId: file.id, - }); -} - function toggleSensitive(file) { if (mock) { emit('changeSensitive', file, !file.isSensitive); return; } - misskeyApi('drive/files/update', { + os.api('drive/files/update', { fileId: file.id, isSensitive: !file.isSensitive, }).then(() => { @@ -101,10 +75,10 @@ async function rename(file) { const { canceled, result } = await os.inputText({ title: i18n.ts.enterFileName, default: file.name, - minLength: 1, + allowEmpty: false, }); if (canceled) return; - misskeyApi('drive/files/update', { + os.api('drive/files/update', { fileId: file.id, name: result, }).then(() => { @@ -116,21 +90,20 @@ async function rename(file) { async function describe(file) { if (mock) return; - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { + os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { default: file.comment !== null ? file.comment : '', file: file, }, { done: caption => { let comment = caption.length === 0 ? null : caption; - misskeyApi('drive/files/update', { + os.api('drive/files/update', { fileId: file.id, comment: comment, }).then(() => { file.comment = comment; }); }, - closed: () => dispose(), - }); + }, 'closed'); } async function crop(file: Misskey.entities.DriveFile): Promise { @@ -140,49 +113,31 @@ async function crop(file: Misskey.entities.DriveFile): Promise { emit('replaceFile', file, newFile); } -function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | KeyboardEvent): void { +function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void { if (menuShowing) return; const isImage = file.type.startsWith('image/'); - - const menuItems: MenuItem[] = []; - - menuItems.push({ + os.popupMenu([{ text: i18n.ts.renameFile, - icon: 'ti ti-forms', + icon: 'ph-textbox ph-bold ph-lg', action: () => { rename(file); }, }, { text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, - icon: file.isSensitive ? 'ti ti-eye-exclamation' : 'ti ti-eye', + icon: file.isSensitive ? 'ph-eye-closed ph-bold ph-lg' : 'ph-eye ph-bold ph-lg', action: () => { toggleSensitive(file); }, }, { text: i18n.ts.describeFile, - icon: 'ti ti-text-caption', + icon: 'ph-text-indent ph-bold ph-lg', action: () => { describe(file); }, - }); - - if (isImage) { - menuItems.push({ - text: i18n.ts.cropImage, - icon: 'ti ti-crop', - action: () : void => { crop(file); }, - }); - } - - menuItems.push({ - type: 'divider', - }, { + }, ...isImage ? [{ + text: i18n.ts.cropImage, + icon: 'ph-crop ph-bold ph-lg', + action: () : void => { crop(file); }, + }] : [], { text: i18n.ts.attachCancel, - icon: 'ti ti-circle-x', + icon: 'ph-x-circle ph-bold ph-lg', action: () => { detachMedia(file.id); }, - }, { - text: i18n.ts.deleteFile, - icon: 'ti ti-trash', - danger: true, - action: () => { detachAndDeleteMedia(file); }, - }); - - os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => menuShowing = false); + }], ev.currentTarget ?? ev.target).then(() => menuShowing = false); menuShowing = true; } @@ -206,10 +161,6 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar border-radius: var(--radius-xs); overflow: hidden; cursor: move; - - &:focus-visible { - outline-offset: 4px; - } } .thumbnail { diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index 811a6378f2..25a8788a38 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -1,64 +1,53 @@ diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue deleted file mode 100644 index 6efd99d14b..0000000000 --- a/packages/frontend/src/components/MkPreview.vue +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index 4fb4c6fe56..9f50f7ad5d 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -1,5 +1,5 @@ @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -23,10 +23,10 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue index e02f76a58f..f22774ef97 100644 --- a/packages/frontend/src/components/MkRadio.vue +++ b/packages/frontend/src/components/MkRadio.vue @@ -1,5 +1,5 @@ @@ -9,7 +9,6 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]" :aria-checked="checked" :aria-disabled="disabled" - role="checkbox" @click="toggle" > diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 957ee0e76b..bab592317d 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -1,5 +1,5 @@ @@ -10,9 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only class="_button" :class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]" @click="toggleReaction()" - @contextmenu.prevent.stop="menu" > - + {{ count }} @@ -20,21 +19,15 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index d48780e9de..05cef6ed3b 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -1,27 +1,26 @@ diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 4c55831a3a..389acb82bc 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -1,65 +1,64 @@ - - diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue index aac07008a4..eeb9325a29 100644 --- a/packages/frontend/src/components/MkTooltip.vue +++ b/packages/frontend/src/components/MkTooltip.vue @@ -1,5 +1,5 @@ @@ -13,10 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only >
- + + {{ text }}
@@ -55,7 +53,6 @@ const el = shallowRef(); const zIndex = os.claimZIndex('high'); function setPosition() { - if (el.value == null) return; const data = calcPopupPosition(el.value, { anchorElement: props.targetElement, direction: props.direction, diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue index cec7d69943..3fca958055 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Note.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue @@ -1,5 +1,5 @@ @@ -8,28 +8,17 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._initialTutorial._note.description }}
-
{{ i18n.ts.reply }} … {{ i18n.ts._initialTutorial._note.reply }}
-
{{ i18n.ts.renote }} … {{ i18n.ts._initialTutorial._note.renote }}
+
{{ i18n.ts.reply }} … {{ i18n.ts._initialTutorial._note.reply }}
+
{{ i18n.ts.renote }} … {{ i18n.ts._initialTutorial._note.renote }}
{{ i18n.ts.reaction }} … {{ i18n.ts._initialTutorial._note.reaction }}
-
{{ i18n.ts.menu }} … {{ i18n.ts._initialTutorial._note.menu }}
+
{{ i18n.ts.menu }} … {{ i18n.ts._initialTutorial._note.menu }}
{{ i18n.ts._initialTutorial._reaction.description }}
- - - - -
- {{ i18n.ts._initialTutorial.wellDone }} {{ i18n.ts._initialTutorial._reaction.reactNotification }}
- - - -
+
{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}
+ +
{{ i18n.ts._initialTutorial.wellDone }} {{ i18n.ts._initialTutorial._reaction.reactNotification }}
{{ i18n.ts._initialTutorial._reaction.reactDone }}
@@ -64,7 +53,7 @@ const exampleNote = reactive({ isBot: false, isCat: true, emojis: {}, - onlineStatus: 'unknown', + onlineStatus: null, badgeRoles: [], }, text: 'just setting up my shonk', @@ -74,7 +63,6 @@ const exampleNote = reactive({ reactionAcceptance: null, renoteCount: 0, repliesCount: 1, - reactionCount: 0, reactions: {}, reactionEmojis: {}, fileIds: [], @@ -98,6 +86,7 @@ function doNotification(emoji: string): void { const notification: Misskey.entities.Notification = { id: Math.random().toString(), createdAt: new Date().toUTCString(), + isRead: false, type: 'reaction', reaction: emoji, user: $i, diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue index a9014d4202..f093d6d9ef 100644 --- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue +++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue @@ -1,5 +1,5 @@ @@ -11,16 +11,16 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._initialTutorial._postNote._visibility.description }}
-
{{ i18n.ts._visibility.public }} … {{ i18n.ts._initialTutorial._postNote._visibility.public }}
-
{{ i18n.ts._visibility.home }} … {{ i18n.ts._initialTutorial._postNote._visibility.home }}
-
{{ i18n.ts._visibility.followers }} … {{ i18n.ts._initialTutorial._postNote._visibility.followers }}
+
{{ i18n.ts._visibility.public }} … {{ i18n.ts._initialTutorial._postNote._visibility.public }}
+
{{ i18n.ts._visibility.home }} … {{ i18n.ts._initialTutorial._postNote._visibility.home }}
+
{{ i18n.ts._visibility.followers }} … {{ i18n.ts._initialTutorial._postNote._visibility.followers }}
-
{{ i18n.ts._visibility.specified }} … {{ i18n.ts._initialTutorial._postNote._visibility.direct }}
+
{{ i18n.ts._visibility.specified }} … {{ i18n.ts._initialTutorial._postNote._visibility.direct }}
{{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect1 }} {{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect2 }}
-
{{ i18n.ts._visibility.disableFederation }} … {{ i18n.ts._initialTutorial._postNote._visibility.localOnly }}
+
{{ i18n.ts._visibility.disableFederation }} … {{ i18n.ts._initialTutorial._postNote._visibility.localOnly }}
@@ -58,7 +58,7 @@ const exampleCWNote = reactive({ isBot: false, isCat: true, emojis: {}, - onlineStatus: 'unknown', + onlineStatus: null, badgeRoles: [], }, text: i18n.ts._initialTutorial._postNote._cw._exampleNote.note, @@ -68,7 +68,6 @@ const exampleCWNote = reactive({ reactionAcceptance: null, renoteCount: 0, repliesCount: 1, - reactionCount: 0, reactions: {}, reactionEmojis: {}, fileIds: [], @@ -105,7 +104,7 @@ const exampleCWNote = reactive({ font-weight: bold; text-align: left; - &::before { + &:before { content: ""; display: block; width: calc(100% - 38px); diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue index 322082f5a0..dd255a2214 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue @@ -1,5 +1,5 @@ @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only :initialNote="exampleNote" @fileChangeSensitive="doSucceeded" > -
{{ i18n.ts._initialTutorial.wellDone }} {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}
+
{{ i18n.ts._initialTutorial.wellDone }} {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}
@@ -40,7 +40,7 @@ const emit = defineEmits<{ const onceSucceeded = ref(false); function doSucceeded(fileId: string, to: boolean) { - if (fileId === exampleNote.fileIds?.[0] && to) { + if (fileId === exampleNote.fileIds[0] && to) { onceSucceeded.value = true; emit('succeeded'); } @@ -58,7 +58,6 @@ const exampleNote = reactive({ reactionAcceptance: null, renoteCount: 0, repliesCount: 1, - reactionCount: 0, reactions: {}, reactionEmojis: {}, fileIds: ['0000000002'], @@ -115,7 +114,7 @@ const exampleNote = reactive({ font-weight: bold; text-align: left; - &::before { + &:before { content: ""; display: block; width: calc(100% - 38px); diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue index b900a30c85..421c0a8af8 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue @@ -1,5 +1,5 @@ @@ -7,9 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._initialTutorial._timeline.description1 }}
-
- {{ i18n.ts._timelines[tl] }} … {{ i18n.ts._initialTutorial._timeline[tl] }} -
+
{{ i18n.ts._timelines.home }} … {{ i18n.ts._initialTutorial._timeline.home }}
+
{{ i18n.ts._timelines.local }} … {{ i18n.ts._initialTutorial._timeline.local }}
+
{{ i18n.ts._timelines.social }} … {{ i18n.ts._initialTutorial._timeline.social }}
+
{{ i18n.ts._timelines.global }} … {{ i18n.ts._initialTutorial._timeline.global }}
{{ i18n.ts._initialTutorial._timeline.description2 }}
@@ -18,15 +19,15 @@ SPDX-License-Identifier: AGPL-3.0-only
+
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index 3c5f563aa0..42ccb621b6 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -1,5 +1,5 @@ @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -24,10 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - + + + + @@ -39,24 +39,23 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.needConfirmationToRead }} - {{ i18n.ts.delete }} + {{ i18n.ts.delete }}
- {{ props.announcement ? i18n.ts.update : i18n.ts.create }} + {{ props.announcement ? i18n.ts.update : i18n.ts.create }}
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue index 603f9f2435..978c5005c8 100644 --- a/packages/frontend/src/components/MkUserCardMini.vue +++ b/packages/frontend/src/components/MkUserCardMini.vue @@ -1,24 +1,24 @@ diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index 73cdd9ce00..322ffee38e 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -1,28 +1,20 @@ @@ -48,9 +40,7 @@ import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; -import { getStaticImageUrl } from '@/scripts/media-proxy.js'; -import { defaultStore } from '@/store.js'; +import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; defineProps<{ user: Misskey.entities.UserDetailed; @@ -75,8 +65,8 @@ defineProps<{ top: 62px; left: 13px; z-index: 2; - width: var(--avatar); - height: var(--avatar); + width: 58px; + height: 58px; border: solid 4px var(--panel); } @@ -152,30 +142,4 @@ defineProps<{ top: 8px; right: 8px; } - -.infoBadges { - position: absolute; - top: 12px; - left: 12px; - - display: flex; - flex-direction: row; - - padding: 0; - margin: 0; - - > * { - padding: 4px 8px; - color: #fff; - background: rgba(0, 0, 0, 0.7); - font-size: 0.7em; - border-radius: var(--radius-sm); - list-style-type: none; - margin-left: 0; - } - - > :not(:first-child) { - margin-left: 8px; - } -} diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue index ac82ecc3d6..56a61dce23 100644 --- a/packages/frontend/src/components/MkUserList.vue +++ b/packages/frontend/src/components/MkUserList.vue @@ -1,10 +1,10 @@ @@ -164,7 +133,7 @@ onMounted(() => { diff --git a/packages/frontend/src/components/SkFollowingRecentNotes.vue b/packages/frontend/src/components/SkFollowingRecentNotes.vue deleted file mode 100644 index 6daa8feba5..0000000000 --- a/packages/frontend/src/components/SkFollowingRecentNotes.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/SkFormula.vue b/packages/frontend/src/components/SkFormula.vue deleted file mode 100644 index 6faf36da55..0000000000 --- a/packages/frontend/src/components/SkFormula.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/SkInstanceTicker.vue b/packages/frontend/src/components/SkInstanceTicker.vue deleted file mode 100644 index eb987d9c77..0000000000 --- a/packages/frontend/src/components/SkInstanceTicker.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/SkModPlayer.vue b/packages/frontend/src/components/SkModPlayer.vue deleted file mode 100644 index 6ba983645e..0000000000 --- a/packages/frontend/src/components/SkModPlayer.vue +++ /dev/null @@ -1,697 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue deleted file mode 100644 index bda5282144..0000000000 --- a/packages/frontend/src/components/SkNote.vue +++ /dev/null @@ -1,1404 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue deleted file mode 100644 index 7907da6c94..0000000000 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ /dev/null @@ -1,1245 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/SkNoteHeader.vue b/packages/frontend/src/components/SkNoteHeader.vue deleted file mode 100644 index 45218fafb6..0000000000 --- a/packages/frontend/src/components/SkNoteHeader.vue +++ /dev/null @@ -1,266 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/SkNoteSimple.vue b/packages/frontend/src/components/SkNoteSimple.vue deleted file mode 100644 index b31e337a99..0000000000 --- a/packages/frontend/src/components/SkNoteSimple.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue deleted file mode 100644 index fac35191b9..0000000000 --- a/packages/frontend/src/components/SkNoteSub.vue +++ /dev/null @@ -1,674 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/components/SkOldNoteWindow.vue b/packages/frontend/src/components/SkOldNoteWindow.vue index 3810b62366..63d9f4682a 100644 --- a/packages/frontend/src/components/SkOldNoteWindow.vue +++ b/packages/frontend/src/components/SkOldNoteWindow.vue @@ -1,8 +1,3 @@ - - - - - - - - + + {{ i18n.ts.publishing }} +
- + @@ -65,19 +62,19 @@ SPDX-License-Identifier: AGPL-3.0-only -
- +
+ {{ i18n.ts.save }} - - {{ i18n.ts.remove }} + + {{ i18n.ts.remove }}
- - {{ i18n.ts.more }} + + {{ i18n.ts.more }}
@@ -85,55 +82,43 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index fd37311b21..77649c6c4a 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -1,5 +1,5 @@ @@ -11,114 +11,87 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }} {{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }} - - - - - + + + + - - - +
+ + + {{ i18n.ts.more }} +
diff --git a/packages/frontend/src/pages/admin/approvals.vue b/packages/frontend/src/pages/admin/approvals.vue index 0497e85da3..5d9c9de03b 100644 --- a/packages/frontend/src/pages/admin/approvals.vue +++ b/packages/frontend/src/pages/admin/approvals.vue @@ -1,15 +1,10 @@ - - - - -
raw @@ -193,7 +127,9 @@ SPDX-License-Identifier: AGPL-3.0-only import * as Misskey from 'misskey-js'; import { CodeDiff } from 'v-code-diff'; import JSON5 from 'json5'; +import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; +import { dateString } from '@/filters/date.js'; import MkFolder from '@/components/MkFolder.vue'; const props = defineProps<{ @@ -202,6 +138,9 @@ const props = defineProps<{ + diff --git a/packages/frontend/src/pages/admin/overview.heatmap.vue b/packages/frontend/src/pages/admin/overview.heatmap.vue index 7b2b142b16..4d09f183bf 100644 --- a/packages/frontend/src/pages/admin/overview.heatmap.vue +++ b/packages/frontend/src/pages/admin/overview.heatmap.vue @@ -1,5 +1,5 @@ @@ -17,11 +17,10 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin/system-webhook.item.vue b/packages/frontend/src/pages/admin/system-webhook.item.vue deleted file mode 100644 index 4e767fba16..0000000000 --- a/packages/frontend/src/pages/admin/system-webhook.item.vue +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/pages/admin/system-webhook.vue b/packages/frontend/src/pages/admin/system-webhook.vue deleted file mode 100644 index c59abda24a..0000000000 --- a/packages/frontend/src/pages/admin/system-webhook.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index e99dcfa489..7f809ed9cc 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -1,5 +1,5 @@ @@ -34,17 +34,17 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - +
- +
@@ -58,40 +58,40 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index a9bcfee803..51e1efda53 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -1,5 +1,5 @@ @@ -7,53 +7,44 @@ SPDX-License-Identifier: AGPL-3.0-only - -
- {{ i18n.ts.youHaveUnreadAnnouncements }} - -
-
{{ i18n.ts.forYou }}
-
- 🆕 - - - - - - - {{ announcement.title }} +
+ {{ i18n.ts.youHaveUnreadAnnouncements }} + +
+
{{ i18n.ts.forYou }}
+
+ 🆕 + + + + + + + {{ announcement.title }} +
+
+ + +
+
-
- - - -
- {{ i18n.ts.createdAt }}: -
-
- {{ i18n.ts.updatedAt }}: -
-
-
-
- {{ i18n.ts.gotIt }} -
-
-
-
- +
+
+ {{ i18n.ts.gotIt }} +
+
+
+
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index d3f4a65b89..acff4211d1 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -1,5 +1,5 @@ @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -29,10 +29,10 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts._channel.setBanner }} + {{ i18n.ts._channel.setBanner }}
- {{ i18n.ts._channel.removeBanner }} + {{ i18n.ts._channel.removeBanner }}
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + @@ -60,8 +60,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ channelId ? i18n.ts.save : i18n.ts.create }} - {{ i18n.ts.archive }} + {{ channelId ? i18n.ts.save : i18n.ts.create }} + {{ i18n.ts.archive }}
@@ -70,19 +70,17 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 790e16e471..84463db0e1 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -1,5 +1,5 @@ @@ -7,61 +7,59 @@ SPDX-License-Identifier: AGPL-3.0-only - -
-
- - - -
-
-
-
-
-
{{ i18n.ts.sensitive }}
-
-
-
- +
+
+ + + +
+
+
+
+
{{ i18n.ts.sensitive }}
+
- - - -
- -
-
-
-
- {{ i18n.ts.thisChannelArchived }} - - - - - -
-
- -
-
-
-
- - - - {{ i18n.ts.search }} -
- +
+
- + + + +
+ +
+
+
+
+ {{ i18n.ts.thisChannelArchived }} + + + + + +
+
+ +
+
+
+
+ + + + {{ i18n.ts.search }} +
+ +
+
diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue deleted file mode 100644 index 54352c9b0d..0000000000 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index c5f0dde878..b4ae855d5d 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -1,22 +1,20 @@ @@ -239,12 +233,10 @@ async function del() { .footer { position: sticky; - z-index: 10000; bottom: 0; left: 0; padding: 12px; border-top: solid 0.5px var(--divider); - background: var(--acrylicBg); -webkit-backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px)); } diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index 03a3b8f1c0..f787b12097 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -1,5 +1,5 @@ @@ -14,15 +14,18 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue index 389cd23ad2..995ccd777c 100644 --- a/packages/frontend/src/pages/explore.roles.vue +++ b/packages/frontend/src/pages/explore.roles.vue @@ -1,5 +1,5 @@ @@ -12,15 +12,14 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index 805d826946..f7215def62 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -1,5 +1,5 @@ @@ -12,11 +12,11 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
{{ tag.tag }} @@ -40,13 +40,13 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -63,28 +63,27 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue index b1a8183d9b..8b5af28b12 100644 --- a/packages/frontend/src/pages/explore.vue +++ b/packages/frontend/src/pages/explore.vue @@ -1,32 +1,31 @@ diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue index cb1feef016..10f4a96a98 100644 --- a/packages/frontend/src/pages/favorites.vue +++ b/packages/frontend/src/pages/favorites.vue @@ -1,5 +1,5 @@ @@ -27,28 +27,21 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index f63a799365..0b4dd13911 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -1,5 +1,5 @@ @@ -7,51 +7,48 @@ SPDX-License-Identifier: AGPL-3.0-only - -
- +
+ +
+ +
+
+
+ +
+
+ +
+
-
-
- - -
- -
-
+
+ +
+
-
- -
- -
- -
-
-
- + +
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index 1229fcfd4e..fd03ac6579 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -1,5 +1,5 @@ @@ -15,46 +15,40 @@ SPDX-License-Identifier: AGPL-3.0-only
-
- -
-
- {{ flash.likedCount }} - {{ flash.likedCount }} - - - -
+ {{ flash.likedCount }} + {{ flash.likedCount }} + +
{{ flash.title }}
-
+
{{ flash.summary }}
Play
- {{ flash.likedCount }} + {{ flash.likedCount }}
- + - +
-
{{ i18n.ts.updatedAt }}:
-
{{ i18n.ts.createdAt }}:
+
{{ i18n.ts.updatedAt }}:
+
{{ i18n.ts.createdAt }}:
{{ i18n.ts._play.editThisPage }}
- + @@ -62,119 +56,73 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index a68a7e5c41..5daaca774c 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -1,5 +1,5 @@ @@ -19,18 +19,18 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ file.name }}
- +
- {{ i18n.ts.attachFile }} + {{ i18n.ts.attachFile }}
{{ i18n.ts.markAsSensitive }}
- {{ i18n.ts.save }} - {{ i18n.ts.publish }} + {{ i18n.ts.save }} + {{ i18n.ts.publish }} - {{ i18n.ts.delete }} + {{ i18n.ts.delete }}
@@ -38,8 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index e0b137ed28..f65380eee1 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -1,5 +1,5 @@ @@ -7,10 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only - -
+
+
- +
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -26,35 +26,34 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
-
- {{ i18n.ts.postToGallery }} +
+ {{ i18n.ts.postToGallery }}
- +
diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extentions.vue similarity index 93% rename from packages/frontend/src/pages/install-extensions.vue rename to packages/frontend/src/pages/install-extentions.vue index 4bee437f65..7e6c75ac99 100644 --- a/packages/frontend/src/pages/install-extensions.vue +++ b/packages/frontend/src/pages/install-extentions.vue @@ -1,5 +1,5 @@ @@ -10,9 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - + + +

{{ i18n.ts._externalResourceInstaller[`_${data.type}`].title }}

{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}
@@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -69,18 +69,18 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.install }} + {{ i18n.ts.install }}
- +

{{ errorKV?.title }}

{{ errorKV?.description }}
@@ -105,7 +105,6 @@ import MkInfo from '@/components/MkInfo.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js'; import { parseThemeCode, installTheme } from '@/scripts/install-theme.js'; import { unisonReload } from '@/scripts/unison-reload.js'; @@ -160,7 +159,7 @@ async function fetch() { uiPhase.value = 'error'; return; } - const res = await misskeyApi('fetch-external-resources', { + const res = await os.api('fetch-external-resources', { url: url.value, hash: hash.value, }).catch((err) => { @@ -312,10 +311,10 @@ const headerActions = computed(() => []); const headerTabs = computed(() => []); -definePageMetadata(() => ({ +definePageMetadata({ title: i18n.ts._externalResourceInstaller.title, - icon: 'ti ti-download', -})); + icon: 'ph-download ph-bold ph-lg', +}); diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue index ef485a9446..a5bdc29dbb 100644 --- a/packages/frontend/src/pages/invite.vue +++ b/packages/frontend/src/pages/invite.vue @@ -1,5 +1,5 @@ @@ -12,21 +12,21 @@ SPDX-License-Identifier: AGPL-3.0-only
- + {{ i18n.ts.nothing }}
-
{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}
- {{ i18n.ts.createInviteCode }} -
{{ i18n.tsx.createLimitRemaining({ limit: currentInviteLimit }) }}
+
{{ i18n.t('inviteLimitResetCycle', { time: resetCycle, limit: inviteLimit }) }}
+ {{ i18n.ts.createInviteCode }} +
{{ i18n.t('createLimitRemaining', { limit: currentInviteLimit }) }}
@@ -40,7 +40,6 @@ import { computed, ref, shallowRef } from 'vue'; import type * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import MkButton from '@/components/MkButton.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkInviteCode from '@/components/MkInviteCode.vue'; @@ -69,7 +68,7 @@ const resetCycle = computed(() => { }); async function create() { - const ticket = await misskeyApi('invite/create'); + const ticket = await os.api('invite/create'); os.alert({ type: 'success', title: i18n.ts.inviteCodeCreated, @@ -88,15 +87,15 @@ function deleted(id: string) { } async function update() { - currentInviteLimit.value = (await misskeyApi('invite/limit')).remaining; + currentInviteLimit.value = (await os.api('invite/limit')).remaining; } update(); -definePageMetadata(() => ({ +definePageMetadata({ title: i18n.ts.invite, - icon: 'ti ti-user-plus', -})); + icon: 'ph-user-plus ph-bold ph-lg', +}); diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue index 9233695900..5c6432b80d 100644 --- a/packages/frontend/src/pages/my-antennas/index.vue +++ b/packages/frontend/src/pages/my-antennas/index.vue @@ -1,5 +1,5 @@ @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.add }} + {{ i18n.ts.add }}
@@ -28,14 +28,14 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue index 733e34eb2c..53b609e0bd 100644 --- a/packages/frontend/src/pages/oauth.vue +++ b/packages/frontend/src/pages/oauth.vue @@ -1,5 +1,5 @@ @@ -9,13 +9,13 @@ SPDX-License-Identifier: AGPL-3.0-only
-

{{ i18n.tsx._auth.permission({ name }) }}

+

{{ i18n.t('_auth.permission', { name }) }}

{{ i18n.ts._auth.permissionAsk }}

    -
  • {{ i18n.ts._permissions[p] }}
  • +
  • {{ i18n.t(`_permissions.${p}`) }}
-
{{ i18n.tsx._auth.shareAccess({ name }) }}
+
{{ i18n.t('_auth.shareAccess', { name }) }}
{{ i18n.ts._auth.shareAccessAsk }}
@@ -51,10 +51,10 @@ function onLogin(res): void { login(res.i); } -definePageMetadata(() => ({ +definePageMetadata({ title: 'OAuth', - icon: 'ti ti-apps', -})); + icon: 'ph-squares-four ph-bold ph-lg', +}); -.pageUser { - display: flex; - align-items: center; - - border-top: 1px solid var(--divider); - padding-top: 1.5rem; - margin-bottom: 1.5rem; - - .avatar, - .name, - .acct { - display: block; - } - - .avatar { - width: 4rem; - height: 4rem; - margin-right: 1rem; - } - - .name { - font-size: 110%; - font-weight: 700; - } - - .acct { - font-size: 90%; - opacity: 0.7; - } - - .follow { - margin-left: auto; - } -} - -.pageDate { - margin-bottom: 1.5rem; -} - -.pageLinks { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: var(--marginHalf); -} - + diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue deleted file mode 100644 index 08bb3cb76c..0000000000 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ /dev/null @@ -1,298 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue deleted file mode 100644 index 10ea3717ab..0000000000 --- a/packages/frontend/src/pages/reversi/game.vue +++ /dev/null @@ -1,121 +0,0 @@ - - - - - diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue deleted file mode 100644 index d823861b4a..0000000000 --- a/packages/frontend/src/pages/reversi/index.vue +++ /dev/null @@ -1,353 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index 9ee3fd7f01..193505f628 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -1,16 +1,16 @@ + diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index 155d8b82d7..6800481d55 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -1,5 +1,5 @@ @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -30,24 +30,6 @@ SPDX-License-Identifier: AGPL-3.0-only
- - -
-
-
{{ c.value.type }}
-
{{ c.value.id }}
- -
- -
-
-
{{ i18n.ts.uiInspectorDescription }}
-
-
-
{{ i18n.ts.scratchpadDescription }}
@@ -57,13 +39,12 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index 66c5c92480..0b1740847d 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -1,27 +1,19 @@ - diff --git a/packages/frontend/src/pages/search.stories.impl.ts b/packages/frontend/src/pages/search.stories.impl.ts deleted file mode 100644 index 0110a7ab8e..0000000000 --- a/packages/frontend/src/pages/search.stories.impl.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; -import search_ from './search.vue'; -import { userDetailed } from '@/../.storybook/fakes.js'; -import { commonHandlers } from '@/../.storybook/mocks.js'; - -const localUser = userDetailed('someuserid', 'miskist', null, 'Local Misskey User'); - -export const Default = { - render(args) { - return { - components: { - search_, - }, - setup() { - return { - args, - }; - }, - computed: { - props() { - return { - ...this.args, - }; - }, - }, - template: '', - }; - }, - args: { - ignoreNotesSearchAvailable: true, - }, - parameters: { - layout: 'fullscreen', - msw: { - handlers: [ - ...commonHandlers, - http.post('/api/users/show', () => { - return HttpResponse.json(userDetailed()); - }), - http.post('/api/users/search', () => { - return HttpResponse.json([userDetailed(), localUser]); - }), - ], - }, - }, -} satisfies StoryObj; - -export const NoteSearchDisabled = { - ...Default, - args: {}, -} satisfies StoryObj; - -export const WithUsernameLocal = { - ...Default, - - args: { - ...Default.args, - username: localUser.username, - host: localUser.host, - }, - parameters: { - layout: 'fullscreen', - msw: { - handlers: [ - ...commonHandlers, - http.post('/api/users/show', () => { - return HttpResponse.json(localUser); - }), - http.post('/api/users/search', () => { - return HttpResponse.json([userDetailed(), localUser]); - }), - ], - }, - }, -} satisfies StoryObj; - -export const WithUserType = { - ...Default, - args: { - type: 'user', - }, -} satisfies StoryObj; diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index a355c0eeaa..c0b2e55986 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -1,13 +1,13 @@ diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue index 38d7548fa8..eae120cb34 100644 --- a/packages/frontend/src/pages/search.vue +++ b/packages/frontend/src/pages/search.vue @@ -1,5 +1,5 @@ @@ -7,69 +7,51 @@ SPDX-License-Identifier: AGPL-3.0-only - - -
- -
-
- {{ i18n.ts.notesSearchNotAvailable }} -
-
+ +
+ +
+
+ {{ i18n.ts.notesSearchNotAvailable }} +
+
- - - -
+ + +
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue index 2244047b31..9a2a98ad89 100644 --- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue +++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue @@ -1,5 +1,5 @@ @@ -25,8 +25,6 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -58,12 +52,12 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._2fa.step3Title }}
- +
{{ i18n.ts._2fa.step3 }}
- {{ i18n.ts.goBack }} - {{ i18n.ts.continue }} + {{ i18n.ts.goBack }} + {{ i18n.ts.continue }}
@@ -77,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._2fa.checkBackupCodesBeforeCloseThisWizard }}
- +
@@ -90,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.download }} + {{ i18n.ts.download }}
@@ -115,11 +109,8 @@ import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import MkFolder from '@/components/MkFolder.vue'; import MkInfo from '@/components/MkInfo.vue'; -import MkLink from '@/components/MkLink.vue'; import { confetti } from '@/scripts/confetti.js'; -import { signinRequired } from '@/account.js'; - -const $i = signinRequired(); +import { $i } from '@/account.js'; defineProps<{ twoFactorData: { @@ -160,7 +151,7 @@ function downloadBackupCodes() { const txtBlob = new Blob([backupCodes.value.join('\n')], { type: 'text/plain' }); const dummya = document.createElement('a'); dummya.href = URL.createObjectURL(txtBlob); - dummya.download = `${$i.username}-2fa-backup-codes.txt`; + dummya.download = `${$i?.username}-2fa-backup-codes.txt`; dummya.click(); } } @@ -184,14 +175,8 @@ function allDone() { transform: translateX(-50px); } -.qrRoot { - display: block; - margin: 0 auto; +.qr { width: 200px; max-width: 100%; } - -.qr { - width: 100%; -} diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index 6a9a1e16e2..4e2f367374 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -1,5 +1,5 @@ @@ -16,10 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-only - + - +
@@ -30,14 +30,11 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.unregister }}
-
- {{ i18n.ts._2fa.registerTOTP }} - {{ i18n.ts.learnMore }} -
+ {{ i18n.ts._2fa.registerTOTP }} - +
@@ -58,8 +55,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.rename }} - {{ i18n.ts.unregister }} + {{ i18n.ts.rename }} + {{ i18n.ts.unregister }}
@@ -75,20 +72,17 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue deleted file mode 100644 index 4ec4610279..0000000000 --- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue deleted file mode 100644 index 5324a6b7f7..0000000000 --- a/packages/frontend/src/pages/settings/avatar-decoration.vue +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue index cf05e75acc..6e1481ae97 100644 --- a/packages/frontend/src/pages/settings/custom-css.vue +++ b/packages/frontend/src/pages/settings/custom-css.vue @@ -1,5 +1,5 @@ @@ -7,15 +7,15 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.customCssWarn }} - + - +
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue index e574ec7dc0..ff9a78cd52 100644 --- a/packages/frontend/src/pages/settings/deck.vue +++ b/packages/frontend/src/pages/settings/deck.vue @@ -1,5 +1,5 @@ @@ -32,12 +32,12 @@ const useSimpleUiForNonRootPages = computed(deckStore.makeGetterSetter('useSimpl const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); const columnAlign = computed(deckStore.makeGetterSetter('columnAlign')); -const headerActions = computed(() => []); +const headerActions = $computed(() => []); -const headerTabs = computed(() => []); +const headerTabs = $computed(() => []); -definePageMetadata(() => ({ +definePageMetadata({ title: i18n.ts.deck, - icon: 'ti ti-columns', -})); + icon: 'ph-text-columns ph-bold ph-lg', +}); diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index 432295aa7f..00dc022259 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -1,5 +1,5 @@ @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index b42c2f1503..bbfc8da483 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -1,5 +1,5 @@ @@ -14,23 +14,15 @@ SPDX-License-Identifier: AGPL-3.0-only Crowdin - - - - + + + @@ -41,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.add }} - {{ i18n.ts.remove }} + {{ i18n.ts.remove }}
@@ -51,59 +43,21 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - - - {{ i18n.ts.collapseNotesRepliedTo }} - {{ i18n.ts.collapseFiles }} - {{ i18n.ts.uncollapseCW }} - {{ i18n.ts.expandLongNote }} {{ i18n.ts.showNoteActionsOnlyHover }} {{ i18n.ts.showClipButtonInNoteFooter }} + {{ i18n.ts.collapseRenotes }} + {{ i18n.ts.collapseFiles }} {{ i18n.ts.autoloadConversation }} {{ i18n.ts.enableAdvancedMfm }} {{ i18n.ts.enableAnimatedMfm }} - {{ i18n.ts.enableQuickAddMfmFunction }} - {{ i18n.ts.showReactionsCount }} {{ i18n.ts.showGapBetweenNotesInTimeline }} {{ i18n.ts.loadRawImages }} - {{ i18n.ts.showTickerOnReplies }} - {{ i18n.ts.disableCatSpeak }} - - - - - - - - - -
- - - - -
- - - - - - - {{ i18n.ts.limitWidthOfReaction }}
@@ -123,15 +77,10 @@ SPDX-License-Identifier: AGPL-3.0-only - - - + + + - - - - -
@@ -141,38 +90,20 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.useGroupedNotifications }} - - {{ i18n.ts.enableFaviconNotificationDot }} - - - - - - - {{ i18n.ts.verifyNotificationDotWorkingButton }} - - - - - + + + + - - + + - {{ i18n.ts.allowClickingNotifications }} - {{ i18n.ts._notification.checkNotificationBehavior }}
@@ -190,26 +121,16 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.squareAvatars }} {{ i18n.ts.showAvatarDecorations }} {{ i18n.ts.useSystemFont }} + {{ i18n.ts.disableDrawer }} {{ i18n.ts.forceShowAds }} - {{ i18n.ts.oneko }} - {{ i18n.ts.seasonalScreenEffect }} - {{ i18n.ts.useNativeUIForVideoAudioPlayer }} + {{ i18n.ts.dataSaver }}
- - - - - - - -
-
@@ -224,8 +145,8 @@ SPDX-License-Identifier: AGPL-3.0-only - - + +
@@ -235,17 +156,13 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.warnForMissingAltText }} {{ i18n.ts.openImageInNewTab }} {{ i18n.ts.useReactionPickerForContextMenu }} {{ i18n.ts.enableInfiniteScroll }} {{ i18n.ts.keepScreenOn }} {{ i18n.ts.clickToOpen }} + {{ i18n.ts.showBots }} {{ i18n.ts.disableStreamingTimeline }} - {{ i18n.ts.enableHorizontalSwipe }} - {{ i18n.ts.alwaysConfirmFollow }} - {{ i18n.ts.confirmWhenRevealingSensitiveMedia }} - {{ i18n.ts.warnExternalUrl }}
@@ -253,63 +170,10 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - - - - - - -
- - {{ i18n.ts.showVisibilitySelectorOnBoost }} - - - - - - - - -
-
- - - - -
- {{ i18n.ts.reloadRequiredToApplySettings }} - -
- {{ i18n.ts.enableAll }} - {{ i18n.ts.disableAll }} -
-
- - {{ i18n.ts._dataSaver._media.title }} - - - - {{ i18n.ts._dataSaver._avatar.title }} - - - - {{ i18n.ts._dataSaver._urlPreview.title }} - - - - {{ i18n.ts._dataSaver._code.title }} - - -
-
-
@@ -317,72 +181,65 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - - - - -
- +
+ {{ lang }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }} + {{ i18n.ts.remove }}
{{ i18n.ts.deck }} - {{ i18n.ts.customCss }} + {{ i18n.ts.customCss }}
diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index e000c608fe..31bfd7e732 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -1,17 +1,17 @@ diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index c94cd512f3..b585333bbd 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -1,41 +1,36 @@ diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index 9fcf564e55..89241b8c02 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -1,16 +1,10 @@ diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue index 67943524ef..f1b7dcc0a8 100644 --- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue @@ -1,5 +1,5 @@ @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -38,15 +38,15 @@ import MkSelect from '@/components/MkSelect.vue'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import { Theme, getBuiltinThemesRef } from '@/scripts/theme.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; -import { getThemes, removeTheme } from '@/theme-store.js'; +import { getThemes, removeTheme } from '@/theme-store'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; const installedThemes = ref(getThemes()); const builtinThemes = getBuiltinThemesRef(); -const selectedThemeId = ref(null); +const selectedThemeId = ref(null); const themes = computed(() => [...installedThemes.value, ...builtinThemes.value]); @@ -72,12 +72,12 @@ function uninstall() { os.success(); } -const headerActions = computed(() => []); +const headerActions = $computed(() => []); -const headerTabs = computed(() => []); +const headerTabs = $computed(() => []); -definePageMetadata(() => ({ +definePageMetadata({ title: i18n.ts._theme.manage, - icon: 'ti ti-tool', -})); + icon: 'ph-wrench ph-bold ph-lg', +}); diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue index e7aef55a53..eb69912609 100644 --- a/packages/frontend/src/pages/settings/theme.vue +++ b/packages/frontend/src/pages/settings/theme.vue @@ -1,5 +1,5 @@ @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -58,10 +58,10 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts._theme.manage }} - {{ i18n.ts._theme.explore }} - {{ i18n.ts._theme.install }} - {{ i18n.ts._theme.make }} + {{ i18n.ts._theme.manage }} + {{ i18n.ts._theme.explore }} + {{ i18n.ts._theme.install }} + {{ i18n.ts._theme.make }}
@@ -85,11 +85,9 @@ import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { uniqueBy } from '@/scripts/array.js'; -import { fetchThemes, getThemes } from '@/theme-store.js'; +import { fetchThemes, getThemes } from '@/theme-store'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; -import { reloadAsk } from '@/scripts/reload-ask.js'; -import * as os from '@/os.js'; const installedThemes = ref(getThemes()); const builtinThemes = getBuiltinThemesRef(); @@ -126,7 +124,6 @@ const lightThemeId = computed({ } }, }); - const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); const wallpaper = ref(miLocalStorage.getItem('wallpaper')); @@ -138,13 +135,13 @@ watch(syncDeviceDarkMode, () => { } }); -watch(wallpaper, async () => { +watch(wallpaper, () => { if (wallpaper.value == null) { miLocalStorage.removeItem('wallpaper'); } else { miLocalStorage.setItem('wallpaper', wallpaper.value); } - await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); + location.reload(); }); onActivated(() => { @@ -163,14 +160,14 @@ function setWallpaper(event) { }); } -const headerActions = computed(() => []); +const headerActions = $computed(() => []); -const headerTabs = computed(() => []); +const headerTabs = $computed(() => []); -definePageMetadata(() => ({ +definePageMetadata({ title: i18n.ts.theme, - icon: 'ti ti-palette', -})); + icon: 'ph-palette ph-bold ph-lg', +}); diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue index d62357caaf..f08f143d46 100644 --- a/packages/frontend/src/pages/settings/webhook.new.vue +++ b/packages/frontend/src/pages/settings/webhook.new.vue @@ -1,5 +1,5 @@ @@ -14,12 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only - + - +
{{ i18n.ts._webhookSettings._events.follow }} @@ -33,13 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.create }} + {{ i18n.ts.create }}
diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue index 0d11b00c97..e9a7f9a02e 100644 --- a/packages/frontend/src/pages/settings/webhook.vue +++ b/packages/frontend/src/pages/settings/webhook.vue @@ -1,5 +1,5 @@ @@ -15,10 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ webhook.name || webhook.url }} diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index 37f6558d64..6d0492250d 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -1,5 +1,5 @@ @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only :renote="renote" :initialVisibleUsers="visibleUsers" class="_panel" - @posted="onPosted" + @posted="state = 'posted'" />
{{ i18n.ts.close }} @@ -30,71 +30,43 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index 226f08bf8e..0bb6548447 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -1,5 +1,5 @@ @@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
-
{{ i18n.tsx.clickToFinishEmailVerification({ ok: i18n.ts.gotIt }) }}
+
{{ i18n.t('clickToFinishEmailVerification', { ok: i18n.ts.gotIt }) }}
{{ submitting ? i18n.ts.processing : i18n.ts.gotIt }} @@ -25,25 +25,24 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index 2fa6eb81ba..b78cf531a5 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -1,5 +1,5 @@ @@ -47,13 +47,13 @@ SPDX-License-Identifier: AGPL-3.0-only - +
- + - + {{ i18n.ts.apply }}
@@ -73,25 +73,24 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue index 64514716d6..5ba4af7ca1 100644 --- a/packages/frontend/src/pages/user/activity.notes.vue +++ b/packages/frontend/src/pages/user/activity.notes.vue @@ -1,5 +1,5 @@ @@ -14,11 +14,11 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue index 37b25f694f..ba5ddafbdb 100644 --- a/packages/frontend/src/pages/user/following.vue +++ b/packages/frontend/src/pages/user/following.vue @@ -1,5 +1,5 @@ @@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/user/gallery.vue b/packages/frontend/src/pages/user/gallery.vue index 9ba81322ba..0d806100d9 100644 --- a/packages/frontend/src/pages/user/gallery.vue +++ b/packages/frontend/src/pages/user/gallery.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend/src/pages/user/home.stories.impl.ts b/packages/frontend/src/pages/user/home.stories.impl.ts index c623ef9ee4..80b4f1a9e9 100644 --- a/packages/frontend/src/pages/user/home.stories.impl.ts +++ b/packages/frontend/src/pages/user/home.stories.impl.ts @@ -1,13 +1,13 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { StoryObj } from '@storybook/vue3'; -import { HttpResponse, http } from 'msw'; -import { userDetailed } from '../../../.storybook/fakes.js'; -import { commonHandlers } from '../../../.storybook/mocks.js'; +import { rest } from 'msw'; +import { userDetailed } from '../../../.storybook/fakes'; +import { commonHandlers } from '../../../.storybook/mocks'; import home_ from './home.vue'; export const Default = { render(args) { @@ -39,13 +39,12 @@ export const Default = { msw: { handlers: [ ...commonHandlers, - http.post('/api/users/notes', () => { - return HttpResponse.json([]); + rest.post('/api/users/notes', (req, res, ctx) => { + return res(ctx.json([])); }), - http.get('/api/charts/user/notes', ({ request }) => { - const url = new URL(request.url); - const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300); - return HttpResponse.json({ + rest.get('/api/charts/user/notes', (req, res, ctx) => { + const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300); + return res(ctx.json({ total: Array.from({ length }, () => 0), inc: Array.from({ length }, () => 0), dec: Array.from({ length }, () => 0), @@ -55,12 +54,11 @@ export const Default = { renote: Array.from({ length }, () => 0), withFile: Array.from({ length }, () => 0), }, - }); + })); }), - http.get('/api/charts/user/pv', ({ request }) => { - const url = new URL(request.url); - const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300); - return HttpResponse.json({ + rest.get('/api/charts/user/pv', (req, res, ctx) => { + const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300); + return res(ctx.json({ upv: { user: Array.from({ length }, () => 0), visitor: Array.from({ length }, () => 0), @@ -69,7 +67,7 @@ export const Default = { user: Array.from({ length }, () => 0), visitor: Array.from({ length }, () => 0), }, - }); + })); }), ], }, diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index f2621f4ad0..03d6705f6b 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -1,10 +1,10 @@ @@ -469,6 +417,17 @@ onUnmounted(() => { background: linear-gradient(transparent, rgba(#000, 0.7)); } + > .followed { + position: absolute; + top: 12px; + left: 12px; + padding: 4px 8px; + color: #fff; + background: rgba(0, 0, 0, 0.7); + font-size: 0.7em; + border-radius: var(--radius-sm); + } + > .actions { position: absolute; top: 12px; @@ -505,12 +464,11 @@ onUnmounted(() => { > .name { display: block; - margin: -10px; - padding: 10px; + margin: 0; line-height: 32px; font-weight: bold; font-size: 1.8em; - filter: drop-shadow(0 0 4px #000); + text-shadow: 0 0 8px #000; } > .bottom { @@ -565,11 +523,6 @@ onUnmounted(() => { filter: drop-shadow(1px 1px 3px rgba(#000, 0.2)); } - > .followedMessage { - padding: 24px 24px 0 154px; - font-size: 0.9em; - } - > .roles { padding: 24px 24px 0 154px; font-size: 0.95em; @@ -752,10 +705,6 @@ onUnmounted(() => { margin: auto; } - > .followedMessage { - padding: 16px 16px 0 16px; - } - > .roles { padding: 16px 16px 0 16px; justify-content: center; @@ -802,7 +751,7 @@ onUnmounted(() => { } .tab { - margin-bottom: calc(var(--margin) / 2); + margin: calc(var(--margin) / 2) 0; padding: calc(var(--margin) / 2) 0; background: color-mix(in srgb, var(--bg) 65%, transparent); backdrop-filter: var(--blur, blur(15px)); @@ -819,30 +768,4 @@ onUnmounted(() => { margin-left: 4px; color: var(--success); } - -.infoBadges { - position: absolute; - top: 12px; - left: 12px; - - display: flex; - flex-direction: row; - - padding: 0; - margin: 0; - - > * { - padding: 4px 8px; - color: #fff; - background: rgba(0, 0, 0, 0.7); - font-size: 0.7em; - border-radius: var(--radius-sm); - list-style-type: none; - margin-left: 0; - } - - > :not(:first-child) { - margin-left: 8px; - } -} diff --git a/packages/frontend/src/pages/user/index.activity.vue b/packages/frontend/src/pages/user/index.activity.vue index ba94b5c822..b7576d4e20 100644 --- a/packages/frontend/src/pages/user/index.activity.vue +++ b/packages/frontend/src/pages/user/index.activity.vue @@ -1,15 +1,15 @@ @@ -94,7 +88,6 @@ onMounted(() => { } .img { - position: relative; height: 128px; border-radius: var(--radius-sm); overflow: clip; @@ -106,24 +99,8 @@ onMounted(() => { text-align: center; } -.sensitiveImg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - filter: brightness(0.7); -} .sensitive { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; display: grid; place-items: center; - font-size: 0.8em; - color: #fff; - cursor: pointer; } diff --git a/packages/frontend/src/pages/user/index.listenbrainz.vue b/packages/frontend/src/pages/user/index.listenbrainz.vue index 1c9ef8dd22..d58bd96347 100644 --- a/packages/frontend/src/pages/user/index.listenbrainz.vue +++ b/packages/frontend/src/pages/user/index.listenbrainz.vue @@ -1,46 +1,42 @@ - - @@ -116,7 +114,8 @@ if (props.user.listenbrainz) { } .text-sm { font-size: 0.875rem; - margin: 0 0 0.3rem; + margin: 0; + margin-bottom: 0.3rem; } .font-bold { font-weight: 700; diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue index 28a5091059..6cf5bcf91f 100644 --- a/packages/frontend/src/pages/user/index.timeline.vue +++ b/packages/frontend/src/pages/user/index.timeline.vue @@ -1,74 +1,52 @@ diff --git a/packages/frontend/src/pages/user/reactions.vue b/packages/frontend/src/pages/user/reactions.vue index 3671decc18..916b6615d5 100644 --- a/packages/frontend/src/pages/user/reactions.vue +++ b/packages/frontend/src/pages/user/reactions.vue @@ -1,5 +1,5 @@ diff --git a/packages/frontend/src/pages/user/recent-notes.vue b/packages/frontend/src/pages/user/recent-notes.vue deleted file mode 100644 index d636068408..0000000000 --- a/packages/frontend/src/pages/user/recent-notes.vue +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue index 0d132e6a86..52949c294c 100644 --- a/packages/frontend/src/pages/welcome.entrance.a.vue +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -1,5 +1,5 @@ @@ -9,10 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
Powered by
- -
+
@@ -36,31 +33,37 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -112,24 +115,14 @@ misskeyApiGet('federation/instances', { opacity: 0.5; } - > .logo-wrapper { + > .misskey { position: fixed; - top: 36px; - left: 36px; - flex: auto; - color: #fff; - user-select: none; - pointer-events: none; + top: 42px; + left: 42px; + width: 140px; - > .powered-by { - margin-bottom: 2px; - } - - > .misskey { - width: 140px; - @media (max-width: 450px) { - width: 130px; - } + @media (max-width: 450px) { + width: 130px; } } diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 5a41100bf1..bac6cc1f0c 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -1,5 +1,5 @@ @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -35,31 +35,30 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 16d558cc91..6c13af2f0a 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -1,75 +1,72 @@ diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue index 38d257506c..2e6bb8b38f 100644 --- a/packages/frontend/src/pages/welcome.vue +++ b/packages/frontend/src/pages/welcome.vue @@ -1,36 +1,35 @@ diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts index ac325e923f..b2254a0611 100644 --- a/packages/frontend/src/pizzax.ts +++ b/packages/frontend/src/pizzax.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,12 +8,11 @@ import { onUnmounted, Ref, ref, watch } from 'vue'; import { BroadcastChannel } from 'broadcast-channel'; import { $i } from '@/account.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { api } from '@/os.js'; import { get, set } from '@/scripts/idb-proxy.js'; import { defaultStore } from '@/store.js'; import { useStream } from '@/stream.js'; import { deepClone } from '@/scripts/clone.js'; -import { deepMerge } from '@/scripts/merge.js'; type StateDef = Record { this.loaded = this.ready.then(() => this.load()); } - private isPureObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); - } - - private mergeState(value: X, def: X): X { - if (this.isPureObject(value) && this.isPureObject(def)) { - const merged = deepMerge(value, def); - - if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged); - - return merged as X; - } - return value; - } - private async init(): Promise { await this.migrate(); @@ -105,11 +89,11 @@ export class Storage { for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) { if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) { - this.reactiveState[k].value = this.state[k] = this.mergeState(deviceState[k], v.default); + this.reactiveState[k].value = this.state[k] = deviceState[k]; } else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) { - this.reactiveState[k].value = this.state[k] = this.mergeState(registryCache[k], v.default); + this.reactiveState[k].value = this.state[k] = registryCache[k]; } else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) { - this.reactiveState[k].value = this.state[k] = this.mergeState(deviceAccountState[k], v.default); + this.reactiveState[k].value = this.state[k] = deviceAccountState[k]; } else { this.reactiveState[k].value = this.state[k] = v.default; if (_DEV_) console.log('Use default value', k, v.default); @@ -150,7 +134,7 @@ export class Storage { window.setTimeout(async () => { await defaultStore.ready; - misskeyApi('i/registry/get-all', { scope: ['client', this.key] }) + api('i/registry/get-all', { scope: ['client', this.key] }) .then(kvs => { const cache: Partial = {}; for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) { @@ -184,7 +168,7 @@ export class Storage { this.reactiveState[key].value = this.state[key] = rawValue; return this.addIdbSetJob(async () => { - if (_DEV_) console.log(`set ${String(key)} start`); + if (_DEV_) console.log(`set ${key} start`); switch (this.def[key].where) { case 'device': { this.pizzaxChannel.postMessage({ @@ -215,7 +199,7 @@ export class Storage { const cache = await get(this.registryCacheKeyName) || {}; cache[key] = rawValue; await set(this.registryCacheKeyName, cache); - await misskeyApi('i/registry/set', { + await api('i/registry/set', { scope: ['client', this.key], key: key.toString(), value: rawValue, @@ -223,7 +207,7 @@ export class Storage { break; } } - if (_DEV_) console.log(`set ${String(key)} complete`); + if (_DEV_) console.log(`set ${key} complete`); }); } @@ -239,12 +223,9 @@ export class Storage { /** * 特定のキーの、簡易的なgetter/setterを作ります - * 主にvue上で設定コントロールのmodelとして使う用 + * 主にvue場で設定コントロールのmodelとして使う用 */ - public makeGetterSetter(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): { - get: () => T[K]['default']; - set: (value: T[K]['default']) => void; - } { + public makeGetterSetter(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]) { const valueRef = ref(this.state[key]); const stop = watch(this.reactiveState[key], val => { diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index c0034d414c..e24f646a35 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -1,19 +1,15 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { ref } from 'vue'; import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; -import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; -import * as os from '@/os.js'; -import { i18n } from '@/i18n.js'; +import { createAiScriptEnv } from '@/scripts/aiscript/api.js'; +import { inputText } from '@/os.js'; import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js'; -import { warningExternalWebsite } from '@/scripts/warning-external-website.js'; const parser = new Parser(); const pluginContexts = new Map(); -export const pluginLogs = ref(new Map()); export async function install(plugin: Plugin): Promise { // 後方互換性のため @@ -23,30 +19,36 @@ export async function install(plugin: Plugin): Promise { plugin: plugin, storageKey: 'plugins:' + plugin.id, }), { - in: aiScriptReadline, + in: (q): Promise => { + return new Promise(ok => { + inputText({ + title: q, + }).then(({ canceled, result: a }) => { + if (canceled) { + ok(''); + } else { + ok(a); + } + }); + }); + }, out: (value): void => { console.log(value); - pluginLogs.value.get(plugin.id).push(utils.reprValue(value)); }, log: (): void => { }, - err: (err): void => { - pluginLogs.value.get(plugin.id).push(`${err}`); - throw err; // install時のtry-catchに反応させる - }, }); initPlugin({ plugin, aiscript }); - aiscript.exec(parser.parse(plugin.src)).then( - () => { - console.info('Plugin installed:', plugin.name, 'v' + plugin.version); - }, - (err) => { - console.error('Plugin install failed:', plugin.name, 'v' + plugin.version); - throw err; - }, - ); + try { + await aiscript.exec(parser.parse(plugin.src)); + } catch (err) { + console.error('Plugin install failed:', plugin.name, 'v' + plugin.version); + return; + } + + console.info('Plugin installed:', plugin.name, 'v' + plugin.version); } function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record { @@ -94,7 +96,7 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record { utils.assertString(url); - warningExternalWebsite(url.value); + window.open(url.value, '_blank'); }), 'Plugin:config': values.OBJ(config), }; @@ -102,7 +104,6 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record) => defineAsyncComponent({ + loader: loader, + loadingComponent: MkLoading, + errorComponent: MkError, +}); + +export const routes = [{ + path: '/@:initUser/pages/:initPageName/view-source', + component: page(() => import('./pages/page-editor/page-editor.vue')), +}, { + path: '/@:username/pages/:pageName', + component: page(() => import('./pages/page.vue')), +}, { + path: '/@:acct/following', + component: page(() => import('./pages/user/following.vue')), +}, { + path: '/@:acct/followers', + component: page(() => import('./pages/user/followers.vue')), +}, { + name: 'user', + path: '/@:acct/:page?', + component: page(() => import('./pages/user/index.vue')), +}, { + name: 'note', + path: '/notes/:noteId', + component: page(() => import('./pages/note.vue')), +}, { + name: 'list', + path: '/list/:listId', + component: page(() => import('./pages/list.vue')), +}, { + path: '/clips/:clipId', + component: page(() => import('./pages/clip.vue')), +}, { + path: '/instance-info/:host', + component: page(() => import('./pages/instance-info.vue')), +}, { + name: 'settings', + path: '/settings', + component: page(() => import('./pages/settings/index.vue')), + loginRequired: true, + children: [{ + path: '/profile', + name: 'profile', + component: page(() => import('./pages/settings/profile.vue')), + }, { + path: '/roles', + name: 'roles', + component: page(() => import('./pages/settings/roles.vue')), + }, { + path: '/privacy', + name: 'privacy', + component: page(() => import('./pages/settings/privacy.vue')), + }, { + path: '/reaction', + name: 'reaction', + component: page(() => import('./pages/settings/reaction.vue')), + }, { + path: '/drive', + name: 'drive', + component: page(() => import('./pages/settings/drive.vue')), + }, { + path: '/drive/cleaner', + name: 'drive', + component: page(() => import('./pages/settings/drive-cleaner.vue')), + }, { + path: '/notifications', + name: 'notifications', + component: page(() => import('./pages/settings/notifications.vue')), + }, { + path: '/email', + name: 'email', + component: page(() => import('./pages/settings/email.vue')), + }, { + path: '/security', + name: 'security', + component: page(() => import('./pages/settings/security.vue')), + }, { + path: '/general', + name: 'general', + component: page(() => import('./pages/settings/general.vue')), + }, { + path: '/theme/install', + name: 'theme', + component: page(() => import('./pages/settings/theme.install.vue')), + }, { + path: '/theme/manage', + name: 'theme', + component: page(() => import('./pages/settings/theme.manage.vue')), + }, { + path: '/theme', + name: 'theme', + component: page(() => import('./pages/settings/theme.vue')), + }, { + path: '/navbar', + name: 'navbar', + component: page(() => import('./pages/settings/navbar.vue')), + }, { + path: '/statusbar', + name: 'statusbar', + component: page(() => import('./pages/settings/statusbar.vue')), + }, { + path: '/sounds', + name: 'sounds', + component: page(() => import('./pages/settings/sounds.vue')), + }, { + path: '/plugin/install', + name: 'plugin', + component: page(() => import('./pages/settings/plugin.install.vue')), + }, { + path: '/plugin', + name: 'plugin', + component: page(() => import('./pages/settings/plugin.vue')), + }, { + path: '/import-export', + name: 'import-export', + component: page(() => import('./pages/settings/import-export.vue')), + }, { + path: '/mute-block', + name: 'mute-block', + component: page(() => import('./pages/settings/mute-block.vue')), + }, { + path: '/api', + name: 'api', + component: page(() => import('./pages/settings/api.vue')), + }, { + path: '/apps', + name: 'api', + component: page(() => import('./pages/settings/apps.vue')), + }, { + path: '/webhook/edit/:webhookId', + name: 'webhook', + component: page(() => import('./pages/settings/webhook.edit.vue')), + }, { + path: '/webhook/new', + name: 'webhook', + component: page(() => import('./pages/settings/webhook.new.vue')), + }, { + path: '/webhook', + name: 'webhook', + component: page(() => import('./pages/settings/webhook.vue')), + }, { + path: '/deck', + name: 'deck', + component: page(() => import('./pages/settings/deck.vue')), + }, { + path: '/preferences-backups', + name: 'preferences-backups', + component: page(() => import('./pages/settings/preferences-backups.vue')), + }, { + path: '/migration', + name: 'migration', + component: page(() => import('./pages/settings/migration.vue')), + }, { + path: '/custom-css', + name: 'general', + component: page(() => import('./pages/settings/custom-css.vue')), + }, { + path: '/accounts', + name: 'profile', + component: page(() => import('./pages/settings/accounts.vue')), + }, { + path: '/other', + name: 'other', + component: page(() => import('./pages/settings/other.vue')), + }, { + path: '/', + component: page(() => import('./pages/_empty_.vue')), + }], +}, { + path: '/reset-password/:token?', + component: page(() => import('./pages/reset-password.vue')), +}, { + path: '/signup-complete/:code', + component: page(() => import('./pages/signup-complete.vue')), +}, { + path: '/announcements', + component: page(() => import('./pages/announcements.vue')), +}, { + path: '/about', + component: page(() => import('./pages/about.vue')), + hash: 'initialTab', +}, { + path: '/about-sharkey', + component: page(() => import('./pages/about-sharkey.vue')), +}, { + path: '/invite', + name: 'invite', + component: page(() => import('./pages/invite.vue')), +}, { + path: '/ads', + component: page(() => import('./pages/ads.vue')), +}, { + path: '/theme-editor', + component: page(() => import('./pages/theme-editor.vue')), + loginRequired: true, +}, { + path: '/roles/:role', + component: page(() => import('./pages/role.vue')), +}, { + path: '/user-tags/:tag', + component: page(() => import('./pages/user-tag.vue')), +}, { + path: '/explore', + component: page(() => import('./pages/explore.vue')), + hash: 'initialTab', +}, { + path: '/search', + component: page(() => import('./pages/search.vue')), + query: { + q: 'query', + channel: 'channel', + type: 'type', + origin: 'origin', + }, +}, { + path: '/authorize-follow', + component: page(() => import('./pages/follow.vue')), + loginRequired: true, +}, { + path: '/share', + component: page(() => import('./pages/share.vue')), + loginRequired: true, +}, { + path: '/api-console', + component: page(() => import('./pages/api-console.vue')), + loginRequired: true, +}, { + path: '/scratchpad', + component: page(() => import('./pages/scratchpad.vue')), +}, { + path: '/auth/:token', + component: page(() => import('./pages/auth.vue')), +}, { + path: '/miauth/:session', + component: page(() => import('./pages/miauth.vue')), + query: { + callback: 'callback', + name: 'name', + icon: 'icon', + permission: 'permission', + }, +}, { + path: '/tags/:tag', + component: page(() => import('./pages/tag.vue')), +}, { + path: '/pages/new', + component: page(() => import('./pages/page-editor/page-editor.vue')), + loginRequired: true, +}, { + path: '/pages/edit/:initPageId', + component: page(() => import('./pages/page-editor/page-editor.vue')), + loginRequired: true, +}, { + path: '/pages', + component: page(() => import('./pages/pages.vue')), +}, { + path: '/play/:id/edit', + component: page(() => import('./pages/flash/flash-edit.vue')), + loginRequired: true, +}, { + path: '/play/new', + component: page(() => import('./pages/flash/flash-edit.vue')), + loginRequired: true, +}, { + path: '/play/:id', + component: page(() => import('./pages/flash/flash.vue')), +}, { + path: '/play', + component: page(() => import('./pages/flash/flash-index.vue')), +}, { + path: '/gallery/:postId/edit', + component: page(() => import('./pages/gallery/edit.vue')), + loginRequired: true, +}, { + path: '/gallery/new', + component: page(() => import('./pages/gallery/edit.vue')), + loginRequired: true, +}, { + path: '/gallery/:postId', + component: page(() => import('./pages/gallery/post.vue')), +}, { + path: '/gallery', + component: page(() => import('./pages/gallery/index.vue')), +}, { + path: '/channels/:channelId/edit', + component: page(() => import('./pages/channel-editor.vue')), + loginRequired: true, +}, { + path: '/channels/new', + component: page(() => import('./pages/channel-editor.vue')), + loginRequired: true, +}, { + path: '/channels/:channelId', + component: page(() => import('./pages/channel.vue')), +}, { + path: '/channels', + component: page(() => import('./pages/channels.vue')), +}, { + path: '/custom-emojis-manager', + component: page(() => import('./pages/custom-emojis-manager.vue')), +}, { + path: '/registry/keys/:domain/:path(*)?', + component: page(() => import('./pages/registry.keys.vue')), +}, { + path: '/registry/value/:domain/:path(*)?', + component: page(() => import('./pages/registry.value.vue')), +}, { + path: '/registry', + component: page(() => import('./pages/registry.vue')), +}, { + path: '/install-extentions', + component: page(() => import('./pages/install-extentions.vue')), + loginRequired: true, +}, { + path: '/admin/user/:userId', + component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.vue')), +}, { + path: '/admin/file/:fileId', + component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), +}, { + path: '/admin', + component: iAmModerator ? page(() => import('./pages/admin/index.vue')) : page(() => import('./pages/not-found.vue')), + children: [{ + path: '/overview', + name: 'overview', + component: page(() => import('./pages/admin/overview.vue')), + }, { + path: '/users', + name: 'users', + component: page(() => import('./pages/admin/users.vue')), + }, { + path: '/emojis', + name: 'emojis', + component: page(() => import('./pages/custom-emojis-manager.vue')), + }, { + path: '/avatar-decorations', + name: 'avatarDecorations', + component: page(() => import('./pages/avatar-decorations.vue')), + }, { + path: '/queue', + name: 'queue', + component: page(() => import('./pages/admin/queue.vue')), + }, { + path: '/files', + name: 'files', + component: page(() => import('./pages/admin/files.vue')), + }, { + path: '/federation', + name: 'federation', + component: page(() => import('./pages/admin/federation.vue')), + }, { + path: '/announcements', + name: 'announcements', + component: page(() => import('./pages/admin/announcements.vue')), + }, { + path: '/ads', + name: 'ads', + component: page(() => import('./pages/admin/ads.vue')), + }, { + path: '/roles/:id/edit', + name: 'roles', + component: page(() => import('./pages/admin/roles.edit.vue')), + }, { + path: '/roles/new', + name: 'roles', + component: page(() => import('./pages/admin/roles.edit.vue')), + }, { + path: '/roles/:id', + name: 'roles', + component: page(() => import('./pages/admin/roles.role.vue')), + }, { + path: '/roles', + name: 'roles', + component: page(() => import('./pages/admin/roles.vue')), + }, { + path: '/database', + name: 'database', + component: page(() => import('./pages/admin/database.vue')), + }, { + path: '/abuses', + name: 'abuses', + component: page(() => import('./pages/admin/abuses.vue')), + }, { + path: '/modlog', + name: 'modlog', + component: page(() => import('./pages/admin/modlog.vue')), + }, { + path: '/settings', + name: 'settings', + component: page(() => import('./pages/admin/settings.vue')), + }, { + path: '/branding', + name: 'branding', + component: page(() => import('./pages/admin/branding.vue')), + }, { + path: '/moderation', + name: 'moderation', + component: page(() => import('./pages/admin/moderation.vue')), + }, { + path: '/email-settings', + name: 'email-settings', + component: page(() => import('./pages/admin/email-settings.vue')), + }, { + path: '/object-storage', + name: 'object-storage', + component: page(() => import('./pages/admin/object-storage.vue')), + }, { + path: '/security', + name: 'security', + component: page(() => import('./pages/admin/security.vue')), + }, { + path: '/relays', + name: 'relays', + component: page(() => import('./pages/admin/relays.vue')), + }, { + path: '/instance-block', + name: 'instance-block', + component: page(() => import('./pages/admin/instance-block.vue')), + }, { + path: '/proxy-account', + name: 'proxy-account', + component: page(() => import('./pages/admin/proxy-account.vue')), + }, { + path: '/external-services', + name: 'external-services', + component: page(() => import('./pages/admin/external-services.vue')), + }, { + path: '/other-settings', + name: 'other-settings', + component: page(() => import('./pages/admin/other-settings.vue')), + }, { + path: '/server-rules', + name: 'server-rules', + component: page(() => import('./pages/admin/server-rules.vue')), + }, { + path: '/invites', + name: 'invites', + component: page(() => import('./pages/admin/invites.vue')), + }, { + path: '/approvals', + name: 'approvals', + component: page(() => import('./pages/admin/approvals.vue')), + }, { + path: '/', + component: page(() => import('./pages/_empty_.vue')), + }], +}, { + path: '/my/notifications', + component: page(() => import('./pages/notifications.vue')), + loginRequired: true, +}, { + path: '/my/favorites', + component: page(() => import('./pages/favorites.vue')), + loginRequired: true, +}, { + path: '/my/achievements', + component: page(() => import('./pages/achievements.vue')), + loginRequired: true, +}, { + path: '/my/drive/folder/:folder', + component: page(() => import('./pages/drive.vue')), + loginRequired: true, +}, { + path: '/my/drive', + component: page(() => import('./pages/drive.vue')), + loginRequired: true, +}, { + path: '/my/drive/file/:fileId', + component: page(() => import('./pages/drive.file.vue')), + loginRequired: true, +}, { + path: '/my/follow-requests', + component: page(() => import('./pages/follow-requests.vue')), + loginRequired: true, +}, { + path: '/my/lists/:listId', + component: page(() => import('./pages/my-lists/list.vue')), + loginRequired: true, +}, { + path: '/my/lists', + component: page(() => import('./pages/my-lists/index.vue')), + loginRequired: true, +}, { + path: '/my/clips', + component: page(() => import('./pages/my-clips/index.vue')), + loginRequired: true, +}, { + path: '/my/antennas/create', + component: page(() => import('./pages/my-antennas/create.vue')), + loginRequired: true, +}, { + path: '/my/antennas/:antennaId', + component: page(() => import('./pages/my-antennas/edit.vue')), + loginRequired: true, +}, { + path: '/my/antennas', + component: page(() => import('./pages/my-antennas/index.vue')), + loginRequired: true, +}, { + path: '/timeline/list/:listId', + component: page(() => import('./pages/user-list-timeline.vue')), + loginRequired: true, +}, { + path: '/timeline/antenna/:antennaId', + component: page(() => import('./pages/antenna-timeline.vue')), + loginRequired: true, +}, { + path: '/clicker', + component: page(() => import('./pages/clicker.vue')), + loginRequired: true, +}, { + path: '/timeline', + component: page(() => import('./pages/timeline.vue')), +}, { + name: 'index', + path: '/', + component: $i ? page(() => import('./pages/timeline.vue')) : page(() => import('./pages/welcome.vue')), + globalCacheKey: 'index', +}, { + path: '/:(*)', + component: page(() => import('./pages/not-found.vue')), +}]; + +export const mainRouter = new Router(routes, location.pathname + location.search + location.hash, !!$i, page(() => import('@/pages/not-found.vue'))); + +window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href); + +mainRouter.addListener('push', ctx => { + window.history.pushState({ key: ctx.key }, '', ctx.path); +}); + +window.addEventListener('popstate', (event) => { + mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); +}); + +export function useRouter(): Router { + return inject('router', null) ?? mainRouter; +} diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts deleted file mode 100644 index 686ac6920a..0000000000 --- a/packages/frontend/src/router/definition.ts +++ /dev/null @@ -1,605 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { AsyncComponentLoader, defineAsyncComponent } from 'vue'; -import type { IRouter, RouteDef } from '@/nirax.js'; -import { Router } from '@/nirax.js'; -import { $i, iAmModerator } from '@/account.js'; -import MkLoading from '@/pages/_loading_.vue'; -import MkError from '@/pages/_error_.vue'; - -export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({ - loader: loader, - loadingComponent: MkLoading, - errorComponent: MkError, -}); - -const routes: RouteDef[] = [{ - path: '/@:initUser/pages/:initPageName/view-source', - component: page(() => import('@/pages/page-editor/page-editor.vue')), -}, { - path: '/@:username/pages/:pageName', - component: page(() => import('@/pages/page.vue')), -}, { - path: '/@:acct/following', - component: page(() => import('@/pages/user/following.vue')), -}, { - path: '/@:acct/followers', - component: page(() => import('@/pages/user/followers.vue')), -}, { - name: 'user', - path: '/@:acct/:page?', - component: page(() => import('@/pages/user/index.vue')), -}, { - name: 'note', - path: '/notes/:noteId/:initialTab?', - component: page(() => import('@/pages/note.vue')), -}, { - name: 'list', - path: '/list/:listId', - component: page(() => import('@/pages/list.vue')), -}, { - path: '/clips/:clipId', - component: page(() => import('@/pages/clip.vue')), -}, { - path: '/instance-info/:host', - component: page(() => import('@/pages/instance-info.vue')), -}, { - name: 'settings', - path: '/settings', - component: page(() => import('@/pages/settings/index.vue')), - loginRequired: true, - children: [{ - path: '/profile', - name: 'profile', - component: page(() => import('@/pages/settings/profile.vue')), - }, { - path: '/avatar-decoration', - name: 'avatarDecoration', - component: page(() => import('@/pages/settings/avatar-decoration.vue')), - }, { - path: '/roles', - name: 'roles', - component: page(() => import('@/pages/settings/roles.vue')), - }, { - path: '/privacy', - name: 'privacy', - component: page(() => import('@/pages/settings/privacy.vue')), - }, { - path: '/emoji-picker', - name: 'emojiPicker', - component: page(() => import('@/pages/settings/emoji-picker.vue')), - }, { - path: '/drive', - name: 'drive', - component: page(() => import('@/pages/settings/drive.vue')), - }, { - path: '/drive/cleaner', - name: 'drive', - component: page(() => import('@/pages/settings/drive-cleaner.vue')), - }, { - path: '/notifications', - name: 'notifications', - component: page(() => import('@/pages/settings/notifications.vue')), - }, { - path: '/email', - name: 'email', - component: page(() => import('@/pages/settings/email.vue')), - }, { - path: '/security', - name: 'security', - component: page(() => import('@/pages/settings/security.vue')), - }, { - path: '/general', - name: 'general', - component: page(() => import('@/pages/settings/general.vue')), - }, { - path: '/theme/install', - name: 'theme', - component: page(() => import('@/pages/settings/theme.install.vue')), - }, { - path: '/theme/manage', - name: 'theme', - component: page(() => import('@/pages/settings/theme.manage.vue')), - }, { - path: '/theme', - name: 'theme', - component: page(() => import('@/pages/settings/theme.vue')), - }, { - path: '/navbar', - name: 'navbar', - component: page(() => import('@/pages/settings/navbar.vue')), - }, { - path: '/statusbar', - name: 'statusbar', - component: page(() => import('@/pages/settings/statusbar.vue')), - }, { - path: '/sounds', - name: 'sounds', - component: page(() => import('@/pages/settings/sounds.vue')), - }, { - path: '/plugin/install', - name: 'plugin', - component: page(() => import('@/pages/settings/plugin.install.vue')), - }, { - path: '/plugin', - name: 'plugin', - component: page(() => import('@/pages/settings/plugin.vue')), - }, { - path: '/import-export', - name: 'import-export', - component: page(() => import('@/pages/settings/import-export.vue')), - }, { - path: '/mute-block', - name: 'mute-block', - component: page(() => import('@/pages/settings/mute-block.vue')), - }, { - path: '/api', - name: 'api', - component: page(() => import('@/pages/settings/api.vue')), - }, { - path: '/apps', - name: 'api', - component: page(() => import('@/pages/settings/apps.vue')), - }, { - path: '/webhook/edit/:webhookId', - name: 'webhook', - component: page(() => import('@/pages/settings/webhook.edit.vue')), - }, { - path: '/webhook/new', - name: 'webhook', - component: page(() => import('@/pages/settings/webhook.new.vue')), - }, { - path: '/webhook', - name: 'webhook', - component: page(() => import('@/pages/settings/webhook.vue')), - }, { - path: '/deck', - name: 'deck', - component: page(() => import('@/pages/settings/deck.vue')), - }, { - path: '/preferences-backups', - name: 'preferences-backups', - component: page(() => import('@/pages/settings/preferences-backups.vue')), - }, { - path: '/migration', - name: 'migration', - component: page(() => import('@/pages/settings/migration.vue')), - }, { - path: '/custom-css', - name: 'general', - component: page(() => import('@/pages/settings/custom-css.vue')), - }, { - path: '/accounts', - name: 'profile', - component: page(() => import('@/pages/settings/accounts.vue')), - }, { - path: '/other', - name: 'other', - component: page(() => import('@/pages/settings/other.vue')), - }, { - path: '/', - component: page(() => import('@/pages/_empty_.vue')), - }], -}, { - path: '/reset-password/:token?', - component: page(() => import('@/pages/reset-password.vue')), -}, { - path: '/signup-complete/:code', - component: page(() => import('@/pages/signup-complete.vue')), -}, { - path: '/announcements', - component: page(() => import('@/pages/announcements.vue')), -}, { - path: '/announcements/:announcementId', - component: page(() => import('@/pages/announcement.vue')), -}, { - path: '/about', - component: page(() => import('@/pages/about.vue')), - hash: 'initialTab', -}, { - path: '/contact', - component: page(() => import('@/pages/contact.vue')), -}, { - path: '/about-sharkey', - component: page(() => import('@/pages/about-sharkey.vue')), -}, { - path: '/invite', - name: 'invite', - component: page(() => import('@/pages/invite.vue')), -}, { - path: '/ads', - component: page(() => import('@/pages/ads.vue')), -}, { - path: '/theme-editor', - component: page(() => import('@/pages/theme-editor.vue')), - loginRequired: true, -}, { - path: '/roles/:role', - component: page(() => import('@/pages/role.vue')), -}, { - path: '/user-tags/:tag', - component: page(() => import('@/pages/user-tag.vue')), -}, { - path: '/explore', - component: page(() => import('@/pages/explore.vue')), - hash: 'initialTab', -}, { - path: '/following-feed', - component: page(() => import('@/pages/following-feed.vue')), - loginRequired: true, -}, { - path: '/following-feed/:userId', - component: page(() => import('@/pages/user/recent-notes.vue')), - loginRequired: true, -}, { - path: '/search', - component: page(() => import('@/pages/search.vue')), - query: { - q: 'query', - userId: 'userId', - username: 'username', - host: 'host', - channel: 'channel', - type: 'type', - origin: 'origin', - }, -}, { - // Legacy Compatibility - path: '/authorize-follow', - redirect: '/lookup', - loginRequired: true, -}, { - // Mastodon Compatibility - path: '/authorize_interaction', - redirect: '/lookup', - loginRequired: true, -}, { - path: '/lookup', - component: page(() => import('@/pages/lookup.vue')), - loginRequired: true, -}, { - path: '/share', - component: page(() => import('@/pages/share.vue')), - loginRequired: true, -}, { - path: '/api-console', - component: page(() => import('@/pages/api-console.vue')), - loginRequired: true, -}, { - path: '/scratchpad', - component: page(() => import('@/pages/scratchpad.vue')), -}, { - path: '/preview', - component: page(() => import('@/pages/preview.vue')), -}, { - path: '/auth/:token', - component: page(() => import('@/pages/auth.vue')), -}, { - path: '/miauth/:session', - component: page(() => import('@/pages/miauth.vue')), - query: { - callback: 'callback', - name: 'name', - icon: 'icon', - permission: 'permission', - }, -}, { - path: '/oauth/authorize', - component: page(() => import('@/pages/oauth.vue')), -}, { - path: '/tags/:tag', - component: page(() => import('@/pages/tag.vue')), -}, { - path: '/pages/new', - component: page(() => import('@/pages/page-editor/page-editor.vue')), - loginRequired: true, -}, { - path: '/pages/edit/:initPageId', - component: page(() => import('@/pages/page-editor/page-editor.vue')), - loginRequired: true, -}, { - path: '/pages', - component: page(() => import('@/pages/pages.vue')), -}, { - path: '/play/:id/edit', - component: page(() => import('@/pages/flash/flash-edit.vue')), - loginRequired: true, -}, { - path: '/play/new', - component: page(() => import('@/pages/flash/flash-edit.vue')), - loginRequired: true, -}, { - path: '/play/:id', - component: page(() => import('@/pages/flash/flash.vue')), -}, { - path: '/play', - component: page(() => import('@/pages/flash/flash-index.vue')), -}, { - path: '/gallery/:postId/edit', - component: page(() => import('@/pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/new', - component: page(() => import('@/pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/:postId', - component: page(() => import('@/pages/gallery/post.vue')), -}, { - path: '/gallery', - component: page(() => import('@/pages/gallery/index.vue')), -}, { - path: '/channels/:channelId/edit', - component: page(() => import('@/pages/channel-editor.vue')), - loginRequired: true, -}, { - path: '/channels/new', - component: page(() => import('@/pages/channel-editor.vue')), - loginRequired: true, -}, { - path: '/channels/:channelId', - component: page(() => import('@/pages/channel.vue')), -}, { - path: '/channels', - component: page(() => import('@/pages/channels.vue')), -}, { - path: '/custom-emojis-manager', - component: page(() => import('@/pages/custom-emojis-manager.vue')), -}, { - path: '/avatar-decorations', - name: 'avatarDecorations', - component: page(() => import('@/pages/avatar-decorations.vue')), -}, { - path: '/registry/keys/:domain/:path(*)?', - component: page(() => import('@/pages/registry.keys.vue')), -}, { - path: '/registry/value/:domain/:path(*)?', - component: page(() => import('@/pages/registry.value.vue')), -}, { - path: '/registry', - component: page(() => import('@/pages/registry.vue')), -}, { - path: '/install-extentions', - redirect: '/install-extensions', - loginRequired: true, -}, { - path: '/install-extensions', - component: page(() => import('@/pages/install-extensions.vue')), - loginRequired: true, -}, { - path: '/admin/user/:userId', - component: iAmModerator ? page(() => import('@/pages/admin-user.vue')) : page(() => import('@/pages/not-found.vue')), -}, { - path: '/admin/file/:fileId', - component: iAmModerator ? page(() => import('@/pages/admin-file.vue')) : page(() => import('@/pages/not-found.vue')), -}, { - path: '/admin', - component: iAmModerator ? page(() => import('@/pages/admin/index.vue')) : page(() => import('@/pages/not-found.vue')), - children: [{ - path: '/overview', - name: 'overview', - component: page(() => import('@/pages/admin/overview.vue')), - }, { - path: '/users', - name: 'users', - component: page(() => import('@/pages/admin/users.vue')), - }, { - path: '/emojis', - name: 'emojis', - component: page(() => import('@/pages/custom-emojis-manager.vue')), - }, { - path: '/avatar-decorations', - name: 'avatarDecorations', - component: page(() => import('@/pages/avatar-decorations.vue')), - }, { - path: '/queue', - name: 'queue', - component: page(() => import('@/pages/admin/queue.vue')), - }, { - path: '/files', - name: 'files', - component: page(() => import('@/pages/admin/files.vue')), - }, { - path: '/federation', - name: 'federation', - component: page(() => import('@/pages/admin/federation.vue')), - }, { - path: '/announcements', - name: 'announcements', - component: page(() => import('@/pages/admin/announcements.vue')), - }, { - path: '/ads', - name: 'ads', - component: page(() => import('@/pages/admin/ads.vue')), - }, { - path: '/roles/:id/edit', - name: 'roles', - component: page(() => import('@/pages/admin/roles.edit.vue')), - }, { - path: '/roles/new', - name: 'roles', - component: page(() => import('@/pages/admin/roles.edit.vue')), - }, { - path: '/roles/:id', - name: 'roles', - component: page(() => import('@/pages/admin/roles.role.vue')), - }, { - path: '/roles', - name: 'roles', - component: page(() => import('@/pages/admin/roles.vue')), - }, { - path: '/database', - name: 'database', - component: page(() => import('@/pages/admin/database.vue')), - }, { - path: '/abuses', - name: 'abuses', - component: page(() => import('@/pages/admin/abuses.vue')), - }, { - path: '/modlog', - name: 'modlog', - component: page(() => import('@/pages/admin/modlog.vue')), - }, { - path: '/settings', - name: 'settings', - component: page(() => import('@/pages/admin/settings.vue')), - }, { - path: '/branding', - name: 'branding', - component: page(() => import('@/pages/admin/branding.vue')), - }, { - path: '/moderation', - name: 'moderation', - component: page(() => import('@/pages/admin/moderation.vue')), - }, { - path: '/email-settings', - name: 'email-settings', - component: page(() => import('@/pages/admin/email-settings.vue')), - }, { - path: '/object-storage', - name: 'object-storage', - component: page(() => import('@/pages/admin/object-storage.vue')), - }, { - path: '/security', - name: 'security', - component: page(() => import('@/pages/admin/security.vue')), - }, { - path: '/relays', - name: 'relays', - component: page(() => import('@/pages/admin/relays.vue')), - }, { - path: '/external-services', - name: 'external-services', - component: page(() => import('@/pages/admin/external-services.vue')), - }, { - path: '/performance', - name: 'performance', - component: page(() => import('@/pages/admin/performance.vue')), - }, { - path: '/server-rules', - name: 'server-rules', - component: page(() => import('@/pages/admin/server-rules.vue')), - }, { - path: '/invites', - name: 'invites', - component: page(() => import('@/pages/admin/invites.vue')), - }, { - path: '/approvals', - name: 'approvals', - component: page(() => import('@/pages/admin/approvals.vue')), - }, { - path: '/abuse-report-notification-recipient', - name: 'abuse-report-notification-recipient', - component: page(() => import('@/pages/admin/abuse-report/notification-recipient.vue')), - }, { - path: '/system-webhook', - name: 'system-webhook', - component: page(() => import('@/pages/admin/system-webhook.vue')), - }, { - path: '/', - component: page(() => import('@/pages/_empty_.vue')), - }], -}, { - path: '/my/notifications', - component: page(() => import('@/pages/notifications.vue')), - loginRequired: true, -}, { - path: '/my/favorites', - component: page(() => import('@/pages/favorites.vue')), - loginRequired: true, -}, { - path: '/my/achievements', - component: page(() => import('@/pages/achievements.vue')), - loginRequired: true, -}, { - path: '/my/drive/folder/:folder', - component: page(() => import('@/pages/drive.vue')), - loginRequired: true, -}, { - path: '/my/drive', - component: page(() => import('@/pages/drive.vue')), - loginRequired: true, -}, { - path: '/my/drive/file/:fileId', - component: page(() => import('@/pages/drive.file.vue')), - loginRequired: true, -}, { - path: '/my/follow-requests', - component: page(() => import('@/pages/follow-requests.vue')), - loginRequired: true, -}, { - path: '/my/lists/:listId', - component: page(() => import('@/pages/my-lists/list.vue')), - loginRequired: true, -}, { - path: '/my/lists', - component: page(() => import('@/pages/my-lists/index.vue')), - loginRequired: true, -}, { - path: '/my/clips', - component: page(() => import('@/pages/my-clips/index.vue')), - loginRequired: true, -}, { - path: '/my/antennas/create', - component: page(() => import('@/pages/my-antennas/create.vue')), - loginRequired: true, -}, { - path: '/my/antennas/:antennaId', - component: page(() => import('@/pages/my-antennas/edit.vue')), - loginRequired: true, -}, { - path: '/my/antennas', - component: page(() => import('@/pages/my-antennas/index.vue')), - loginRequired: true, -}, { - path: '/timeline/list/:listId', - component: page(() => import('@/pages/user-list-timeline.vue')), - loginRequired: true, -}, { - path: '/timeline/antenna/:antennaId', - component: page(() => import('@/pages/antenna-timeline.vue')), - loginRequired: true, -}, { - path: '/clicker', - component: page(() => import('@/pages/clicker.vue')), - loginRequired: true, -}, { - path: '/games', - component: page(() => import('@/pages/games.vue')), - loginRequired: false, -}, { - path: '/bubble-game', - component: page(() => import('@/pages/drop-and-fusion.vue')), - loginRequired: true, -}, { - path: '/reversi', - component: page(() => import('@/pages/reversi/index.vue')), - loginRequired: false, -}, { - path: '/reversi/g/:gameId', - component: page(() => import('@/pages/reversi/game.vue')), - loginRequired: false, -}, { - path: '/timeline', - component: page(() => import('@/pages/timeline.vue')), -}, { - name: 'index', - path: '/', - component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')), - globalCacheKey: 'index', -}, { - // テスト用リダイレクト設定。ログイン中ユーザのプロフィールにリダイレクトする - path: '/redirect-test', - redirect: $i ? `@${$i.username}` : '/', - loginRequired: true, -}, { - path: '/:(*)', - component: page(() => import('@/pages/not-found.vue')), -}]; - -export function createMainRouter(path: string): IRouter { - return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue'))); -} diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts deleted file mode 100644 index 709c508741..0000000000 --- a/packages/frontend/src/router/main.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { EventEmitter } from 'eventemitter3'; -import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js'; - -import type { App, ShallowRef } from 'vue'; - -/** - * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。 - * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能) - */ -export function setupRouter(app: App, routerFactory: ((path: string) => IRouter)): void { - app.provide('routerFactory', routerFactory); - - const mainRouter = routerFactory(location.pathname + location.search + location.hash); - - window.addEventListener('popstate', (event) => { - mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); - }); - - mainRouter.addListener('push', ctx => { - window.history.pushState({ key: ctx.key }, '', ctx.path); - }); - - mainRouter.addListener('same', () => { - window.scroll({ top: 0, behavior: 'smooth' }); - }); - - mainRouter.addListener('replace', ctx => { - window.history.replaceState({ key: ctx.key }, '', ctx.path); - }); - - mainRouter.init(); - - setMainRouter(mainRouter); -} - -function getMainRouter(): IRouter { - const router = mainRouterHolder; - if (!router) { - throw new Error('mainRouter is not found.'); - } - - return router; -} - -/** - * メインルータを設定する。一度設定すると、それ以降は変更できない。 - * {@link setupRouter}から呼び出されることのみを想定している。 - */ -export function setMainRouter(router: IRouter) { - if (mainRouterHolder) { - throw new Error('mainRouter is already exists.'); - } - - mainRouterHolder = router; -} - -/** - * {@link mainRouter}用のプロキシ実装。 - * {@link mainRouter}は起動シーケンスの一部にて初期化されるため、僅かにundefinedになる期間がある。 - * その僅かな期間のためだけに型をundefined込みにしたくないのでこのクラスを緩衝材として使用する。 - */ -class MainRouterProxy implements IRouter { - private supplier: () => IRouter; - - constructor(supplier: () => IRouter) { - this.supplier = supplier; - } - - get current(): Resolved { - return this.supplier().current; - } - - get currentRef(): ShallowRef { - return this.supplier().currentRef; - } - - get currentRoute(): ShallowRef { - return this.supplier().currentRoute; - } - - get navHook(): ((path: string, flag?: any) => boolean) | null { - return this.supplier().navHook; - } - - set navHook(value) { - this.supplier().navHook = value; - } - - getCurrentKey(): string { - return this.supplier().getCurrentKey(); - } - - getCurrentPath(): any { - return this.supplier().getCurrentPath(); - } - - push(path: string, flag?: any): void { - this.supplier().push(path, flag); - } - - replace(path: string, key?: string | null): void { - this.supplier().replace(path, key); - } - - resolve(path: string): Resolved | null { - return this.supplier().resolve(path); - } - - init(): void { - this.supplier().init(); - } - - eventNames(): Array> { - return this.supplier().eventNames(); - } - - listeners>( - event: T, - ): Array> { - return this.supplier().listeners(event); - } - - listenerCount( - event: EventEmitter.EventNames, - ): number { - return this.supplier().listenerCount(event); - } - - emit>( - event: T, - ...args: EventEmitter.EventArgs - ): boolean { - return this.supplier().emit(event, ...args); - } - - on>( - event: T, - fn: EventEmitter.EventListener, - context?: any, - ): this { - this.supplier().on(event, fn, context); - return this; - } - - addListener>( - event: T, - fn: EventEmitter.EventListener, - context?: any, - ): this { - this.supplier().addListener(event, fn, context); - return this; - } - - once>( - event: T, - fn: EventEmitter.EventListener, - context?: any, - ): this { - this.supplier().once(event, fn, context); - return this; - } - - removeListener>( - event: T, - fn?: EventEmitter.EventListener, - context?: any, - once?: boolean, - ): this { - this.supplier().removeListener(event, fn, context, once); - return this; - } - - off>( - event: T, - fn?: EventEmitter.EventListener, - context?: any, - once?: boolean, - ): this { - this.supplier().off(event, fn, context, once); - return this; - } - - removeAllListeners( - event?: EventEmitter.EventNames, - ): this { - this.supplier().removeAllListeners(event); - return this; - } -} - -let mainRouterHolder: IRouter | null = null; - -export const mainRouter: IRouter = new MainRouterProxy(getMainRouter); diff --git a/packages/frontend/src/router/supplier.ts b/packages/frontend/src/router/supplier.ts deleted file mode 100644 index 7da236f4e7..0000000000 --- a/packages/frontend/src/router/supplier.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { inject } from 'vue'; -import { IRouter, Router } from '@/nirax.js'; -import { mainRouter } from '@/router/main.js'; - -/** - * メインの{@link Router}を取得する。 - * あらかじめ{@link setupRouter}を実行しておく必要がある({@link provide}により{@link IRouter}のインスタンスを注入可能であるならばこの限りではない) - */ -export function useRouter(): IRouter { - return inject('router', null) ?? mainRouter; -} - -/** - * 任意の{@link Router}を取得するためのファクトリを取得する。 - * あらかじめ{@link setupRouter}を実行しておく必要がある。 - */ -export function useRouterFactory(): (path: string) => IRouter { - const factory = inject<(path: string) => IRouter>('routerFactory'); - if (!factory) { - console.error('routerFactory is not defined.'); - throw new Error('routerFactory is not defined.'); - } - - return factory; -} diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts index f5d0ab559f..e7585fcf81 100644 --- a/packages/frontend/src/scripts/achievements.ts +++ b/packages/frontend/src/scripts/achievements.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { misskeyApi } from '@/scripts/misskey-api.js'; +import * as os from '@/os.js'; import { $i } from '@/account.js'; export const ACHIEVEMENT_TYPES = [ @@ -83,8 +83,6 @@ export const ACHIEVEMENT_TYPES = [ 'brainDiver', 'smashTestNotificationButton', 'tutorialCompleted', - 'bubbleGameExplodingHead', - 'bubbleGameDoubleExplodingHead', ] as const; export const ACHIEVEMENT_BADGES = { @@ -468,16 +466,6 @@ export const ACHIEVEMENT_BADGES = { bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', frame: 'bronze', }, - 'bubbleGameExplodingHead': { - img: '/fluent-emoji/1f92f.png', - bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', - frame: 'bronze', - }, - 'bubbleGameDoubleExplodingHead': { - img: '/fluent-emoji/1f92f.png', - bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', - frame: 'silver', - }, /* @see } as const satisfies Record { claimingQueue.delete(type); }, 500); - misskeyApi('i/claim-achievement', { name: type }); + os.api('i/claim-achievement', { name: type }); } if (_DEV_) { diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 46aed49330..fb7ab924b7 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -1,26 +1,15 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { utils, values } from '@syuilo/aiscript'; -import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; -import { url, lang } from '@@/js/config.js'; - -export function aiScriptReadline(q: string): Promise { - return new Promise(ok => { - os.inputText({ - title: q, - }).then(({ result: a }) => { - ok(a ?? ''); - }); - }); -} +import { url, lang } from '@/config.js'; +import { nyaize } from '@/scripts/nyaize.js'; export function createAiScriptEnv(opts) { return { @@ -55,13 +44,12 @@ export function createAiScriptEnv(opts) { if (typeof token.value !== 'string') throw new Error('invalid token'); } const actualToken: string|null = token?.value ?? opts.token ?? null; - return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => { + return os.api(ep.value, utils.valToJs(param), actualToken).then(res => { return utils.jsToVal(res); }, err => { return values.ERROR('request_failed', utils.jsToVal(err)); }); }), - /* セキュリティ上の問題があるため無効化 'Mk:apiExternal': values.FN_NATIVE(async ([host, ep, param, token]) => { utils.assertString(host); utils.assertString(ep); @@ -72,7 +60,6 @@ export function createAiScriptEnv(opts) { return values.ERROR('request_failed', utils.jsToVal(err)); }); }), - */ 'Mk:save': values.FN_NATIVE(([key, value]) => { utils.assertString(key); miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value))); @@ -87,7 +74,7 @@ export function createAiScriptEnv(opts) { }), 'Mk:nyaize': values.FN_NATIVE(([text]) => { utils.assertString(text); - return values.STR(Misskey.nyaize(text.value)); + return values.STR(nyaize(text.value)); }), }; } diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index 2b386bebb8..d326b956e8 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -1,12 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { utils, values } from '@syuilo/aiscript'; import { v4 as uuid } from 'uuid'; import { ref, Ref } from 'vue'; -import * as Misskey from 'misskey-js'; export type AsUiComponentBase = { id: string; @@ -27,8 +26,6 @@ export type AsUiContainer = AsUiComponentBase & { font?: 'serif' | 'sans-serif' | 'monospace'; borderWidth?: number; borderColor?: string; - borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset'; - borderRadius?: number; padding?: number; rounded?: boolean; hidden?: boolean; @@ -50,7 +47,6 @@ export type AsUiMfm = AsUiComponentBase & { bold?: boolean; color?: string; font?: 'serif' | 'sans-serif' | 'monospace'; - onClickEv?: (evId: string) => void }; export type AsUiButton = AsUiComponentBase & { @@ -118,24 +114,21 @@ export type AsUiFolder = AsUiComponentBase & { opened?: boolean; }; -type PostFormPropsForAsUi = { - text: string; - cw?: string; - visibility?: (typeof Misskey.noteVisibilities)[number]; - localOnly?: boolean; -}; - export type AsUiPostFormButton = AsUiComponentBase & { type: 'postFormButton'; text?: string; primary?: boolean; rounded?: boolean; - form?: PostFormPropsForAsUi; + form?: { + text: string; + }; }; export type AsUiPostForm = AsUiComponentBase & { type: 'postForm'; - form?: PostFormPropsForAsUi; + form?: { + text: string; + }; }; export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm; @@ -175,10 +168,6 @@ function getContainerOptions(def: values.Value | undefined): Omit Promise): Omit { +function getMfmOptions(def: values.Value | undefined): Omit { utils.assertObject(def); const text = def.value.get('text'); @@ -241,8 +228,6 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg if (color) utils.assertString(color); const font = def.value.get('font'); if (font) utils.assertString(font); - const onClickEv = def.value.get('onClickEv'); - if (onClickEv) utils.assertFunction(onClickEv); return { text: text?.value, @@ -250,9 +235,6 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg bold: bold?.value, color: color?.value, font: font?.value, - onClickEv: (evId: string) => { - if (onClickEv) call(onClickEv, [values.STR(evId)]); - }, }; } @@ -457,24 +439,6 @@ function getFolderOptions(def: values.Value | undefined): Omit Promise): Omit { utils.assertObject(def); @@ -487,11 +451,19 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu const form = def.value.get('form'); if (form) utils.assertObject(form); + const getForm = () => { + const text = form!.value.get('text'); + utils.assertString(text); + return { + text: text.value, + }; + }; + return { text: text?.value, primary: primary?.value, rounded: rounded?.value, - form: form ? getPostFormProps(form) : { + form: form ? getForm() : { text: '', }, }; @@ -503,8 +475,16 @@ function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn const form = def.value.get('form'); if (form) utils.assertObject(form); + const getForm = () => { + const text = form!.value.get('text'); + utils.assertString(text); + return { + text: text.value, + }; + }; + return { - form: form ? getPostFormProps(form) : { + form: form ? getForm() : { text: '', }, }; diff --git a/packages/frontend/src/scripts/api.ts b/packages/frontend/src/scripts/api.ts new file mode 100644 index 0000000000..080977e5e4 --- /dev/null +++ b/packages/frontend/src/scripts/api.ts @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { ref } from 'vue'; +import { apiUrl } from '@/config.js'; +import { $i } from '@/account.js'; +export const pendingApiRequestsCount = ref(0); + +// Implements Misskey.api.ApiClient.request +export function api(endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal): Promise { + if (endpoint.includes('://')) throw new Error('invalid endpoint'); + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const promise = new Promise((resolve, reject) => { + // Append a credential + if ($i) (data as any).i = $i.token; + if (token !== undefined) (data as any).i = token; + + // Send request + window.fetch(`${apiUrl}/${endpoint}`, { + method: 'POST', + body: JSON.stringify(data), + credentials: 'omit', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', + }, + signal, + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +} + +export function apiExternal(hostUrl: string, endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal): Promise { + if (!/^https?:\/\//.test(hostUrl)) throw new Error('invalid host name'); + if (endpoint.includes('://')) throw new Error('invalid endpoint'); + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const promise = new Promise((resolve, reject) => { + // Append a credential + (data as any).i = token; + + const fullUrl = (hostUrl.slice(-1) === '/' ? hostUrl.slice(0, -1) : hostUrl) + + '/api/' + (endpoint.slice(0, 1) === '/' ? endpoint.slice(1) : endpoint); + // Send request + window.fetch(fullUrl, { + method: 'POST', + body: JSON.stringify(data), + credentials: 'omit', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', + }, + signal, + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +} + +// Implements Misskey.api.ApiClient.request +export function apiGet (endpoint: E, data: P = {} as any): Promise { + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const query = new URLSearchParams(data as any); + + const promise = new Promise((resolve, reject) => { + // Send request + window.fetch(`${apiUrl}/${endpoint}?${query}`, { + method: 'GET', + credentials: 'omit', + cache: 'default', + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +} diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/scripts/array.ts index f2feb29dfc..082703a450 100644 --- a/packages/frontend/src/scripts/array.ts +++ b/packages/frontend/src/scripts/array.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -77,6 +77,44 @@ export function maximum(xs: number[]): number { return Math.max(...xs); } +/** + * Splits an array based on the equivalence relation. + * The concatenation of the result is equal to the argument. + */ +export function groupBy(f: EndoRelation, xs: T[]): T[][] { + const groups = [] as T[][]; + for (const x of xs) { + const lastGroup = groups.at(-1); + if (lastGroup !== undefined && f(lastGroup[0], x)) { + lastGroup.push(x); + } else { + groups.push([x]); + } + } + return groups; +} + +/** + * Splits an array based on the equivalence relation induced by the function. + * The concatenation of the result is equal to the argument. + */ +export function groupOn(f: (x: T) => S, xs: T[]): T[][] { + return groupBy((a, b) => f(a) === f(b), xs); +} + +export function groupByX(collections: T[], keySelector: (x: T) => string) { + return collections.reduce((obj: Record, item: T) => { + const key = keySelector(item); + if (typeof obj[key] === 'undefined') { + obj[key] = []; + } + + obj[key].push(item); + + return obj; + }, {}); +} + /** * Compare two arrays by lexicographical order */ diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts index d942402ffc..0b4ebb4410 100644 --- a/packages/frontend/src/scripts/autocomplete.ts +++ b/packages/frontend/src/scripts/autocomplete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,20 +8,17 @@ import getCaretCoordinates from 'textarea-caret'; import { toASCII } from 'punycode/'; import { popup } from '@/os.js'; -export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam'; - export class Autocomplete { private suggestion: { x: Ref; y: Ref; - q: Ref; + q: Ref; close: () => void; } | null; private textarea: HTMLInputElement | HTMLTextAreaElement; private currentType: string; - private textRef: Ref; + private textRef: Ref; private opening: boolean; - private onlyType: SuggestionType[]; private get text(): string { // Use raw .value to get the latest value @@ -38,7 +35,7 @@ export class Autocomplete { /** * 対象のテキストエリアを与えてインスタンスを初期化します。 */ - constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref, onlyType?: SuggestionType[]) { + constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref) { //#region BIND this.onInput = this.onInput.bind(this); this.complete = this.complete.bind(this); @@ -49,7 +46,6 @@ export class Autocomplete { this.textarea = textarea; this.textRef = textRef; this.opening = false; - this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag', 'mfmParam']; this.attach(); } @@ -80,7 +76,6 @@ export class Autocomplete { const hashtagIndex = text.lastIndexOf('#'); const emojiIndex = text.lastIndexOf(':'); const mfmTagIndex = text.lastIndexOf('$'); - const mfmParamIndex = text.lastIndexOf('.'); const max = Math.max( mentionIndex, @@ -93,17 +88,14 @@ export class Autocomplete { return; } - const afterLastMfmParam = text.split(/\$\[[a-zA-Z]+/).pop(); - const isMention = mentionIndex !== -1; const isHashtag = hashtagIndex !== -1; - const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam?.includes(' '); - const isMfmTag = mfmTagIndex !== -1 && !isMfmParam; - const isEmoji = emojiIndex !== -1 && text.split(/:[\p{Letter}\p{Number}\p{Mark}_+-]+:/u).pop()!.includes(':'); + const isMfmTag = mfmTagIndex !== -1; + const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':'); let opened = false; - if (isMention && this.onlyType.includes('user')) { + if (isMention) { const username = text.substring(mentionIndex + 1); if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) { this.open('user', username); @@ -114,7 +106,7 @@ export class Autocomplete { } } - if (isHashtag && !opened && this.onlyType.includes('hashtag')) { + if (isHashtag && !opened) { const hashtag = text.substring(hashtagIndex + 1); if (!hashtag.includes(' ')) { this.open('hashtag', hashtag); @@ -122,15 +114,15 @@ export class Autocomplete { } } - if (isEmoji && !opened && this.onlyType.includes('emoji')) { + if (isEmoji && !opened) { const emoji = text.substring(emojiIndex + 1); if (!emoji.includes(' ')) { - this.open('emoji', emoji.normalize('NFC')); + this.open('emoji', emoji); opened = true; } } - if (isMfmTag && !opened && this.onlyType.includes('mfmTag')) { + if (isMfmTag && !opened) { const mfmTag = text.substring(mfmTagIndex + 1); if (!mfmTag.includes(' ')) { this.open('mfmTag', mfmTag.replace('[', '')); @@ -138,17 +130,6 @@ export class Autocomplete { } } - if (isMfmParam && !opened && this.onlyType.includes('mfmParam')) { - const mfmParam = text.substring(mfmParamIndex + 1); - if (!mfmParam.includes(' ')) { - this.open('mfmParam', { - tag: text.substring(mfmTagIndex + 2, mfmParamIndex), - params: mfmParam.split(','), - }); - opened = true; - } - } - if (!opened) { this.close(); } @@ -157,7 +138,7 @@ export class Autocomplete { /** * サジェストを提示します。 */ - private async open(type: string, q: any) { + private async open(type: string, q: string | null) { if (type !== this.currentType) { this.close(); } @@ -295,22 +276,6 @@ export class Autocomplete { const pos = trimmedBefore.length + (value.length + 3); this.textarea.setSelectionRange(pos, pos); }); - } else if (type === 'mfmParam') { - const source = this.text; - - const before = source.substring(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('.')); - const after = source.substring(caret); - - // 挿入 - this.text = `${trimmedBefore}.${value}${after}`; - - // キャレットを戻す - nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (value.length + 1); - this.textarea.setSelectionRange(pos, pos); - }); } } } diff --git a/packages/frontend/src/scripts/boost-quote.ts b/packages/frontend/src/scripts/boost-quote.ts deleted file mode 100644 index 4e025f5d4f..0000000000 --- a/packages/frontend/src/scripts/boost-quote.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SPDX-FileCopyrightText: dakkar and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only -*/ - -import { ref, Ref } from 'vue'; -import * as Misskey from 'misskey-js'; -import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; -import { MenuItem } from '@/types/menu.js'; - -/* - this script should eventually contain all Sharkey-specific bits of - boosting and quoting that we would otherwise have to replicate in - `{M,S}kNote{,Detailed,Sub}.vue` - */ - -export type Visibility = 'public' | 'home' | 'followers' | 'specified'; - -export function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility { - if (a === 'specified' || b === 'specified') return 'specified'; - if (a === 'followers' || b === 'followers') return 'followers'; - if (a === 'home' || b === 'home') return 'home'; - // if (a === 'public' || b === 'public') - return 'public'; -} - -export function visibilityIsAtLeast(a: Visibility | string, b: Visibility | string): boolean { - return smallerVisibility(a, b) === b; -} - -export function boostMenuItems(appearNote: Ref, renote: (v: Visibility, l: boolean) => void): MenuItem[] { - const localOnly = ref(defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); - const effectiveVisibility = ( - appearNote.value.channel?.isSensitive - ? smallerVisibility(appearNote.value.visibility, 'home') - : appearNote.value.visibility - ); - - const menuItems: MenuItem[] = []; - if (visibilityIsAtLeast(effectiveVisibility, 'public')) { - menuItems.push({ - type: 'button', - icon: 'ph-globe-hemisphere-west ph-bold ph-lg', - text: i18n.ts._visibility['public'], - action: () => { - renote('public', localOnly.value); - }, - } as MenuItem); - } - if (visibilityIsAtLeast(effectiveVisibility, 'home')) { - menuItems.push({ - type: 'button', - icon: 'ph-house ph-bold ph-lg', - text: i18n.ts._visibility['home'], - action: () => { - renote('home', localOnly.value); - }, - } as MenuItem); - } - if (visibilityIsAtLeast(effectiveVisibility, 'followers')) { - menuItems.push({ - type: 'button', - icon: 'ph-lock ph-bold ph-lg', - text: i18n.ts._visibility['followers'], - action: () => { - renote('followers', localOnly.value); - }, - } as MenuItem); - } - - return [ - ...menuItems, - { - type: 'switch', - icon: 'ph-planet ph-bold ph-lg', - text: i18n.ts._timelines.local, - ref: localOnly, - } as MenuItem, - ]; -} diff --git a/packages/frontend/src/scripts/cache.ts b/packages/frontend/src/scripts/cache.ts index 0fbdf34d5d..12347cf4b1 100644 --- a/packages/frontend/src/scripts/cache.ts +++ b/packages/frontend/src/scripts/cache.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/chart-legend.ts b/packages/frontend/src/scripts/chart-legend.ts index 2d534f60c1..e91908e0cb 100644 --- a/packages/frontend/src/scripts/chart-legend.ts +++ b/packages/frontend/src/scripts/chart-legend.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/chart-vline.ts b/packages/frontend/src/scripts/chart-vline.ts index 24e41245e7..336ec6cfbb 100644 --- a/packages/frontend/src/scripts/chart-vline.ts +++ b/packages/frontend/src/scripts/chart-vline.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/check-animated-mfm.ts b/packages/frontend/src/scripts/check-animated-mfm.ts index 2614dfb4f1..f47c40d4bc 100644 --- a/packages/frontend/src/scripts/check-animated-mfm.ts +++ b/packages/frontend/src/scripts/check-animated-mfm.ts @@ -1,9 +1,4 @@ -/* - * SPDX-FileCopyrightText: marie and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as mfm from '@transfem-org/sfm-js'; +import * as mfm from 'mfm-js'; export function checkAnimationFromMfm(nodes: mfm.MfmNode[]): boolean { const animatedNodes = mfm.extract(nodes, (node) => { @@ -13,12 +8,10 @@ export function checkAnimationFromMfm(nodes: mfm.MfmNode[]): boolean { node.props.name === 'twitch' || node.props.name === 'shake' || node.props.name === 'spin' || - node.props.name === 'jump' || - node.props.name === 'bounce' || - node.props.name === 'rainbow' || - node.props.name === 'sparkle' || - node.props.name === 'fade' || - node.props.name === 'followmouse') { + node.props.name === 'jump' || + node.props.name === 'bounce' || + node.props.name === 'rainbow' || + node.props.name === 'sparkle') { return true; } else { return false; @@ -27,7 +20,7 @@ export function checkAnimationFromMfm(nodes: mfm.MfmNode[]): boolean { return false; } }); - + if (animatedNodes.length > 0) { return true; } else { diff --git a/packages/frontend/src/scripts/check-permissions.ts b/packages/frontend/src/scripts/check-permissions.ts deleted file mode 100644 index ed86529d5b..0000000000 --- a/packages/frontend/src/scripts/check-permissions.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { instance } from '@/instance.js'; -import { $i } from '@/account.js'; - -export const notesSearchAvailable = ( - // FIXME: instance.policies would be null in Vitest - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - ($i == null && instance.policies != null && instance.policies.canSearchNotes) || - ($i != null && $i.policies.canSearchNotes) || - false -) as boolean; - -export const canSearchNonLocalNotes = ( - instance.noteSearchableScope === 'global' -); diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts deleted file mode 100644 index c3c3f419a9..0000000000 --- a/packages/frontend/src/scripts/check-reaction-permissions.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { UnicodeEmojiDef } from '@@/js/emojilist.js'; - -export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean { - if (typeof emoji === 'string') return true; // UnicodeEmojiDefにも無い絵文字であれば文字列で来る。Unicode絵文字であることには変わりないので常にリアクション可能とする; - if ('char' in emoji) return true; // UnicodeEmojiDefなら常にリアクション可能 - - const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? []; - return !(emoji.localOnly && note.user.host !== me.host) - && !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote')) - && (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id))); -} diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts index e65c327ffe..5ac19c8d5b 100644 --- a/packages/frontend/src/scripts/check-word-mute.ts +++ b/packages/frontend/src/scripts/check-word-mute.ts @@ -1,16 +1,14 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Note, MeDetailed } from "misskey-js/entities.js"; - -export function checkWordMute(note: Note, me: MeDetailed | null | undefined, mutedWords: Array): boolean { +export function checkWordMute(note: Record, me: Record | null | undefined, mutedWords: Array): boolean { // 自分自身 if (me && (note.userId === me.id)) return false; if (mutedWords.length > 0) { - const text = getNoteText(note); + const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim(); if (text === '') return false; @@ -42,25 +40,3 @@ export function checkWordMute(note: Note, me: MeDetailed | null | undefined, mut return false; } - -function getNoteText(note: Note): string { - const textParts: string[] = []; - - if (note.cw) textParts.push(note.cw); - - if (note.text) textParts.push(note.text); - - if (note.files) { - for (const file of note.files) { - if (file.comment) textParts.push(file.comment); - } - } - - if (note.poll) { - for (const choice of note.poll.choices) { - if (choice.text) textParts.push(choice.text); - } - } - - return textParts.join('\n').trim(); -} diff --git a/packages/frontend/src/scripts/chiptune2.ts b/packages/frontend/src/scripts/chiptune2.ts index 220002ff1e..e52adb07d1 100644 --- a/packages/frontend/src/scripts/chiptune2.ts +++ b/packages/frontend/src/scripts/chiptune2.ts @@ -1,28 +1,16 @@ -/* - * SPDX-FileCopyrightText: marie and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - +/* global libopenmpt UTF8ToString writeAsciiToMemory */ /* eslint-disable */ const ChiptuneAudioContext = window.AudioContext || window.webkitAudioContext; -let libopenmpt -let libopenmptLoadPromise - -type ChiptuneJsConfig = { - repeatCount: number | null; - context: AudioContext | null; -}; - -export function ChiptuneJsConfig (repeatCount?: number, context?: AudioContext) { +export function ChiptuneJsConfig (repeatCount: number, context: AudioContext) { this.repeatCount = repeatCount; this.context = context; } ChiptuneJsConfig.prototype.constructor = ChiptuneJsConfig; -export function ChiptuneJsPlayer (config: ChiptuneJsConfig) { +export function ChiptuneJsPlayer (config: object) { this.config = config; this.audioContext = config.context || new ChiptuneAudioContext(); this.context = this.audioContext.createGain(); @@ -32,28 +20,6 @@ export function ChiptuneJsPlayer (config: ChiptuneJsConfig) { this.volume = 1; } -ChiptuneJsPlayer.prototype.initialize = function() { - if (libopenmptLoadPromise) return libopenmptLoadPromise; - if (libopenmpt) return Promise.resolve(); - - libopenmptLoadPromise = new Promise(async (resolve, reject) => { - try { - const { Module } = await import('./libopenmpt/libopenmpt.js'); - await new Promise((resolve) => { - Module['onRuntimeInitialized'] = resolve; - }) - libopenmpt = Module; - resolve() - } catch (e) { - reject(e) - } finally { - libopenmptLoadPromise = undefined; - } - }) - - return libopenmptLoadPromise; -} - ChiptuneJsPlayer.prototype.constructor = ChiptuneJsPlayer; ChiptuneJsPlayer.prototype.fireEvent = function (eventName: string, response) { @@ -95,12 +61,12 @@ ChiptuneJsPlayer.prototype.seek = function (position: number) { ChiptuneJsPlayer.prototype.metadata = function () { const data = {}; - const keys = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';'); + const keys = UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';'); let keyNameBuffer = 0; for (const key of keys) { keyNameBuffer = libopenmpt._malloc(key.length + 1); - libopenmpt.writeAsciiToMemory(key, keyNameBuffer); - data[key] = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer)); + writeAsciiToMemory(key, keyNameBuffer); + data[key] = UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer)); libopenmpt._free(keyNameBuffer); } return data; @@ -118,7 +84,7 @@ ChiptuneJsPlayer.prototype.unlock = function () { }; ChiptuneJsPlayer.prototype.load = function (input) { - return this.initialize().then(() => new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { if(this.touchLocked) { this.unlock(); } @@ -140,7 +106,7 @@ ChiptuneJsPlayer.prototype.load = function (input) { reject(error); }); } - })); + }); }; ChiptuneJsPlayer.prototype.play = function (buffer: ArrayBuffer) { @@ -184,27 +150,6 @@ ChiptuneJsPlayer.prototype.getRow = function () { return 0; }; -ChiptuneJsPlayer.prototype.getNumPatterns = function () { - if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { - return libopenmpt._openmpt_module_get_num_patterns(this.currentPlayingNode.modulePtr); - } - return 0; -}; - -ChiptuneJsPlayer.prototype.getCurrentSpeed = function () { - if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { - return libopenmpt._openmpt_module_get_current_speed(this.currentPlayingNode.modulePtr); - } - return 0; -}; - -ChiptuneJsPlayer.prototype.getCurrentTempo = function () { - if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { - return libopenmpt._openmpt_module_get_current_tempo(this.currentPlayingNode.modulePtr); - } - return 0; -}; - ChiptuneJsPlayer.prototype.getPatternNumRows = function (pattern: number) { if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { return libopenmpt._openmpt_module_get_pattern_num_rows(this.currentPlayingNode.modulePtr, pattern); @@ -214,25 +159,11 @@ ChiptuneJsPlayer.prototype.getPatternNumRows = function (pattern: number) { ChiptuneJsPlayer.prototype.getPatternRowChannel = function (pattern: number, row: number, channel: number) { if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { - return libopenmpt.UTF8ToString(libopenmpt._openmpt_module_format_pattern_row_channel(this.currentPlayingNode.modulePtr, pattern, row, channel, 0, true)); + return UTF8ToString(libopenmpt._openmpt_module_format_pattern_row_channel(this.currentPlayingNode.modulePtr, pattern, row, channel, 0, true)); } return ''; }; -ChiptuneJsPlayer.prototype.getCtls = function () { - if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { - return libopenmpt._openmpt_module_get_ctls(this.currentPlayingNode.modulePtr); - } - return 0; -}; - -ChiptuneJsPlayer.prototype.version = function () { - if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { - return libopenmpt._openmpt_get_library_version(); - } - return 0; -}; - ChiptuneJsPlayer.prototype.createLibopenmptNode = function (buffer, config: object) { const maxFramesPerChunk = 4096; const processNode = this.audioContext.createScriptProcessor(2048, 0, 2); @@ -247,7 +178,6 @@ ChiptuneJsPlayer.prototype.createLibopenmptNode = function (buffer, config: obje processNode.paused = false; processNode.leftBufferPtr = libopenmpt._malloc(4 * maxFramesPerChunk); processNode.rightBufferPtr = libopenmpt._malloc(4 * maxFramesPerChunk); - processNode.perf = { 'current': 0, 'max': 0 }; processNode.cleanup = function () { if (this.modulePtr !== 0) { libopenmpt._openmpt_module_destroy(this.modulePtr); @@ -275,13 +205,7 @@ ChiptuneJsPlayer.prototype.createLibopenmptNode = function (buffer, config: obje processNode.togglePause = function () { this.paused = !this.paused; }; - processNode.getProcessTime = function() { - const max = this.perf.max; - this.perf.max = 0; - return { 'current': this.perf.current, 'max': max }; - }; processNode.onaudioprocess = function (e) { - let startTimeP1 = performance.now(); const outputL = e.outputBuffer.getChannelData(0); const outputR = e.outputBuffer.getChannelData(1); let framesToRender = outputL.length; @@ -307,13 +231,11 @@ ChiptuneJsPlayer.prototype.createLibopenmptNode = function (buffer, config: obje const currentPattern = libopenmpt._openmpt_module_get_current_pattern(this.modulePtr); const currentRow = libopenmpt._openmpt_module_get_current_row(this.modulePtr); - startTimeP1 = startTimeP1 - performance.now(); if (currentPattern !== this.patternIndex) { processNode.player.fireEvent('onPatternChange'); } processNode.player.fireEvent('onRowChange', { index: currentRow }); - const startTimeP2 = performance.now(); while (framesToRender > 0) { const framesPerChunk = Math.min(framesToRender, maxFramesPerChunk); const actualFramesPerChunk = libopenmpt._openmpt_module_read_float_stereo(this.modulePtr, this.context.sampleRate, framesPerChunk, this.leftBufferPtr, this.rightBufferPtr); @@ -340,8 +262,6 @@ ChiptuneJsPlayer.prototype.createLibopenmptNode = function (buffer, config: obje this.cleanup(); error ? processNode.player.fireEvent('onError', { type: 'openmpt' }) : processNode.player.fireEvent('onEnded'); } - this.perf.current = performance.now() - startTimeP2 + startTimeP1; - if (this.perf.current > this.perf.max) this.perf.max = this.perf.current; }; return processNode; }; diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/scripts/clear-cache.ts deleted file mode 100644 index 71d1232710..0000000000 --- a/packages/frontend/src/scripts/clear-cache.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { unisonReload } from '@/scripts/unison-reload.js'; -import * as os from '@/os.js'; -import { miLocalStorage } from '@/local-storage.js'; -import { fetchCustomEmojis } from '@/custom-emojis.js'; -import { fetchInstance } from '@/instance.js'; - -export async function clearCache() { - os.waiting(); - miLocalStorage.removeItem('instance'); - miLocalStorage.removeItem('instanceCachedAt'); - miLocalStorage.removeItem('locale'); - miLocalStorage.removeItem('localeVersion'); - miLocalStorage.removeItem('theme'); - miLocalStorage.removeItem('emojis'); - miLocalStorage.removeItem('lastEmojisFetchedAt'); - await fetchInstance(true); - await fetchCustomEmojis(true); - unisonReload(); -} diff --git a/packages/frontend/src/scripts/clicker-game.ts b/packages/frontend/src/scripts/clicker-game.ts index f9c4bc1829..5ad076e5ef 100644 --- a/packages/frontend/src/scripts/clicker-game.ts +++ b/packages/frontend/src/scripts/clicker-game.ts @@ -1,10 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { ref, computed } from 'vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import * as os from '@/os.js'; type SaveData = { gameVersion: number; @@ -23,7 +23,7 @@ let prev = ''; export async function load() { try { - saveData.value = await misskeyApi('i/registry/get', { + saveData.value = await os.api('i/registry/get', { scope: ['clickerGame'], key: 'saveData', }); @@ -63,7 +63,7 @@ export async function save() { const current = JSON.stringify(saveData.value); if (current === prev) return; - await misskeyApi('i/registry/set', { + await os.api('i/registry/set', { scope: ['clickerGame'], key: 'saveData', value: saveData.value, diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts index ea8eea14b5..96b53684f3 100644 --- a/packages/frontend/src/scripts/clone.ts +++ b/packages/frontend/src/scripts/clone.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,15 +8,15 @@ // あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった // https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045 -export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[]; +type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[]; export function deepClone(x: T): T { if (typeof x === 'object') { if (x === null) return x; if (Array.isArray(x)) return x.map(deepClone) as T; - const obj = {} as Record; + const obj = {} as Record; for (const [k, v] of Object.entries(x)) { - obj[k] = v === undefined ? undefined : deepClone(v); + obj[k] = deepClone(v); } return obj as T; } else { diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index 6710d9826e..957669122e 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -1,94 +1,28 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ +import { setWasm, setCDN, Highlighter, getHighlighter as _getHighlighter } from 'shiki'; -import { createHighlighterCore, loadWasm } from 'shiki/core'; -import darkPlus from 'shiki/themes/dark-plus.mjs'; -import { bundledThemesInfo } from 'shiki/themes'; -import { bundledLanguagesInfo } from 'shiki/langs'; -import lightTheme from '@@/themes/_light.json5'; -import darkTheme from '@@/themes/_dark.json5'; -import { unique } from './array.js'; -import { deepClone } from './clone.js'; -import { deepMerge } from './merge.js'; -import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core'; -import { ColdDeviceStorage } from '@/store.js'; +setWasm('/assets/shiki/dist/onig.wasm'); +setCDN('/assets/shiki/'); -let _highlighter: HighlighterCore | null = null; +let _highlighter: Highlighter | null = null; -export async function getTheme(mode: 'light' | 'dark', getName: true): Promise; -export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise; -export async function getTheme(mode: 'light' | 'dark', getName = false): Promise { - const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme')); - - if (theme.base) { - const base = [lightTheme, darkTheme].find(x => x.id === theme.base); - if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter); - } - - if (theme.codeHighlighter) { - let _res: ThemeRegistration = {}; - if (theme.codeHighlighter.base === '_none_') { - _res = deepClone(theme.codeHighlighter.overrides); - } else { - const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus; - _res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base); - } - if (_res.name == null) { - _res.name = theme.id; - } - _res.type = mode; - - if (getName) { - return _res.name; - } - return _res; - } - - if (getName) { - return 'dark-plus'; - } - return darkPlus; -} - -export async function getHighlighter(): Promise { +export async function getHighlighter(): Promise { if (!_highlighter) { return await initHighlighter(); } return _highlighter; } -async function initHighlighter() { - await loadWasm(import('shiki/onig.wasm?init')); - - // テーマの重複を消す - const themes = unique([ - darkPlus, - ...(await Promise.all([getTheme('light'), getTheme('dark')])), - ]); - - const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript'); - const highlighter = await createHighlighterCore({ - themes, - langs: [ - ...(jsLangInfo ? [async () => await jsLangInfo.import()] : []), - async () => (await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json')).default as unknown as LanguageRegistration, - ], +export async function initHighlighter() { + const highlighter = await _getHighlighter({ + theme: 'dark-plus', + langs: ['js'], }); - ColdDeviceStorage.watch('lightTheme', async () => { - const newTheme = await getTheme('light'); - if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { - highlighter.loadTheme(newTheme); - } - }); - - ColdDeviceStorage.watch('darkTheme', async () => { - const newTheme = await getTheme('dark'); - if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { - highlighter.loadTheme(newTheme); - } + await highlighter.loadLanguage({ + path: 'languages/aiscript.tmLanguage.json', + id: 'aiscript', + scopeName: 'source.aiscript', + aliases: ['is', 'ais'], }); _highlighter = highlighter; diff --git a/packages/frontend/src/scripts/collapsed.ts b/packages/frontend/src/scripts/collapsed.ts new file mode 100644 index 0000000000..57e6ecf5b5 --- /dev/null +++ b/packages/frontend/src/scripts/collapsed.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; + +export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean { + const collapsed = note.cw == null && note.text != null && ( + (note.text.includes('$[x2')) || + (note.text.includes('$[x3')) || + (note.text.includes('$[x4')) || + (note.text.includes('$[scale')) || + (note.text.split('\n').length > 9) || + (note.text.length > 500) || + (note.files.length >= 5) || + (urls.length >= 4) + ); + + return collapsed; +} diff --git a/packages/frontend/src/scripts/collect-page-vars.ts b/packages/frontend/src/scripts/collect-page-vars.ts index 5096c0669e..79356e60eb 100644 --- a/packages/frontend/src/scripts/collect-page-vars.ts +++ b/packages/frontend/src/scripts/collect-page-vars.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/color.ts b/packages/frontend/src/scripts/color.ts index a11255ffd1..25ef41d9b7 100644 --- a/packages/frontend/src/scripts/color.ts +++ b/packages/frontend/src/scripts/color.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/confetti.ts b/packages/frontend/src/scripts/confetti.ts index 8e53a6ceeb..b394ba3e2a 100644 --- a/packages/frontend/src/scripts/confetti.ts +++ b/packages/frontend/src/scripts/confetti.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/contains.ts b/packages/frontend/src/scripts/contains.ts index 6137c06e85..b50ce4128c 100644 --- a/packages/frontend/src/scripts/contains.ts +++ b/packages/frontend/src/scripts/contains.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/copy-to-clipboard.ts b/packages/frontend/src/scripts/copy-to-clipboard.ts index 08f5b52dae..3884d4a20a 100644 --- a/packages/frontend/src/scripts/copy-to-clipboard.ts +++ b/packages/frontend/src/scripts/copy-to-clipboard.ts @@ -1,11 +1,38 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ /** * Clipboardに値をコピー(TODO: 文字列以外も対応) */ -export function copyToClipboard(input: string | null) { - if (input) navigator.clipboard.writeText(input); -} +export default val => { + // 空div 生成 + const tmp = document.createElement('div'); + // 選択用のタグ生成 + const pre = document.createElement('pre'); + + // 親要素のCSSで user-select: none だとコピーできないので書き換える + pre.style.webkitUserSelect = 'auto'; + pre.style.userSelect = 'auto'; + + tmp.appendChild(pre).textContent = val; + + // 要素を画面外へ + const s = tmp.style; + s.position = 'fixed'; + s.right = '200%'; + + // body に追加 + document.body.appendChild(tmp); + // 要素を選択 + document.getSelection().selectAllChildren(tmp); + + // クリップボードにコピー + const result = document.execCommand('copy'); + + // 要素削除 + document.body.removeChild(tmp); + + return result; +}; diff --git a/packages/frontend/src/scripts/device-kind.ts b/packages/frontend/src/scripts/device-kind.ts index 7c33f8ccee..3843052a24 100644 --- a/packages/frontend/src/scripts/device-kind.ts +++ b/packages/frontend/src/scripts/device-kind.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -11,13 +11,6 @@ const ua = navigator.userAgent.toLowerCase(); const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700); const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua); -const isIPhone = /iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; -// navigator.platform may be deprecated but this check is still required -const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; -const isIos = /ipad|iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; - -export const isFullscreenNotSupported = isIPhone || isIos; - export const deviceKind: 'smartphone' | 'tablet' | 'desktop' = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind : isSmartphone ? 'smartphone' : isTablet ? 'tablet' diff --git a/packages/frontend-shared/js/emoji-base.ts b/packages/frontend/src/scripts/emoji-base.ts similarity index 57% rename from packages/frontend-shared/js/emoji-base.ts rename to packages/frontend/src/scripts/emoji-base.ts index 858cd801de..46a13462a1 100644 --- a/packages/frontend-shared/js/emoji-base.ts +++ b/packages/frontend/src/scripts/emoji-base.ts @@ -1,11 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ const twemojiSvgBase = '/twemoji'; const fluentEmojiPngBase = '/fluent-emoji'; -const tossfaceSvgBase = '/tossface'; export function char2twemojiFilePath(char: string): string { let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); @@ -20,18 +19,7 @@ export function char2fluentEmojiFilePath(char: string): string { // Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25 if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char); if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x != null && x.length > 0); - const fileName = (codes as string[]).map(x => x.padStart(4, '0')).join('-'); + codes = codes.filter(x => x && x.length); + const fileName = codes.map(x => x!.padStart(4, '0')).join('-'); return `${fluentEmojiPngBase}/${fileName}.png`; } - -export function char2tossfaceFilePath(char: string): string { - let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); - // Twemoji is the only emoji font which still supports the shibuya 50 emoji to this day - if (codes[0]?.startsWith('e50a')) return char2twemojiFilePath(char); - // Tossface does not use the fe0f modifier - codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.join('-'); - return `${tossfaceSvgBase}/${fileName}.svg`; -} diff --git a/packages/frontend/src/scripts/emoji-picker.ts b/packages/frontend/src/scripts/emoji-picker.ts deleted file mode 100644 index 14b5cbf35e..0000000000 --- a/packages/frontend/src/scripts/emoji-picker.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { defineAsyncComponent, Ref, ref } from 'vue'; -import { popup } from '@/os.js'; -import { defaultStore } from '@/store.js'; - -/** - * 絵文字ピッカーを表示する。 - * 類似の機能として{@link ReactionPicker}が存在しているが、この機能とは動きが異なる。 - * 投稿フォームなどで絵文字を選択する時など、絵文字ピックアップ後でもダイアログが消えずに残り、 - * 一度表示したダイアログを連続で使用できることが望ましいシーンでの利用が想定される。 - */ -class EmojiPicker { - private src: Ref = ref(null); - private manualShowing = ref(false); - private onChosen?: (emoji: string) => void; - private onClosed?: () => void; - - constructor() { - // nop - } - - public async init() { - const emojisRef = defaultStore.reactiveState.pinnedEmojis; - await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { - src: this.src, - pinnedEmojis: emojisRef, - asReactionPicker: false, - manualShowing: this.manualShowing, - choseAndClose: false, - }, { - done: emoji => { - if (this.onChosen) this.onChosen(emoji); - }, - close: () => { - this.manualShowing.value = false; - }, - closed: () => { - this.src.value = null; - if (this.onClosed) this.onClosed(); - }, - }); - } - - public show( - src: HTMLElement, - onChosen?: EmojiPicker['onChosen'], - onClosed?: EmojiPicker['onClosed'], - ) { - this.src.value = src; - this.manualShowing.value = true; - this.onChosen = onChosen; - this.onClosed = onClosed; - } -} - -export const emojiPicker = new EmojiPicker(); diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts new file mode 100644 index 0000000000..4159da84c8 --- /dev/null +++ b/packages/frontend/src/scripts/emojilist.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const; + +export type UnicodeEmojiDef = { + name: string; + char: string; + category: typeof unicodeEmojiCategories[number]; +} + +// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb +import _emojilist from '../emojilist.json'; + +export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ + name: x[1] as string, + char: x[0] as string, + category: unicodeEmojiCategories[x[2]], +})); + +const _indexByChar = new Map(); +const _charGroupByCategory = new Map(); +for (let i = 0; i < emojilist.length; i++) { + const emo = emojilist[i]; + _indexByChar.set(emo.char, i); + + if (_charGroupByCategory.has(emo.category)) { + _charGroupByCategory.get(emo.category)?.push(emo.char); + } else { + _charGroupByCategory.set(emo.category, [emo.char]); + } +} + +export const emojiCharByCategory = _charGroupByCategory; + +export function getEmojiName(char: string): string | null { + const idx = _indexByChar.get(char); + if (idx == null) { + return null; + } else { + return emojilist[idx].name; + } +} diff --git a/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts similarity index 85% rename from packages/frontend-shared/js/extract-avg-color-from-blurhash.ts rename to packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts index 992f6e9a16..57b296ab2a 100644 --- a/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts +++ b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/extract-mentions.ts b/packages/frontend/src/scripts/extract-mentions.ts index 89a5ce1df8..74ce45d324 100644 --- a/packages/frontend/src/scripts/extract-mentions.ts +++ b/packages/frontend/src/scripts/extract-mentions.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ // test is located in test/extract-mentions -import * as mfm from '@transfem-org/sfm-js'; +import * as mfm from 'mfm-js'; export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { // TODO: 重複を削除 diff --git a/packages/frontend/src/scripts/extract-url-from-mfm.ts b/packages/frontend/src/scripts/extract-url-from-mfm.ts index a4c84aa740..c1ed9338f8 100644 --- a/packages/frontend/src/scripts/extract-url-from-mfm.ts +++ b/packages/frontend/src/scripts/extract-url-from-mfm.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import * as mfm from '@transfem-org/sfm-js'; +import * as mfm from 'mfm-js'; import { unique } from '@/scripts/array.js'; // unique without hash diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts deleted file mode 100644 index a5972276e2..0000000000 --- a/packages/frontend/src/scripts/favicon-dot.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* - * SPDX-FileCopyrightText: leah and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import tinycolor from 'tinycolor2'; - -class FavIconDot { - private readonly canvas: HTMLCanvasElement; - private src: string | null = null; - private ctx: CanvasRenderingContext2D | null = null; - private faviconImage: HTMLImageElement | null = null; - private faviconEL: HTMLLinkElement | undefined; - private hasLoaded: Promise | undefined; - - constructor() { - this.canvas = document.createElement('canvas'); - } - - /** - * Must be called before calling any other functions - */ - public async setup() { - const element: HTMLLinkElement = await this.getOrMakeFaviconElement(); - - this.faviconEL = element; - this.src = this.faviconEL.getAttribute('href'); - this.ctx = this.canvas.getContext('2d'); - - this.faviconImage = document.createElement('img'); - - this.hasLoaded = new Promise((resolve, reject) => { - (this.faviconImage as HTMLImageElement).addEventListener('load', () => { - this.canvas.width = (this.faviconImage as HTMLImageElement).width; - this.canvas.height = (this.faviconImage as HTMLImageElement).height; - resolve(); - }); - (this.faviconImage as HTMLImageElement).addEventListener('error', () => { - reject('Failed to create favicon img element'); - }); - }); - - this.faviconImage.src = this.faviconEL.href; - } - - private async getOrMakeFaviconElement(): Promise { - return new Promise((resolve, reject) => { - const favicon = (document.querySelector('link[rel=icon]') ?? this.createFaviconElem()) as HTMLLinkElement; - favicon.addEventListener('load', () => { - resolve(favicon); - }); - - favicon.onerror = () => { - reject('Failed to load favicon'); - }; - resolve(favicon); - }); - } - - private createFaviconElem() { - const newLink = document.createElement('link'); - newLink.setAttribute('rel', 'icon'); - newLink.setAttribute('href', '/favicon.ico'); - newLink.setAttribute('type', 'image/x-icon'); - - document.head.appendChild(newLink); - return newLink; - } - - private drawIcon() { - if (!this.ctx || !this.faviconImage) return; - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.ctx.drawImage(this.faviconImage, 0, 0, this.faviconImage.width, this.faviconImage.height); - } - - private drawDot() { - if (!this.ctx || !this.faviconImage) return; - this.ctx.beginPath(); - this.ctx.arc(this.faviconImage.width - 10, 10, 10, 0, 2 * Math.PI); - const computedStyle = getComputedStyle(document.documentElement); - this.ctx.fillStyle = tinycolor(computedStyle.getPropertyValue('--navIndicator')).toHexString(); - this.ctx.strokeStyle = 'white'; - this.ctx.fill(); - this.ctx.stroke(); - } - - private setFavicon() { - if (this.faviconEL) this.faviconEL.href = this.canvas.toDataURL('image/png'); - } - - public async setVisible(isVisible: boolean) { - // Wait for it to have loaded the icon - await this.hasLoaded; - this.drawIcon(); - if (isVisible) this.drawDot(); - this.setFavicon(); - } - - public async worksOnInstance() { - try { - // Wait for it to have loaded the icon - await this.hasLoaded; - this.drawIcon(); - this.drawDot(); - this.canvas.toDataURL('image/png'); - } catch (error) { - return false; - } - return true; - } -} - -let icon: FavIconDot | undefined = undefined; - -export async function setFavIconDot(visible: boolean) { - const setIconVisibility = async () => { - if (!icon) { - icon = new FavIconDot(); - await icon.setup(); - } - - try { - (icon as FavIconDot).setVisible(visible); - } catch (error) { - //Probably failed due to CORS and a dirty canvas - } - }; - - // If document is already loaded, set visibility immediately - if (document.readyState === 'complete') { - await setIconVisibility(); - } else { - // Otherwise, set visibility when window loads - window.addEventListener('load', setIconVisibility); - } -} - -export async function worksOnInstance() { - if (!icon) { - icon = new FavIconDot(); - await icon.setup(); - } - - return await icon.worksOnInstance(); -} diff --git a/packages/frontend/src/scripts/focus-trap.ts b/packages/frontend/src/scripts/focus-trap.ts deleted file mode 100644 index fb7caea830..0000000000 --- a/packages/frontend/src/scripts/focus-trap.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; - -const focusTrapElements = new Set(); -const ignoreElements = [ - 'script', - 'style', -]; - -function containsFocusTrappedElements(el: HTMLElement): boolean { - return Array.from(focusTrapElements).some((focusTrapElement) => { - return el.contains(focusTrapElement); - }); -} - -function getZIndex(el: HTMLElement): number { - const zIndex = parseInt(window.getComputedStyle(el).zIndex || '0', 10); - if (isNaN(zIndex)) { - return 0; - } - return zIndex; -} - -function getHighestZIndexElement(): { el: HTMLElement; zIndex: number; } | null { - let highestZIndexElement: HTMLElement | null = null; - let highestZIndex = -Infinity; - - focusTrapElements.forEach((el) => { - const zIndex = getZIndex(el); - if (zIndex > highestZIndex) { - highestZIndex = zIndex; - highestZIndexElement = el; - } - }); - - return highestZIndexElement == null ? null : { - el: highestZIndexElement, - zIndex: highestZIndex, - }; -} - -function releaseFocusTrap(el: HTMLElement): void { - focusTrapElements.delete(el); - if (el.inert === true) { - el.inert = false; - } - - const highestZIndexElement = getHighestZIndexElement(); - - if (el.parentElement != null && el !== document.body) { - el.parentElement.childNodes.forEach((siblingNode) => { - const siblingEl = getHTMLElementOrNull(siblingNode); - if (!siblingEl) return; - if ( - siblingEl !== el && - ( - highestZIndexElement == null || - siblingEl === highestZIndexElement.el || - siblingEl.contains(highestZIndexElement.el) - ) - ) { - siblingEl.inert = false; - } else if ( - highestZIndexElement != null && - siblingEl !== highestZIndexElement.el && - !siblingEl.contains(highestZIndexElement.el) && - !ignoreElements.includes(siblingEl.tagName.toLowerCase()) - ) { - siblingEl.inert = true; - } else { - siblingEl.inert = false; - } - }); - releaseFocusTrap(el.parentElement); - } -} - -export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void; -export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; }; -export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void { - const highestZIndexElement = getHighestZIndexElement(); - - const highestZIndex = highestZIndexElement == null ? -Infinity : highestZIndexElement.zIndex; - const zIndex = getZIndex(el); - - // If the element has a lower z-index than the highest z-index element, focus trap the highest z-index element instead - // Focus trapping for this element will be done in the release function - if (!parent && zIndex < highestZIndex) { - focusTrapElements.add(el); - if (highestZIndexElement) { - focusTrap(highestZIndexElement.el, hasInteractionWithOtherFocusTrappedEls); - } - return { - release: () => { - releaseFocusTrap(el); - }, - }; - } - - if (el.inert === true) { - el.inert = false; - } - - if (el.parentElement != null && el !== document.body) { - el.parentElement.childNodes.forEach((siblingNode) => { - const siblingEl = getHTMLElementOrNull(siblingNode); - if (!siblingEl) return; - if ( - siblingEl !== el && - ( - hasInteractionWithOtherFocusTrappedEls === false || - (!focusTrapElements.has(siblingEl) && !containsFocusTrappedElements(siblingEl)) - ) && - !ignoreElements.includes(siblingEl.tagName.toLowerCase()) - ) { - siblingEl.inert = true; - } - }); - focusTrap(el.parentElement, hasInteractionWithOtherFocusTrappedEls, true); - } - - if (!parent) { - focusTrapElements.add(el); - - return { - release: () => { - releaseFocusTrap(el); - }, - }; - } -} diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts index 81278b17ea..6a31ebd431 100644 --- a/packages/frontend/src/scripts/focus.ts +++ b/packages/frontend/src/scripts/focus.ts @@ -1,80 +1,32 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js'; -import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js'; - -type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement; - -export const isFocusable = (input: MaybeHTMLElement | null | undefined): input is HTMLElement => { - if (input == null || !(input instanceof HTMLElement)) return false; - - if (input.tabIndex < 0) return false; - if ('disabled' in input && input.disabled === true) return false; - if ('readonly' in input && input.readonly === true) return false; - - if (!input.ownerDocument.contains(input)) return false; - - const style = window.getComputedStyle(input); - if (style.display === 'none') return false; - if (style.visibility === 'hidden') return false; - if (style.opacity === '0') return false; - if (style.pointerEvents === 'none') return false; - - return true; -}; - -export const focusPrev = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => { - const element = self ? input : getElementOrNull(input)?.previousElementSibling; - if (element == null) return; - if (isFocusable(element)) { - focusOrScroll(element, scroll); - } else { - focusPrev(element, false, scroll); - } -}; - -export const focusNext = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => { - const element = self ? input : getElementOrNull(input)?.nextElementSibling; - if (element == null) return; - if (isFocusable(element)) { - focusOrScroll(element, scroll); - } else { - focusNext(element, false, scroll); - } -}; - -export const focusParent = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => { - const element = self ? input : getNodeOrNull(input)?.parentElement; - if (element == null) return; - if (isFocusable(element)) { - focusOrScroll(element, scroll); - } else { - focusParent(element, false, scroll); - } -}; - -const focusOrScroll = (element: HTMLElement, scroll: boolean) => { - if (scroll) { - const scrollContainer = getScrollContainer(element) ?? document.documentElement; - const scrollContainerTop = getScrollPosition(scrollContainer); - const stickyTop = getStickyTop(element, scrollContainer); - const stickyBottom = getStickyBottom(element, scrollContainer); - const top = element.getBoundingClientRect().top; - const bottom = element.getBoundingClientRect().bottom; - - let scrollTo = scrollContainerTop; - if (top < stickyTop) { - scrollTo += top - stickyTop; - } else if (bottom > window.innerHeight - stickyBottom) { - scrollTo += bottom - window.innerHeight + stickyBottom; +export function focusPrev(el: Element | null, self = false, scroll = true) { + if (el == null) return; + if (!self) el = el.previousElementSibling; + if (el) { + if (el.hasAttribute('tabindex')) { + (el as HTMLElement).focus({ + preventScroll: !scroll, + }); + } else { + focusPrev(el.previousElementSibling, true); } - scrollContainer.scrollTo({ top: scrollTo, behavior: 'instant' }); } +} - if (document.activeElement !== element) { - element.focus({ preventScroll: true }); +export function focusNext(el: Element | null, self = false, scroll = true) { + if (el == null) return; + if (!self) el = el.nextElementSibling; + if (el) { + if (el.hasAttribute('tabindex')) { + (el as HTMLElement).focus({ + preventScroll: !scroll, + }); + } else { + focusPrev(el.nextElementSibling, true); + } } -}; +} diff --git a/packages/frontend/src/scripts/following-feed-utils.ts b/packages/frontend/src/scripts/following-feed-utils.ts deleted file mode 100644 index 39f17949d6..0000000000 --- a/packages/frontend/src/scripts/following-feed-utils.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* - * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { computed, Ref, WritableComputedRef } from 'vue'; -import { defaultStore } from '@/store.js'; -import { deepMerge } from '@/scripts/merge.js'; -import { PageHeaderItem } from '@/types/page-header.js'; -import { i18n } from '@/i18n.js'; -import { popupMenu } from '@/os.js'; -import { MenuItem } from '@/types/menu.js'; - -export const followingTab = 'following' as const; -export const mutualsTab = 'mutuals' as const; -export const followersTab = 'followers' as const; -export const followingFeedTabs = [followingTab, mutualsTab, followersTab] as const; -export type FollowingFeedTab = typeof followingFeedTabs[number]; - -export function followingTabName(tab: FollowingFeedTab): string; -export function followingTabName(tab: FollowingFeedTab | null | undefined): null; -export function followingTabName(tab: FollowingFeedTab | null | undefined): string | null { - if (tab === followingTab) return i18n.ts.following; - if (tab === followersTab) return i18n.ts.followers; - if (tab === mutualsTab) return i18n.ts.mutuals; - return null; -} - -export function followingTabIcon(tab: FollowingFeedTab | null | undefined): string { - if (tab === followersTab) return 'ph-user ph-bold ph-lg'; - if (tab === mutualsTab) return 'ph-user-switch ph-bold ph-lg'; - return 'ph-user-check ph-bold ph-lg'; -} - -export type FollowingFeedModel = { - [Key in keyof FollowingFeedState]: WritableComputedRef; -} - -export interface FollowingFeedState { - withNonPublic: boolean, - withQuotes: boolean, - withBots: boolean, - withReplies: boolean, - onlyFiles: boolean, - userList: FollowingFeedTab, - remoteWarningDismissed: boolean, -} - -export const defaultFollowingFeedState: FollowingFeedState = { - withNonPublic: false, - withQuotes: false, - withBots: true, - withReplies: false, - onlyFiles: false, - userList: followingTab, - remoteWarningDismissed: false, -}; - -interface StorageInterface = Partial> { - readonly state: Partial; - readonly reactiveState: Ref>; - save(updated: T): void; -} - -export function createHeaderItem(storage?: Ref): PageHeaderItem { - const menu = createOptionsMenu(storage); - return { - icon: 'ti ti-dots', - text: i18n.ts.options, - handler: ev => popupMenu(menu, ev.currentTarget ?? ev.target), - }; -} - -export function createOptionsMenu(storage?: Ref): MenuItem[] { - const { - userList, - withNonPublic, - withQuotes, - withBots, - withReplies, - onlyFiles, - } = createModel(storage); - - return [ - { - type: 'switch', - text: i18n.ts.showNonPublicNotes, - ref: withNonPublic, - disabled: computed(() => userList.value === followersTab), - }, - { - type: 'switch', - text: i18n.ts.showQuotes, - ref: withQuotes, - }, - { - type: 'switch', - text: i18n.ts.showBots, - ref: withBots, - }, - { - type: 'switch', - text: i18n.ts.showReplies, - ref: withReplies, - disabled: onlyFiles, - }, - { - type: 'divider', - }, - { - type: 'switch', - text: i18n.ts.fileAttachedOnly, - ref: onlyFiles, - disabled: withReplies, - }, - ]; -} - -export function createModel(storage?: Ref): FollowingFeedModel { - // eslint-disable-next-line no-param-reassign - storage ??= createDefaultStorage(); - - // Based on timeline.saveTlFilter() - const saveFollowingFilter = (key: K, value: FollowingFeedState[K]) => { - const state = deepMerge(storage.value.state, defaultFollowingFeedState); - const out = deepMerge({ [key]: value }, state); - storage.value.save(out); - }; - - const userList: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.userList ?? defaultFollowingFeedState.userList, - set: value => saveFollowingFilter('userList', value), - }); - const withNonPublic: WritableComputedRef = computed({ - get: () => { - if (userList.value === 'followers') return false; - return storage.value.reactiveState.value.withNonPublic ?? defaultFollowingFeedState.withNonPublic; - }, - set: value => saveFollowingFilter('withNonPublic', value), - }); - const withQuotes: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.withQuotes ?? defaultFollowingFeedState.withQuotes, - set: value => saveFollowingFilter('withQuotes', value), - }); - const withBots: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.withBots ?? defaultFollowingFeedState.withBots, - set: value => saveFollowingFilter('withBots', value), - }); - const withReplies: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.withReplies ?? defaultFollowingFeedState.withReplies, - set: value => saveFollowingFilter('withReplies', value), - }); - const onlyFiles: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.onlyFiles ?? defaultFollowingFeedState.onlyFiles, - set: value => saveFollowingFilter('onlyFiles', value), - }); - const remoteWarningDismissed: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.remoteWarningDismissed ?? defaultFollowingFeedState.remoteWarningDismissed, - set: value => saveFollowingFilter('remoteWarningDismissed', value), - }); - - return { - userList, - withNonPublic, - withQuotes, - withBots, - withReplies, - onlyFiles, - remoteWarningDismissed, - }; -} - -function createDefaultStorage() { - return computed(() => ({ - state: defaultStore.state.followingFeed, - reactiveState: defaultStore.reactiveState.followingFeed, - save(updated: typeof defaultStore.state.followingFeed) { - return defaultStore.set('followingFeed', updated); - }, - })); -} diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts index 242a504c3b..222fd9b0b7 100644 --- a/packages/frontend/src/scripts/form.ts +++ b/packages/frontend/src/scripts/form.ts @@ -1,88 +1,51 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import * as Misskey from 'misskey-js'; - -type EnumItem = string | { - label: string; - value: string; -}; - -type Hidden = boolean | ((v: any) => boolean); - +type EnumItem = string | {label: string; value: string;}; export type FormItem = { label?: string; type: 'string'; default: string | null; - description?: string; - required?: boolean; - hidden?: Hidden; + hidden?: boolean; multiline?: boolean; - treatAsMfm?: boolean; } | { label?: string; type: 'number'; default: number | null; - description?: string; - required?: boolean; - hidden?: Hidden; + hidden?: boolean; step?: number; } | { label?: string; type: 'boolean'; default: boolean | null; - description?: string; - hidden?: Hidden; + hidden?: boolean; } | { label?: string; type: 'enum'; default: string | null; - required?: boolean; - hidden?: Hidden; + hidden?: boolean; enum: EnumItem[]; } | { label?: string; type: 'radio'; default: unknown | null; - required?: boolean; - hidden?: Hidden; + hidden?: boolean; options: { label: string; value: unknown; }[]; -} | { - label?: string; - type: 'range'; - default: number | null; - description?: string; - required?: boolean; - step?: number; - min: number; - max: number; - textConverter?: (value: number) => string; - hidden?: Hidden; } | { label?: string; type: 'object'; default: Record | null; - hidden: Hidden; + hidden: true; } | { label?: string; type: 'array'; default: unknown[] | null; - hidden: Hidden; -} | { - type: 'button'; - content?: string; - hidden?: Hidden; - action: (ev: MouseEvent, v: any) => void; -} | { - type: 'drive-file'; - defaultFileId?: string | null; - hidden?: Hidden; - validate?: (v: Misskey.entities.DriveFile) => Promise; + hidden: true; }; export type Form = Record; @@ -92,12 +55,10 @@ type GetItemType = Item['type'] extends 'number' ? number : Item['type'] extends 'boolean' ? boolean : Item['type'] extends 'radio' ? unknown : - Item['type'] extends 'range' ? number : Item['type'] extends 'enum' ? string : Item['type'] extends 'array' ? unknown[] : - Item['type'] extends 'object' ? Record : - Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined : - never; + Item['type'] extends 'object' ? Record + : never; export type GetFormResultType = { [P in keyof F]: GetItemType; diff --git a/packages/frontend/src/scripts/format-time-string.ts b/packages/frontend/src/scripts/format-time-string.ts index 35ad77d982..918996dd10 100644 --- a/packages/frontend/src/scripts/format-time-string.ts +++ b/packages/frontend/src/scripts/format-time-string.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts index a85ee01e26..54654980f2 100644 --- a/packages/frontend/src/scripts/gen-search-query.ts +++ b/packages/frontend/src/scripts/gen-search-query.ts @@ -1,10 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import * as Misskey from 'misskey-js'; -import { host as localHost } from '@@/js/config.js'; +import { host as localHost } from '@/config.js'; export async function genSearchQuery(v: any, q: string) { let host: string; @@ -18,7 +18,7 @@ export async function genSearchQuery(v: any, q: string) { host = at; } } else { - const user = await v.api('users/show', Misskey.acct.parse(at)).catch(x => null); + const user = await v.os.api('users/show', Misskey.acct.parse(at)).catch(x => null); if (user) { userId = user.id; } else { diff --git a/packages/frontend/src/scripts/get-account-from-id.ts b/packages/frontend/src/scripts/get-account-from-id.ts index 40afa10f2d..346d283572 100644 --- a/packages/frontend/src/scripts/get-account-from-id.ts +++ b/packages/frontend/src/scripts/get-account-from-id.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/get-appear-note.ts b/packages/frontend/src/scripts/get-appear-note.ts deleted file mode 100644 index 40ce80eac9..0000000000 --- a/packages/frontend/src/scripts/get-appear-note.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; - -export function getAppearNote(note: Misskey.entities.Note) { - return Misskey.note.isPureRenote(note) ? note.renote : note; -} diff --git a/packages/frontend/src/scripts/get-dom-node-or-null.ts b/packages/frontend/src/scripts/get-dom-node-or-null.ts deleted file mode 100644 index fbf54675fd..0000000000 --- a/packages/frontend/src/scripts/get-dom-node-or-null.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const getNodeOrNull = (input: unknown): Node | null => { - if (input instanceof Node) return input; - return null; -}; - -export const getElementOrNull = (input: unknown): Element | null => { - if (input instanceof Element) return input; - return null; -}; - -export const getHTMLElementOrNull = (input: unknown): HTMLElement | null => { - if (input instanceof HTMLElement) return input; - return null; -}; diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index c8ab9238d3..87f3886847 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -1,15 +1,14 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import * as Misskey from 'misskey-js'; import { defineAsyncComponent } from 'vue'; import { i18n } from '@/i18n.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import type { MenuItem } from '@/types/menu.js'; +import { MenuItem } from '@/types/menu.js'; import { defaultStore } from '@/store.js'; function rename(file: Misskey.entities.DriveFile) { @@ -19,7 +18,7 @@ function rename(file: Misskey.entities.DriveFile) { default: file.name, }).then(({ canceled, result: name }) => { if (canceled) return; - misskeyApi('drive/files/update', { + os.api('drive/files/update', { fileId: file.id, name: name, }); @@ -27,31 +26,21 @@ function rename(file: Misskey.entities.DriveFile) { } function describe(file: Misskey.entities.DriveFile) { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { + os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { default: file.comment ?? '', file: file, }, { done: caption => { - misskeyApi('drive/files/update', { + os.api('drive/files/update', { fileId: file.id, comment: caption.length === 0 ? null : caption, }); }, - closed: () => dispose(), - }); -} - -function move(file: Misskey.entities.DriveFile) { - os.selectDriveFolder(false).then(folder => { - misskeyApi('drive/files/update', { - fileId: file.id, - folderId: folder[0] ? folder[0].id : null, - }); - }); + }, 'closed'); } function toggleSensitive(file: Misskey.entities.DriveFile) { - misskeyApi('drive/files/update', { + os.api('drive/files/update', { fileId: file.id, isSensitive: !file.isSensitive, }).catch(err => { @@ -76,87 +65,75 @@ function addApp() { async function deleteFile(file: Misskey.entities.DriveFile) { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }), + text: i18n.t('driveFileDeleteConfirm', { name: file.name }), }); if (canceled) return; - misskeyApi('drive/files/delete', { + os.api('drive/files/delete', { fileId: file.id, }); } export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] { const isImage = file.type.startsWith('image/'); - - const menuItems: MenuItem[] = []; - - menuItems.push({ + let menu; + menu = [{ type: 'link', to: `/my/drive/file/${file.id}`, text: i18n.ts._fileViewer.title, - icon: 'ti ti-info-circle', - }, { type: 'divider' }, { + icon: 'ph-file-text ph-bold ph-lg', + }, null, { text: i18n.ts.rename, - icon: 'ti ti-forms', + icon: 'ph-textbox ph-bold ph-lg', action: () => rename(file), - }, { - text: i18n.ts.move, - icon: 'ti ti-folder-symlink', - action: () => move(file), }, { text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, - icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation', + icon: file.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-closed ph-bold ph-lg', action: () => toggleSensitive(file), }, { text: i18n.ts.describeFile, - icon: 'ti ti-text-caption', + icon: 'ph-text-indent ph-bold ph-lg', action: () => describe(file), - }); - - if (isImage) { - menuItems.push({ - text: i18n.ts.cropImage, - icon: 'ti ti-crop', - action: () => os.cropImage(file, { - aspectRatio: NaN, - uploadFolder: folder ? folder.id : folder, - }), - }); - } - - menuItems.push({ type: 'divider' }, { + }, ...isImage ? [{ + text: i18n.ts.cropImage, + icon: 'ph-crop ph-bold ph-lg', + action: () => os.cropImage(file, { + aspectRatio: NaN, + uploadFolder: folder ? folder.id : folder, + }), + }] : [], null, { text: i18n.ts.createNoteFromTheFile, - icon: 'ti ti-pencil', + icon: 'ph-pencil ph-bold ph-lg', action: () => os.post({ initialFiles: [file], }), }, { text: i18n.ts.copyUrl, - icon: 'ti ti-link', + icon: 'ph-link ph-bold ph-lg', action: () => copyUrl(file), }, { type: 'a', href: file.url, target: '_blank', text: i18n.ts.download, - icon: 'ti ti-download', + icon: 'ph-download ph-bold ph-lg', download: file.name, - }, { type: 'divider' }, { + }, null, { text: i18n.ts.delete, - icon: 'ti ti-trash', + icon: 'ph-trash ph-bold ph-lg', danger: true, action: () => deleteFile(file), - }); + }]; if (defaultStore.state.devMode) { - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-id', + menu = menu.concat([null, { + icon: 'ph-identification-card ph-bold ph-lg', text: i18n.ts.copyFileId, action: () => { copyToClipboard(file.id); }, - }); + }]); } - return menuItems; + return menu; } diff --git a/packages/frontend/src/scripts/get-embed-code.ts b/packages/frontend/src/scripts/get-embed-code.ts deleted file mode 100644 index 158ab9c7f8..0000000000 --- a/packages/frontend/src/scripts/get-embed-code.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { defineAsyncComponent } from 'vue'; -import { v4 as uuid } from 'uuid'; -import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js'; -import { url } from '@@/js/config.js'; -import * as os from '@/os.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js'; - -const MOBILE_THRESHOLD = 500; - -/** - * パラメータを正規化する(埋め込みコード作成用) - * @param params パラメータ - * @returns 正規化されたパラメータ - */ -export function normalizeEmbedParams(params: EmbedParams): Record { - // paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す - const normalizedParams: Record = {}; - for (const key in params) { - // デフォルトの値と同じならparamsに含めない - if (params[key] == null || params[key] === defaultEmbedParams[key]) { - continue; - } - switch (typeof params[key]) { - case 'number': - normalizedParams[key] = params[key].toString(); - break; - case 'boolean': - normalizedParams[key] = params[key] ? 'true' : 'false'; - break; - default: - normalizedParams[key] = params[key]; - break; - } - } - return normalizedParams; -} - -/** - * 埋め込みコードを生成(iframe IDの発番もやる) - */ -export function getEmbedCode(path: string, params?: EmbedParams): string { - const iframeId = 'v1_' + uuid(); // 将来embed.jsのバージョンが上がったとき用にv1_を付けておく - - let paramString = ''; - if (params) { - const searchParams = new URLSearchParams(normalizeEmbedParams(params)); - paramString = searchParams.toString() === '' ? '' : '?' + searchParams.toString(); - } - - const iframeCode = [ - ``, - ``, - ]; - return iframeCode.join('\n'); -} - -/** - * 埋め込みコードを生成してコピーする(カスタマイズ機能つき) - * - * カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください - */ -export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) { - const _params = { ...params }; - - if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) { - _params.maxHeight = 700; - } - - // PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー - if (window.innerWidth < MOBILE_THRESHOLD) { - copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params)); - os.success(); - } else { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), { - entity, - id, - params: _params, - }, { - closed: () => dispose(), - }); - } -} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 4f3fb65665..e64c08c0ab 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -1,77 +1,56 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { defineAsyncComponent, Ref, ShallowRef } from 'vue'; +import { defineAsyncComponent, Ref } from 'vue'; import * as Misskey from 'misskey-js'; import { claimAchievement } from './achievements.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { url } from '@@/js/config.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { url } from '@/config.js'; import { defaultStore, noteActions } from '@/store.js'; import { miLocalStorage } from '@/local-storage.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; -import { clipsCache, favoritedChannelsCache } from '@/cache.js'; -import type { MenuItem } from '@/types/menu.js'; +import { clipsCache } from '@/cache.js'; +import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; -import { isSupportShare } from '@/scripts/navigator.js'; -import { getAppearNote } from '@/scripts/get-appear-note.js'; -import { genEmbedCode } from '@/scripts/get-embed-code.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; isDeleted: Ref; currentClip?: Misskey.entities.Clip; }) { - function getClipName(clip: Misskey.entities.Clip) { - if ($i && clip.userId === $i.id && clip.notesCount != null) { - return `${clip.name} (${clip.notesCount}/${$i.policies.noteEachClipsLimit})`; - } else { - return clip.name; - } - } + const isRenote = ( + props.note.renote != null && + props.note.text == null && + props.note.fileIds.length === 0 && + props.note.poll == null + ); - const appearNote = getAppearNote(props.note); + const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note; const clips = await clipsCache.fetch(); - const menu: MenuItem[] = [...clips.map(clip => ({ - text: getClipName(clip), + return [...clips.map(clip => ({ + text: clip.name, action: () => { claimAchievement('noteClipped1'); os.promiseDialog( - misskeyApi('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), + os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), null, async (err) => { if (err.id === '734806c4-542c-463a-9311-15c512803965') { const confirm = await os.confirm({ type: 'warning', - text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }), + text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }), }); if (!confirm.canceled) { - os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }).then(() => { - clipsCache.set(clips.map(c => { - if (c.id === clip.id) { - return { - ...c, - notesCount: Math.max(0, ((c.notesCount ?? 0) - 1)), - }; - } else { - return c; - } - })); - }); + os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); if (props.currentClip?.id === clip.id) props.isDeleted.value = true; } - } else if (err.id === 'f0dba960-ff73-4615-8df4-d6ac5d9dc118') { - os.alert({ - type: 'error', - text: i18n.ts.clipNoteLimitExceeded, - }); } else { os.alert({ type: 'error', @@ -79,33 +58,20 @@ export async function getNoteClipMenu(props: { }); } }, - ).then(() => { - clipsCache.set(clips.map(c => { - if (c.id === clip.id) { - return { - ...c, - notesCount: (c.notesCount ?? 0) + 1, - }; - } else { - return c; - } - })); - }); + ); }, - })), { type: 'divider' }, { - icon: 'ti ti-plus', + })), null, { + icon: 'ph-plus ph-bold ph-lg', text: i18n.ts.createNew, action: async () => { const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', - default: null, label: i18n.ts.name, }, description: { type: 'string', required: false, - default: null, multiline: true, label: i18n.ts.description, }, @@ -125,32 +91,25 @@ export async function getNoteClipMenu(props: { os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); }, }]; - - return menu; } -export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem { +export function getAbuseNoteMenu(note: misskey.entities.Note, text: string): MenuItem { return { - icon: 'ti ti-exclamation-circle', + icon: 'ph-warning-circle ph-bold ph-lg', text, action: (): void => { - const localUrl = `${url}/notes/${note.id}`; - let noteInfo = ''; - if (note.url ?? note.uri != null) noteInfo = `Note: ${note.url ?? note.uri}\n`; - noteInfo += `Local Note: ${localUrl}\n`; - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { + const u = note.url ?? note.uri ?? `${url}/notes/${note.id}`; + os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { user: note.user, - initialComment: `${noteInfo}-----\n`, - }, { - closed: () => dispose(), - }); + initialComment: `Note: ${u}\n-----\n`, + }, {}, 'closed'); }, }; } -export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): MenuItem { +export function getCopyNoteLinkMenu(note: misskey.entities.Note, text: string): MenuItem { return { - icon: 'ti ti-link', + icon: 'ph-link ph-bold ph-lg', text, action: (): void => { copyToClipboard(`${url}/notes/${note.id}`); @@ -170,27 +129,22 @@ export function getCopyNoteOriginLinkMenu(note: misskey.entities.Note, text: str }; } -function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined { - if (note.url != null || note.uri != null) return undefined; - if (['specified', 'followers'].includes(note.visibility)) return undefined; - - return { - icon: 'ti ti-code', - text, - action: (): void => { - genEmbedCode('notes', note.id); - }, - }; -} - export function getNoteMenu(props: { note: Misskey.entities.Note; - translation: Ref; + menuButton: Ref; + translation: Ref; translating: Ref; isDeleted: Ref; currentClip?: Misskey.entities.Clip; }) { - const appearNote = getAppearNote(props.note); + const isRenote = ( + props.note.renote != null && + props.note.text == null && + props.note.fileIds.length === 0 && + props.note.poll == null + ); + + const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note; const cleanups = [] as (() => void)[]; @@ -201,7 +155,7 @@ export function getNoteMenu(props: { }).then(({ canceled }) => { if (canceled) return; - misskeyApi('notes/delete', { + os.api('notes/delete', { noteId: appearNote.id, }); @@ -218,7 +172,7 @@ export function getNoteMenu(props: { }).then(({ canceled }) => { if (canceled) return; - misskeyApi('notes/delete', { + os.api('notes/delete', { noteId: appearNote.id, }); @@ -278,7 +232,6 @@ export function getNoteMenu(props: { } async function unclip(): Promise { - if (!props.currentClip) return; os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id }); props.isDeleted.value = true; } @@ -288,7 +241,7 @@ export function getNoteMenu(props: { title: i18n.ts.numberOfDays, }); - if (canceled || days == null) return; + if (canceled) return; os.apiWithDialog('admin/promo/create', { noteId: appearNote.id, @@ -298,8 +251,8 @@ export function getNoteMenu(props: { function share(): void { navigator.share({ - title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }), - text: appearNote.text ?? '', + title: i18n.t('noteOf', { user: appearNote.user.name }), + text: appearNote.text, url: `${url}/notes/${appearNote.id}`, }); } @@ -311,7 +264,7 @@ export function getNoteMenu(props: { async function translate(): Promise { if (props.translation.value != null) return; props.translating.value = true; - const res = await misskeyApi('notes/translate', { + const res = await os.api('notes/translate', { noteId: appearNote.id, targetLang: miLocalStorage.getItem('lang') ?? navigator.language, }); @@ -319,230 +272,173 @@ export function getNoteMenu(props: { props.translation.value = res; } - const menuItems: MenuItem[] = []; - + let menu: MenuItem[]; if ($i) { - const statePromise = misskeyApi('notes/state', { + const statePromise = os.api('notes/state', { noteId: appearNote.id, }); - if (props.currentClip?.userId === $i.id) { - menuItems.push({ - icon: 'ti ti-backspace', - text: i18n.ts.unclip, - danger: true, - action: unclip, - }, { type: 'divider' }); - } - - menuItems.push({ - icon: 'ti ti-info-circle', - text: i18n.ts.details, - action: openDetail, - }, { - icon: 'ti ti-copy', - text: i18n.ts.copyContent, - action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); - - if (appearNote.url || appearNote.uri) { - menuItems.push( + menu = [ + ...( + props.currentClip?.userId === $i.id ? [{ + icon: 'ph-backspace ph-bold ph-lg', + text: i18n.ts.unclip, + danger: true, + action: unclip, + }, null] : [] + ), { + icon: 'ph-info ph-bold ph-lg', + text: i18n.ts.details, + action: openDetail, + }, { + icon: 'ph-copy ph-bold ph-lg', + text: i18n.ts.copyContent, + action: copyContent, + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) + , (appearNote.url || appearNote.uri) ? getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') - ); - menuItems.push({ - icon: 'ti ti-external-link', + : undefined, + (appearNote.url || appearNote.uri) ? { + icon: 'ph-arrow-square-out ph-bold ph-lg', text: i18n.ts.showOnRemote, action: () => { - window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); + window.open(appearNote.url ?? appearNote.uri, '_blank'); }, - }); - } else { - menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); - } - - if (isSupportShare()) { - menuItems.push({ - icon: 'ti ti-share', + } : undefined, + { + icon: 'ph-share-network ph-bold ph-lg', text: i18n.ts.share, action: share, - }); - } - - if ($i.policies.canUseTranslator && instance.translatorAvailable) { - menuItems.push({ - icon: 'ti ti-language-hiragana', + }, + $i && $i.policies.canUseTranslator && instance.translatorAvailable ? { + icon: 'ph-translate ph-bold ph-lg', text: i18n.ts.translate, action: translate, - }); - } - - menuItems.push({ type: 'divider' }); - - menuItems.push(statePromise.then(state => state.isFavorited ? { - icon: 'ti ti-star-off', - text: i18n.ts.unfavorite, - action: () => toggleFavorite(false), - } : { - icon: 'ti ti-star', - text: i18n.ts.favorite, - action: () => toggleFavorite(true), - })); - - menuItems.push({ - type: 'parent', - icon: 'ti ti-paperclip', - text: i18n.ts.clip, - children: () => getNoteClipMenu(props), - }); - - menuItems.push(statePromise.then(state => state.isMutedThread ? { - icon: 'ti ti-message-off', - text: i18n.ts.unmuteThread, - action: () => toggleThreadMute(false), - } : { - icon: 'ti ti-message-off', - text: i18n.ts.muteThread, - action: () => toggleThreadMute(true), - })); - - if (appearNote.userId === $i.id) { - if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) { - menuItems.push({ - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => togglePin(false), - }); - } else { - menuItems.push({ - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => togglePin(true), - }); - } - } - - menuItems.push({ - type: 'parent', - icon: 'ti ti-user', - text: i18n.ts.user, - children: async () => { - const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); - const { menu, cleanup } = getUserMenu(user); - cleanups.push(cleanup); - return menu; + } : undefined, + null, + statePromise.then(state => state.isFavorited ? { + icon: 'ph-star-half ph-bold ph-lg', + text: i18n.ts.unfavorite, + action: () => toggleFavorite(false), + } : { + icon: 'ph-star ph-bold ph-lg', + text: i18n.ts.favorite, + action: () => toggleFavorite(true), + }), + { + type: 'parent' as const, + icon: 'ph-paperclip ph-bold ph-lg', + text: i18n.ts.clip, + children: () => getNoteClipMenu(props), }, - }); - - if (appearNote.userId !== $i.id) { - menuItems.push({ type: 'divider' }); - menuItems.push(getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse)); - } - - if (appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin)) { - menuItems.push({ type: 'divider' }); - menuItems.push({ - type: 'parent', - icon: 'ti ti-device-tv', - text: i18n.ts.channel, + statePromise.then(state => state.isMutedThread ? { + icon: 'ph-bell-slash ph-bold ph-lg', + text: i18n.ts.unmuteThread, + action: () => toggleThreadMute(false), + } : { + icon: 'ph-bell-slash ph-bold ph-lg', + text: i18n.ts.muteThread, + action: () => toggleThreadMute(true), + }), + appearNote.userId === $i.id ? ($i.pinnedNoteIds ?? []).includes(appearNote.id) ? { + icon: 'ph-push-pin ph-bold ph-lgned-off', + text: i18n.ts.unpin, + action: () => togglePin(false), + } : { + icon: 'ph-push-pin ph-bold ph-lg', + text: i18n.ts.pin, + action: () => togglePin(true), + } : undefined, + { + type: 'parent' as const, + icon: 'ph-user ph-bold ph-lg', + text: i18n.ts.user, children: async () => { - const channelChildMenu = [] as MenuItem[]; - - const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); - - if (channel.pinnedNoteIds.includes(appearNote.id)) { - channelChildMenu.push({ - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => os.apiWithDialog('channels/update', { - channelId: appearNote.channel!.id, - pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), - }), - }); - } else { - channelChildMenu.push({ - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => os.apiWithDialog('channels/update', { - channelId: appearNote.channel!.id, - pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], - }), - }); - } - return channelChildMenu; + const user = appearNote.userId === $i?.id ? $i : await os.api('users/show', { userId: appearNote.userId }); + const { menu, cleanup } = getUserMenu(user); + cleanups.push(cleanup); + return menu; }, - }); - } - - if (appearNote.userId === $i.id || $i.isModerator || $i.isAdmin) { - menuItems.push({ type: 'divider' }); - if (appearNote.userId === $i.id) { - menuItems.push({ - icon: 'ph-pencil-simple ph-bold ph-lg', + }, + /* + ...($i.isModerator || $i.isAdmin ? [ + null, + { + icon: 'ph-megaphone ph-bold ph-lg', + text: i18n.ts.promote, + action: promote + }] + : [] + ),*/ + ...(appearNote.userId !== $i.id ? [ + null, + appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined, + ] + : [] + ), + ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ + null, + appearNote.userId === $i.id ? { + icon: 'ph-pencil ph-bold ph-lg', text: i18n.ts.edit, action: edit, - }); - menuItems.push({ - icon: 'ti ti-edit', + } : undefined, + { + icon: 'ph-pencil-line ph-bold ph-lg', text: i18n.ts.deleteAndEdit, danger: true, action: delEdit, - }); - } - menuItems.push({ - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: del, - }); - } + }, + { + icon: 'ph-trash ph-bold ph-lg', + text: i18n.ts.delete, + danger: true, + action: del, + }] + : [] + )] + .filter(x => x !== undefined); } else { - menuItems.push({ - icon: 'ti ti-info-circle', + menu = [{ + icon: 'ph-info ph-bold ph-lg', text: i18n.ts.details, action: openDetail, }, { - icon: 'ti ti-copy', + icon: 'ph-copy ph-bold ph-lg', text: i18n.ts.copyContent, action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); - - if (appearNote.url || appearNote.uri) { - menuItems.push( - getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') - ); - menuItems.push({ - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); - }, - }); - } else { - menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); - } + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) + , (appearNote.url || appearNote.uri) ? + getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') + : undefined, + (appearNote.url || appearNote.uri) ? { + icon: 'ph-arrow-square-out ph-bold ph-lg', + text: i18n.ts.showOnRemote, + action: () => { + window.open(appearNote.url ?? appearNote.uri, '_blank'); + }, + } : undefined] + .filter(x => x !== undefined); } if (noteActions.length > 0) { - menuItems.push({ type: 'divider' }); - - menuItems.push(...noteActions.map(action => ({ - icon: 'ti ti-plug', + menu = menu.concat([null, ...noteActions.map(action => ({ + icon: 'ph-plug ph-bold ph-lg', text: action.title, action: () => { action.handler(appearNote); }, - }))); + }))]); } if (defaultStore.state.devMode) { - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-id', + menu = menu.concat([null, { + icon: 'ph-identification-card ph-bold ph-lg', text: i18n.ts.copyNoteId, action: () => { copyToClipboard(appearNote.id); - os.success(); }, - }); + }]); } const cleanup = () => { @@ -553,14 +449,15 @@ export function getNoteMenu(props: { }; return { - menu: menuItems, + menu, cleanup, }; } -type Visibility = (typeof Misskey.noteVisibilities)[number]; +type Visibility = 'public' | 'home' | 'followers' | 'specified'; -function smallerVisibility(a: Visibility, b: Visibility): Visibility { +// defaultStore.state.visibilityがstringなためstringも受け付けている +function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility { if (a === 'specified' || b === 'specified') return 'specified'; if (a === 'followers' || b === 'followers') return 'followers'; if (a === 'home' || b === 'home') return 'home'; @@ -570,32 +467,36 @@ function smallerVisibility(a: Visibility, b: Visibility): Visibility { export function getRenoteMenu(props: { note: Misskey.entities.Note; - renoteButton: ShallowRef; + renoteButton: Ref; mock?: boolean; }) { - const appearNote = getAppearNote(props.note); + const isRenote = ( + props.note.renote != null && + props.note.text == null && + props.note.fileIds.length === 0 && + props.note.poll == null + ); + + const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note; const channelRenoteItems: MenuItem[] = []; const normalRenoteItems: MenuItem[] = []; - const normalExternalChannelRenoteItems: MenuItem[] = []; if (appearNote.channel) { channelRenoteItems.push(...[{ text: i18n.ts.inChannelRenote, icon: 'ti ti-repeat', action: () => { - const el = props.renoteButton.value; + const el = props.renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - const { dispose } = os.popup(MkRippleEffect, { x, y }, { - end: () => dispose(), - }); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); } if (!props.mock) { - misskeyApi('notes/create', { + os.api('notes/create', { renoteId: appearNote.id, channelId: appearNote.channelId, }).then(() => { @@ -617,19 +518,17 @@ export function getRenoteMenu(props: { }]); } - if (!appearNote.channel || appearNote.channel.allowRenoteToExternal) { + if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) { normalRenoteItems.push(...[{ text: i18n.ts.renote, icon: 'ti ti-repeat', action: () => { - const el = props.renoteButton.value; + const el = props.renoteButton.value as HTMLElement | null | undefined; if (el) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - const { dispose } = os.popup(MkRippleEffect, { x, y }, { - end: () => dispose(), - }); + os.popup(MkRippleEffect, { x, y }, {}, 'end'); } const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; @@ -642,7 +541,7 @@ export function getRenoteMenu(props: { } if (!props.mock) { - misskeyApi('notes/create', { + os.api('notes/create', { localOnly, visibility, renoteId: appearNote.id, @@ -660,49 +559,13 @@ export function getRenoteMenu(props: { }); }, }]); - - normalExternalChannelRenoteItems.push({ - type: 'parent', - icon: 'ti ti-repeat', - text: appearNote.channel ? i18n.ts.renoteToOtherChannel : i18n.ts.renoteToChannel, - children: async () => { - const channels = await favoritedChannelsCache.fetch(); - return channels.filter((channel) => { - if (!appearNote.channelId) return true; - return channel.id !== appearNote.channelId; - }).map((channel) => ({ - text: channel.name, - action: () => { - const el = props.renoteButton.value; - if (el) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - const { dispose } = os.popup(MkRippleEffect, { x, y }, { - end: () => dispose(), - }); - } - - if (!props.mock) { - misskeyApi('notes/create', { - renoteId: appearNote.id, - channelId: channel.id, - }).then(() => { - os.toast(i18n.tsx.renotedToX({ name: channel.name })); - }); - } - }, - })); - }, - }); } + // nullを挟むことで区切り線を出せる const renoteItems = [ ...normalRenoteItems, - ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] as MenuItem[] : [], + ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [null] : [], ...channelRenoteItems, - ...(normalExternalChannelRenoteItems.length > 0 && (normalRenoteItems.length > 0 || channelRenoteItems.length > 0)) ? [{ type: 'divider' }] as MenuItem[] : [], - ...normalExternalChannelRenoteItems, ]; return { diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts index 58d486bf9b..1fd9f04d46 100644 --- a/packages/frontend/src/scripts/get-note-summary.ts +++ b/packages/frontend/src/scripts/get-note-summary.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,11 +10,7 @@ import { i18n } from '@/i18n.js'; * 投稿を表す文字列を取得します。 * @param {*} note (packされた)投稿 */ -export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { - if (note == null) { - return ''; - } - +export const getNoteSummary = (note: Misskey.entities.Note): string => { if (note.deletedAt) { return `(${i18n.ts.deletedNote})`; } @@ -27,14 +23,14 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { // 本文 if (note.cw != null) { - summary += `CW: ${note.cw}`; - } else if (note.text) { - summary += note.text; + summary += note.cw; + } else { + summary += note.text ? note.text : ''; } // ファイルが添付されているとき - if (note.files && note.files.length !== 0) { - summary += ` (${i18n.tsx.withNFiles({ n: note.files.length })})`; + if ((note.files || []).length !== 0) { + summary += ` (${i18n.t('withNFiles', { n: note.files.length })})`; } // 投票が添付されているとき @@ -44,7 +40,7 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { // 返信のとき if (note.replyId) { - if (note.reply && !note.cw) { + if (note.reply) { summary += `\n\nRE: ${getNoteSummary(note.reply)}`; } else { summary += '\n\nRE: ...'; @@ -53,7 +49,7 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { // Renoteのとき if (note.renoteId) { - if (note.renote && !note.cw) { + if (note.renote) { summary += `\n\nRN: ${getNoteSummary(note.renote)}`; } else { summary += '\n\nRN: ...'; diff --git a/packages/frontend/src/scripts/get-note-versions-menu.ts b/packages/frontend/src/scripts/get-note-versions-menu.ts index 345cec9018..46e3bab3a7 100644 --- a/packages/frontend/src/scripts/get-note-versions-menu.ts +++ b/packages/frontend/src/scripts/get-note-versions-menu.ts @@ -1,13 +1,7 @@ -/* - * SPDX-FileCopyrightText: marie and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { Ref, defineAsyncComponent } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { misskeyApi } from './misskey-api.js'; import { MenuItem } from '@/types/menu.js'; import { dateTimeFormat } from './intl-const.js'; @@ -27,17 +21,16 @@ export async function getNoteVersionsMenu(props: { const cleanups = [] as (() => void)[]; function openVersion(info): void { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/SkOldNoteWindow.vue')), { + os.popup(defineAsyncComponent(() => import('@/components/SkOldNoteWindow.vue')), { note: appearNote, oldText: info.text, updatedAt: info.oldDate ? info.oldDate : info.updatedAt, }, { - closed: () => dispose(), - }); + }, 'closed'); } const menu: MenuItem[] = []; - const statePromise = misskeyApi('notes/versions', { + const statePromise = os.api('notes/versions', { noteId: appearNote.id, }); @@ -46,9 +39,9 @@ export async function getNoteVersionsMenu(props: { const _time = edit.oldDate == null ? NaN : typeof edit.oldDate === 'number' ? edit.oldDate : (edit.oldDate instanceof Date ? edit.oldDate : new Date(edit.oldDate)).getTime(); - + menu.push({ - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ph-pencil ph-bold ph-lg', text: _time ? dateTimeFormat.format(_time) : dateTimeFormat.format(new Date(edit.updatedAt)), action: () => openVersion(edit), }); diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 090cffe203..41d0df1b72 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,20 +7,16 @@ import { toUnicode } from 'punycode'; import { defineAsyncComponent, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { host, url } from '@@/js/config.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { host, url } from '@/config.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore, userActions } from '@/store.js'; import { $i, iAmModerator } from '@/account.js'; -import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js'; -import { IRouter } from '@/nirax.js'; +import { mainRouter } from '@/router.js'; +import { Router } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; -import { mainRouter } from '@/router/main.js'; -import { genEmbedCode } from '@/scripts/get-embed-code.js'; -import type { MenuItem } from '@/types/menu.js'; -export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { +export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) { const meId = $i ? $i.id : null; const cleanups = [] as (() => void)[]; @@ -84,6 +80,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); } + async function toggleWithReplies() { + os.apiWithDialog('following/update', { + userId: user.id, + withReplies: !user.withReplies, + }).then(() => { + user.withReplies = !user.withReplies; + }); + } + async function toggleNotify() { os.apiWithDialog('following/update', { userId: user.id, @@ -94,16 +99,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter } function reportAbuse() { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { + os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { user: user, - }, { - closed: () => dispose(), - }); + }, {}, 'closed'); } async function getConfirmed(text: string): Promise { const confirm = await os.confirm({ - type: 'question', + type: 'warning', + title: 'confirm', text, }); @@ -115,7 +119,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter userId: user.id, }); } - + async function invalidateFollow() { if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return; @@ -127,7 +131,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter } async function editMemo(): Promise { - const userDetailed = await misskeyApi('users/show', { + const userDetailed = await os.api('users/show', { userId: user.id, }); const { canceled, result } = await os.form(i18n.ts.editMemo, { @@ -147,156 +151,117 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); } - const menuItems: MenuItem[] = []; - - menuItems.push({ - icon: 'ti ti-at', + let menu = [{ + icon: 'ph-at ph-bold ph-lg', text: i18n.ts.copyUsername, action: () => { copyToClipboard(`@${user.username}@${user.host ?? host}`); }, - }); - - if (notesSearchAvailable && (user.host == null || canSearchNonLocalNotes)) { - menuItems.push({ - icon: 'ti ti-search', - text: i18n.ts.searchThisUsersNotes, - action: () => { - router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`); - }, - }); - } - - if (iAmModerator) { - menuItems.push({ - icon: 'ti ti-user-exclamation', - text: i18n.ts.moderation, - action: () => { - router.push(`/admin/user/${user.id}`); - }, - }); - } - - menuItems.push({ - icon: 'ti ti-rss', + }, ...(iAmModerator ? [{ + icon: 'ph-warning-circle ph-bold ph-lg', + text: i18n.ts.moderation, + action: () => { + router.push(`/admin/user/${user.id}`); + }, + }] : []), { + icon: 'ph-rss ph-bold ph-lg', text: i18n.ts.copyRSS, action: () => { copyToClipboard(`${user.host ?? host}/@${user.username}.atom`); }, - }); - - if (user.host != null && user.url != null) { - menuItems.push({ - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - if (user.url == null) return; - window.open(user.url, '_blank', 'noopener'); - }, - }); - } else { - menuItems.push({ - icon: 'ti ti-code', - text: i18n.ts.genEmbedCode, - type: 'parent', - children: [{ - text: i18n.ts.noteOfThisUser, - action: () => { - genEmbedCode('user-timeline', user.id); - }, - }], // TODO: ユーザーカードの埋め込みなど - }); - } - - menuItems.push({ - icon: 'ti ti-share', + }, { + icon: 'ph-share-network ph-bold ph-lg', text: i18n.ts.copyProfileUrl, action: () => { const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; copyToClipboard(`${url}/${canonical}`); }, - }); - - if ($i) { - menuItems.push({ - icon: 'ti ti-mail', - text: i18n.ts.sendMessage, - action: () => { - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; - os.post({ specified: user, initialText: `${canonical} ` }); - }, - }, { type: 'divider' }, { - icon: 'ti ti-pencil', - text: i18n.ts.editMemo, - action: editMemo, - }, { - type: 'parent', - icon: 'ti ti-list', - text: i18n.ts.addToList, - children: async () => { - const lists = await userListsCache.fetch(); - return lists.map(list => { - const isListed = ref(list.userIds?.includes(user.id) ?? false); - cleanups.push(watch(isListed, () => { - if (isListed.value) { - os.apiWithDialog('users/lists/push', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds?.push(user.id); - }); - } else { - os.apiWithDialog('users/lists/pull', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds?.splice(list.userIds?.indexOf(user.id), 1); - }); - } - })); - - return { - type: 'switch', - text: list.name, - ref: isListed, - }; - }); - }, - }, { - type: 'parent', - icon: 'ti ti-antenna', - text: i18n.ts.addToAntenna, - children: async () => { - const antennas = await antennasCache.fetch(); - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; - return antennas.filter((a) => a.src === 'users').map(antenna => ({ - text: antenna.name, - action: async () => { - await os.apiWithDialog('antennas/update', { - antennaId: antenna.id, - name: antenna.name, - keywords: antenna.keywords, - excludeKeywords: antenna.excludeKeywords, - src: antenna.src, - userListId: antenna.userListId, - users: [...antenna.users, canonical], - caseSensitive: antenna.caseSensitive, - withReplies: antenna.withReplies, - withFile: antenna.withFile, - notify: antenna.notify, + }, ...(user.host ? [{ + icon: 'ph-share ph-bold ph-lg', + text: i18n.ts.openRemoteProfile, + action: () => { + open(`${user.uri}`, '_blank'); + }, + }] : []), { + icon: 'ph-envelope ph-bold ph-lg', + text: i18n.ts.sendMessage, + action: () => { + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; + os.post({ specified: user, initialText: `${canonical} ` }); + }, + }, null, { + icon: 'ph-pencil ph-bold ph-lg', + text: i18n.ts.editMemo, + action: () => { + editMemo(); + }, + }, { + type: 'parent', + icon: 'ph-list ph-bold ph-lg', + text: i18n.ts.addToList, + children: async () => { + const lists = await userListsCache.fetch(); + return lists.map(list => { + const isListed = ref(list.userIds.includes(user.id)); + cleanups.push(watch(isListed, () => { + if (isListed.value) { + os.apiWithDialog('users/lists/push', { + listId: list.id, + userId: user.id, + }).then(() => { + list.userIds.push(user.id); }); - antennasCache.delete(); - }, + } else { + os.apiWithDialog('users/lists/pull', { + listId: list.id, + userId: user.id, + }).then(() => { + list.userIds.splice(list.userIds.indexOf(user.id), 1); + }); + } })); - }, - }); - } + + return { + type: 'switch', + text: list.name, + ref: isListed, + }; + }); + }, + }, { + type: 'parent', + icon: 'ph-flying-saucer ph-bold ph-lg', + text: i18n.ts.addToAntenna, + children: async () => { + const antennas = await antennasCache.fetch(); + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; + return antennas.filter((a) => a.src === 'users').map(antenna => ({ + text: antenna.name, + action: async () => { + await os.apiWithDialog('antennas/update', { + antennaId: antenna.id, + name: antenna.name, + keywords: antenna.keywords, + excludeKeywords: antenna.excludeKeywords, + src: antenna.src, + userListId: antenna.userListId, + users: [...antenna.users, canonical], + caseSensitive: antenna.caseSensitive, + withReplies: antenna.withReplies, + withFile: antenna.withFile, + notify: antenna.notify, + }); + antennasCache.delete(); + }, + })); + }, + }] as any; if ($i && meId !== user.id) { if (iAmModerator) { - menuItems.push({ + menu = menu.concat([{ type: 'parent', - icon: 'ti ti-badges', + icon: 'ph-seal-check ph-bold ph-lg', text: i18n.ts.roles, children: async () => { const roles = await rolesCache.fetch(); @@ -305,7 +270,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter text: r.name, action: async () => { const { canceled, result: period } = await os.select({ - title: i18n.ts.period + ': ' + r.name, + title: i18n.ts.period, items: [{ value: 'indefinitely', text: i18n.ts.indefinitely, }, { @@ -332,108 +297,98 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }, })); }, - }); + }]); } // フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため //if (user.isFollowing) { - const withRepliesRef = ref(user.withReplies ?? false); - - menuItems.push({ - type: 'switch', - icon: 'ti ti-messages', - text: i18n.ts.showRepliesToOthersInTimeline, - ref: withRepliesRef, + menu = menu.concat([{ + icon: user.withReplies ? 'ph-envelope-open ph-bold ph-lg' : 'ph-envelope ph-bold ph-lg-off', + text: user.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline, + action: toggleWithReplies, }, { - icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off', + icon: user.notify === 'none' ? 'ph-bell ph-bold ph-lg' : 'ph-bell ph-bold ph-lg-off', text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes, action: toggleNotify, - }); - - watch(withRepliesRef, (withReplies) => { - misskeyApi('following/update', { - userId: user.id, - withReplies, - }).then(() => { - user.withReplies = withReplies; - }); - }); + }]); //} - menuItems.push({ type: 'divider' }, { - icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', + menu = menu.concat([null, { + icon: user.isMuted ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, action: toggleMute, }, { - icon: user.isRenoteMuted ? 'ti ti-repeat' : 'ti ti-repeat-off', + icon: user.isRenoteMuted ? 'ph-repeat ph-bold ph-lg' : 'ph-repeat ph-bold ph-lg-off', text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute, action: toggleRenoteMute, }, { - icon: 'ti ti-ban', + icon: 'ph-prohibit ph-bold ph-lg', text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, action: toggleBlock, - }); + }]); if (user.isFollowed) { - menuItems.push({ - icon: 'ti ti-link-off', + menu = menu.concat([{ + icon: 'ph-link ph-bold ph-lg-off', text: i18n.ts.breakFollow, action: invalidateFollow, - }); + }]); } - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-exclamation-circle', + menu = menu.concat([null, { + icon: 'ph-warning-circle ph-bold ph-lg', text: i18n.ts.reportAbuse, action: reportAbuse, - }); + }]); } if (user.host !== null) { - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-refresh', + menu = menu.concat([null, { + icon: 'ph-arrows-counter-clockwise ph-bold ph-lg', text: i18n.ts.updateRemoteUser, action: userInfoUpdate, - }); + }]); } - + if (defaultStore.state.devMode) { - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-id', + menu = menu.concat([null, { + icon: 'ph-identification-card ph-bold ph-lg', text: i18n.ts.copyUserId, action: () => { copyToClipboard(user.id); }, - }); + }]); } if ($i && meId === user.id) { - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-pencil', + menu = menu.concat([null, { + icon: 'ph-pencil ph-bold ph-lg', text: i18n.ts.editProfile, action: () => { router.push('/settings/profile'); }, - }); + }]); } if (userActions.length > 0) { - menuItems.push({ type: 'divider' }, ...userActions.map(action => ({ - icon: 'ti ti-plug', + menu = menu.concat([null, ...userActions.map(action => ({ + icon: 'ph-plug ph-bold ph-lg', text: action.title, action: () => { action.handler(user); }, - }))); + }))]); } + const cleanup = () => { + if (_DEV_) console.log('user menu cleanup', cleanups); + for (const cl of cleanups) { + cl(); + } + }; + return { - menu: menuItems, - cleanup: () => { - if (_DEV_) console.log('user menu cleanup', cleanups); - for (const cl of cleanups) { - cl(); - } - }, + menu, + cleanup, }; } diff --git a/packages/frontend/src/scripts/get-user-name.ts b/packages/frontend/src/scripts/get-user-name.ts index 56e91abba0..3ae80d7fc3 100644 --- a/packages/frontend/src/scripts/get-user-name.ts +++ b/packages/frontend/src/scripts/get-user-name.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts index 04fb235694..48c80c066b 100644 --- a/packages/frontend/src/scripts/hotkey.ts +++ b/packages/frontend/src/scripts/hotkey.ts @@ -1,172 +1,95 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { getHTMLElementOrNull } from "@/scripts/get-dom-node-or-null.js"; -//#region types -export type Keymap = Record; +import keyCode from './keycode.js'; -type CallbackFunction = (ev: KeyboardEvent) => unknown; +type Callback = (ev: KeyboardEvent) => void; -type CallbackObject = { - callback: CallbackFunction; - allowRepeat?: boolean; -}; +type Keymap = Record; type Pattern = { which: string[]; - ctrl: boolean; - alt: boolean; - shift: boolean; + ctrl?: boolean; + shift?: boolean; + alt?: boolean; }; type Action = { patterns: Pattern[]; - callback: CallbackFunction; - options: Required>; -}; -//#endregion - -//#region consts -const KEY_ALIASES = { - 'esc': 'Escape', - 'enter': 'Enter', - 'space': ' ', - 'up': 'ArrowUp', - 'down': 'ArrowDown', - 'left': 'ArrowLeft', - 'right': 'ArrowRight', - 'plus': ['+', ';'], + callback: Callback; + allowRepeat: boolean; }; -const MODIFIER_KEYS = ['ctrl', 'alt', 'shift']; +const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { + const result = { + patterns: [], + callback, + allowRepeat: true, + } as Action; -const IGNORE_ELEMENTS = ['input', 'textarea']; -//#endregion + if (patterns.match(/^\(.*\)$/) !== null) { + result.allowRepeat = false; + patterns = patterns.slice(1, -1); + } -//#region store -let latestHotkey: Pattern & { callback: CallbackFunction } | null = null; -//#endregion + result.patterns = patterns.split('|').map(part => { + const pattern = { + which: [], + ctrl: false, + alt: false, + shift: false, + } as Pattern; + + const keys = part.trim().split('+').map(x => x.trim().toLowerCase()); + for (const key of keys) { + switch (key) { + case 'ctrl': pattern.ctrl = true; break; + case 'alt': pattern.alt = true; break; + case 'shift': pattern.shift = true; break; + default: pattern.which = keyCode(key).map(k => k.toLowerCase()); + } + } + + return pattern; + }); + + return result; +}); + +const ignoreElements = ['input', 'textarea']; + +function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean { + const key = ev.key.toLowerCase(); + return patterns.some(pattern => pattern.which.includes(key) && + pattern.ctrl === ev.ctrlKey && + pattern.shift === ev.shiftKey && + pattern.alt === ev.altKey && + !ev.metaKey, + ); +} -//#region impl export const makeHotkey = (keymap: Keymap) => { const actions = parseKeymap(keymap); + return (ev: KeyboardEvent) => { - if ('pswp' in window && window.pswp != null) return; - if (document.activeElement != null) { - if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return; - if (getHTMLElementOrNull(document.activeElement)?.isContentEditable) return; + if (document.activeElement) { + if (ignoreElements.some(el => document.activeElement!.matches(el))) return; + if (document.activeElement.attributes['contenteditable']) return; } + for (const action of actions) { - if (matchPatterns(ev, action)) { + const matched = match(ev, action.patterns); + + if (matched) { + if (!action.allowRepeat && ev.repeat) return; + ev.preventDefault(); ev.stopPropagation(); action.callback(ev); - storePattern(ev, action.callback); + break; } } }; }; - -const parseKeymap = (keymap: Keymap) => { - return Object.entries(keymap).map(([rawPatterns, rawCallback]) => { - const patterns = parsePatterns(rawPatterns); - const callback = parseCallback(rawCallback); - const options = parseOptions(rawCallback); - return { patterns, callback, options } as const satisfies Action; - }); -}; - -const parsePatterns = (rawPatterns: keyof Keymap) => { - return rawPatterns.split('|').map(part => { - const keys = part.split('+').map(trimLower); - const which = parseKeyCode(keys.findLast(x => !MODIFIER_KEYS.includes(x))); - const ctrl = keys.includes('ctrl'); - const alt = keys.includes('alt'); - const shift = keys.includes('shift'); - return { which, ctrl, alt, shift } as const satisfies Pattern; - }); -}; - -const parseCallback = (rawCallback: Keymap[keyof Keymap]) => { - if (typeof rawCallback === 'object') { - return rawCallback.callback; - } - return rawCallback; -}; - -const parseOptions = (rawCallback: Keymap[keyof Keymap]) => { - const defaultOptions = { - allowRepeat: false, - } as const satisfies Action['options']; - if (typeof rawCallback === 'object') { - const { callback, ...rawOptions } = rawCallback; - const options = { ...defaultOptions, ...rawOptions }; - return { ...options } as const satisfies Action['options']; - } - return { ...defaultOptions } as const satisfies Action['options']; -}; - -const matchPatterns = (ev: KeyboardEvent, action: Action) => { - const { patterns, options, callback } = action; - if (ev.repeat && !options.allowRepeat) return false; - const key = ev.key.toLowerCase(); - return patterns.some(({ which, ctrl, shift, alt }) => { - if ( - options.allowRepeat === false && - latestHotkey != null && - latestHotkey.which.includes(key) && - latestHotkey.ctrl === ctrl && - latestHotkey.alt === alt && - latestHotkey.shift === shift && - latestHotkey.callback === callback - ) { - return false; - } - if (!which.includes(key)) return false; - if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false; - if (alt !== ev.altKey) return false; - if (shift !== ev.shiftKey) return false; - return true; - }); -}; - -let lastHotKeyStoreTimer: number | null = null; - -const storePattern = (ev: KeyboardEvent, callback: CallbackFunction) => { - if (lastHotKeyStoreTimer != null) { - clearTimeout(lastHotKeyStoreTimer); - } - - latestHotkey = { - which: [ev.key.toLowerCase()], - ctrl: ev.ctrlKey || ev.metaKey, - alt: ev.altKey, - shift: ev.shiftKey, - callback, - }; - - lastHotKeyStoreTimer = window.setTimeout(() => { - latestHotkey = null; - }, 500); -}; - -const parseKeyCode = (input?: string | null) => { - if (input == null) return []; - const raw = getValueByKey(KEY_ALIASES, input); - if (raw == null) return [input]; - if (typeof raw === 'string') return [trimLower(raw)]; - return raw.map(trimLower); -}; - -const getValueByKey = < - T extends Record, - K extends keyof T | keyof any, - R extends K extends keyof T ? T[K] : T[keyof T] | undefined, ->(obj: T, key: K) => { - return obj[key] as R; -}; - -const trimLower = (str: string) => str.trim().toLowerCase(); -//#endregion diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts new file mode 100644 index 0000000000..8e5f17f38a --- /dev/null +++ b/packages/frontend/src/scripts/i18n.ts @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class I18n> { + public ts: T; + + constructor(locale: T) { + this.ts = locale; + + //#region BIND + this.t = this.t.bind(this); + //#endregion + } + + // string にしているのは、ドット区切りでのパス指定を許可するため + // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも + public t(key: string, args?: Record): string { + try { + let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; + + if (args) { + for (const [k, v] of Object.entries(args)) { + str = str.replace(`{${k}}`, v.toString()); + } + } + return str; + } catch (err) { + console.warn(`missing localization '${key}'`); + return key; + } + } +} diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts index 20f51660c7..a20cfcb1d0 100644 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,21 +10,10 @@ import { set as iset, del as idel, } from 'idb-keyval'; -import { miLocalStorage } from '@/local-storage.js'; -const PREFIX = 'idbfallback::'; +const fallbackName = (key: string) => `idbfallback::${key}`; -let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true; - -// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 -// バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと -// see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123 -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-expect-error -if (window.Cypress) { - idbAvailable = false; - console.log('Cypress detected. It will use localStorage.'); -} +let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true; if (idbAvailable) { await iset('idb-test', 'test') @@ -39,15 +28,15 @@ if (idbAvailable) { export async function get(key: string) { if (idbAvailable) return iget(key); - return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); + return JSON.parse(window.localStorage.getItem(fallbackName(key))); } export async function set(key: string, val: any) { if (idbAvailable) return iset(key, val); - return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); + return window.localStorage.setItem(fallbackName(key), JSON.stringify(val)); } export async function del(key: string) { if (idbAvailable) return idel(key); - return miLocalStorage.removeItem(`${PREFIX}${key}`); + return window.localStorage.removeItem(fallbackName(key)); } diff --git a/packages/frontend/src/scripts/idle-render.ts b/packages/frontend/src/scripts/idle-render.ts index 6adfedcb9f..ac1be50c73 100644 --- a/packages/frontend/src/scripts/idle-render.ts +++ b/packages/frontend/src/scripts/idle-render.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts index 2465a14703..ebf27667d7 100644 --- a/packages/frontend/src/scripts/init-chart.ts +++ b/packages/frontend/src/scripts/init-chart.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/initialize-sw.ts b/packages/frontend/src/scripts/initialize-sw.ts index 867ebf19ed..007fc0f2f7 100644 --- a/packages/frontend/src/scripts/initialize-sw.ts +++ b/packages/frontend/src/scripts/initialize-sw.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { lang } from '@@/js/config.js'; +import { lang } from '@/config.js'; export async function initializeSw() { if (!('serviceWorker' in navigator)) return; diff --git a/packages/frontend/src/scripts/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts index 72ff8bd5ff..1310a0dc73 100644 --- a/packages/frontend/src/scripts/install-plugin.ts +++ b/packages/frontend/src/scripts/install-plugin.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,7 +10,6 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import type { Plugin } from '@/store.js'; import { ColdDeviceStorage } from '@/store.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; export type AiScriptPluginMeta = { @@ -64,11 +63,7 @@ export async function parsePluginMeta(code: string): Promise try { ast = parser.parse(code); } catch (err) { - if (err instanceof Error) { - throw new Error(`Aiscript syntax error\n${(err as Error).message}`); - } else { - throw new Error('Aiscript syntax error'); - } + throw new Error('Aiscript syntax error'); } const meta = Interpreter.collectMetadata(ast); @@ -107,7 +102,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { } const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), { + os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), { title: i18n.ts.tokenRequested, information: i18n.ts.pluginTokenRequestedDescription, initialName: realMeta.name, @@ -115,15 +110,14 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { }, { done: async result => { const { name, permissions } = result; - const { token } = await misskeyApi('miauth/gen-token', { + const { token } = await os.api('miauth/gen-token', { session: null, name: name, permission: permissions, }); res(token); }, - closed: () => dispose(), - }); + }, 'closed'); }); savePlugin({ diff --git a/packages/frontend/src/scripts/install-theme.ts b/packages/frontend/src/scripts/install-theme.ts index 866f1225bf..394b642bf4 100644 --- a/packages/frontend/src/scripts/install-theme.ts +++ b/packages/frontend/src/scripts/install-theme.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/intl-const.ts b/packages/frontend/src/scripts/intl-const.ts index 385f59ec39..ea16c9c2ae 100644 --- a/packages/frontend/src/scripts/intl-const.ts +++ b/packages/frontend/src/scripts/intl-const.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { lang } from '@@/js/config.js'; +import { lang } from '@/config.js'; export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP'); @@ -33,10 +33,6 @@ try { } export const dateTimeFormat = _dateTimeFormat; -export const timeZone = dateTimeFormat.resolvedOptions().timeZone; - -export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N'; - let _numberFormat: Intl.NumberFormat; try { _numberFormat = new Intl.NumberFormat(versatileLang); diff --git a/packages/frontend/src/scripts/is-device-darkmode.ts b/packages/frontend/src/scripts/is-device-darkmode.ts index 4f487c7cb9..badc295726 100644 --- a/packages/frontend/src/scripts/is-device-darkmode.ts +++ b/packages/frontend/src/scripts/is-device-darkmode.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts index e28e5725bc..0567f3b34a 100644 --- a/packages/frontend/src/scripts/isFfVisibleForMe.ts +++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts @@ -1,24 +1,16 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import * as Misskey from 'misskey-js'; import { $i } from '@/account.js'; -export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean { - if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true; +export function isFfVisibleForMe(user: Misskey.entities.UserDetailed): boolean { + if ($i && $i.id === user.id) return true; - if (user.followingVisibility === 'private') return false; - if (user.followingVisibility === 'followers' && !user.isFollowing) return false; - - return true; -} -export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean { - if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true; - - if (user.followersVisibility === 'private') return false; - if (user.followersVisibility === 'followers' && !user.isFollowing) return false; + if (user.ffVisibility === 'private') return false; + if (user.ffVisibility === 'followers' && !user.isFollowing) return false; return true; } diff --git a/packages/frontend/src/scripts/keycode.ts b/packages/frontend/src/scripts/keycode.ts new file mode 100644 index 0000000000..57bc4d19ba --- /dev/null +++ b/packages/frontend/src/scripts/keycode.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export default (input: string): string[] => { + if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) { + const codes = aliases[input]; + return Array.isArray(codes) ? codes : [codes]; + } else { + return [input]; + } +}; + +export const aliases = { + 'esc': 'Escape', + 'enter': ['Enter', 'NumpadEnter'], + 'up': 'ArrowUp', + 'down': 'ArrowDown', + 'left': 'ArrowLeft', + 'right': 'ArrowRight', + 'plus': ['NumpadAdd', 'Semicolon'], +}; diff --git a/packages/frontend/src/scripts/langmap.ts b/packages/frontend/src/scripts/langmap.ts index b32de15963..3912d58d82 100644 --- a/packages/frontend/src/scripts/langmap.ts +++ b/packages/frontend/src/scripts/langmap.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/libopenmpt/LICENSE b/packages/frontend/src/scripts/libopenmpt/LICENSE deleted file mode 100644 index 2daefe981f..0000000000 --- a/packages/frontend/src/scripts/libopenmpt/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2004-2024, OpenMPT Project Developers and Contributors -Copyright (c) 1997-2003, Olivier Lapicque -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the OpenMPT project nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/frontend/src/scripts/libopenmpt/libopenmpt.js b/packages/frontend/src/scripts/libopenmpt/libopenmpt.js deleted file mode 100644 index e2535529ce..0000000000 --- a/packages/frontend/src/scripts/libopenmpt/libopenmpt.js +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -/* eslint-disable */ - -var Module=typeof libopenmpt!="undefined"?libopenmpt:{};var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary,setWindowTitle;function logExceptionOnExit(e){if(e instanceof ExitStatus)return;let toLog=e;err("exiting due to exception: "+toLog)}if(ENVIRONMENT_IS_NODE){if(ENVIRONMENT_IS_WORKER){scriptDirectory=require("path").dirname(scriptDirectory)+"/"}else{scriptDirectory=__dirname+"/"}var fs,nodePath;if(typeof require==="function"){fs=require("fs");nodePath=require("path")}read_=(filename,binary)=>{filename=nodePath["normalize"](filename);return fs.readFileSync(filename,binary?undefined:"utf8")};readBinary=filename=>{var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}return ret};readAsync=(filename,onload,onerror)=>{filename=nodePath["normalize"](filename);fs.readFile(filename,function(err,data){if(err)onerror(err);else onload(data.buffer)})};if(process["argv"].length>1){thisProgram=process["argv"][1].replace(/\\/g,"/")}arguments_=process["argv"].slice(2);if(typeof module!="undefined"){module["exports"]=Module}process["on"]("uncaughtException",function(ex){if(!(ex instanceof ExitStatus)){throw ex}});process["on"]("unhandledRejection",function(reason){throw reason});quit_=(status,toThrow)=>{if(keepRuntimeAlive()){process["exitCode"]=status;throw toThrow}logExceptionOnExit(toThrow);process["exit"](status)};Module["inspect"]=function(){return"[Emscripten Module object]"}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}else{scriptDirectory=""}{read_=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=(url,onload,onerror)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)}}setWindowTitle=title=>document.title=title}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.warn.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var noExitRuntime=Module["noExitRuntime"]||true;if(typeof WebAssembly!="object"){abort("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort(text)}}var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder("utf8"):undefined;function UTF8ArrayToString(heapOrArray,idx,maxBytesToRead){var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str}function UTF8ToString(ptr,maxBytesToRead){return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""}function stringToUTF8Array(str,heap,outIdx,maxBytesToWrite){if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx}function lengthBytesUTF8(str){var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len}var buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBufferAndViews(buf){buffer=buf;Module["HEAP8"]=HEAP8=new Int8Array(buf);Module["HEAP16"]=HEAP16=new Int16Array(buf);Module["HEAP32"]=HEAP32=new Int32Array(buf);Module["HEAPU8"]=HEAPU8=new Uint8Array(buf);Module["HEAPU16"]=HEAPU16=new Uint16Array(buf);Module["HEAPU32"]=HEAPU32=new Uint32Array(buf);Module["HEAPF32"]=HEAPF32=new Float32Array(buf);Module["HEAPF64"]=HEAPF64=new Float64Array(buf)}var INITIAL_MEMORY=Module["INITIAL_MEMORY"]||16777216;var wasmTable;var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function keepRuntimeAlive(){return noExitRuntime}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.init.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){{if(Module["onAbort"]){Module["onAbort"](what)}}what="Aborted("+what+")";err(what);ABORT=true;EXITSTATUS=1;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return filename.startsWith(dataURIPrefix)}function isFileURI(filename){return filename.startsWith("file://")}var wasmBinaryFile;wasmBinaryFile=new URL("./libopenmpt.wasm", import.meta.url).href;if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=locateFile(wasmBinaryFile)}function getBinary(file){try{if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}catch(err){abort(err)}}function getBinaryPromise(){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch=="function"&&!isFileURI(wasmBinaryFile)){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){if(!response["ok"]){throw"failed to load wasm binary file at '"+wasmBinaryFile+"'"}return response["arrayBuffer"]()}).catch(function(){return getBinary(wasmBinaryFile)})}else{if(readAsync){return new Promise(function(resolve,reject){readAsync(wasmBinaryFile,function(response){resolve(new Uint8Array(response))},reject)})}}}return Promise.resolve().then(function(){return getBinary(wasmBinaryFile)})}function createWasm(){var info={"a":asmLibraryArg};function receiveInstance(instance,module){var exports=instance.exports;Module["asm"]=exports;wasmMemory=Module["asm"]["pa"];updateGlobalBufferAndViews(wasmMemory.buffer);wasmTable=Module["asm"]["oc"];addOnInit(Module["asm"]["qa"]);removeRunDependency("wasm-instantiate")}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}function instantiateArrayBuffer(receiver){return getBinaryPromise().then(function(binary){return WebAssembly.instantiate(binary,info)}).then(function(instance){return instance}).then(receiver,function(reason){err("failed to asynchronously prepare wasm: "+reason);abort(reason)})}function instantiateAsync(){if(!wasmBinary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(wasmBinaryFile)&&!isFileURI(wasmBinaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){var result=WebAssembly.instantiateStreaming(response,info);return result.then(receiveInstantiationResult,function(reason){err("wasm streaming compile failed: "+reason);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(receiveInstantiationResult)})})}else{return instantiateArrayBuffer(receiveInstantiationResult)}}if(Module["instantiateWasm"]){try{var exports=Module["instantiateWasm"](info,receiveInstance);return exports}catch(e){err("Module.instantiateWasm callback failed with error: "+e);return false}}instantiateAsync();return{}}var tempDouble;var tempI64;function ExitStatus(status){this.name="ExitStatus";this.message="Program terminated with exit("+status+")";this.status=status}function callRuntimeCallbacks(callbacks){while(callbacks.length>0){callbacks.shift()(Module)}}function writeArrayToMemory(array,buffer){HEAP8.set(array,buffer)}function ___assert_fail(condition,filename,line,func){abort("Assertion failed: "+UTF8ToString(condition)+", at: "+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"])}function ___cxa_allocate_exception(size){return _malloc(size+24)+24}var exceptionCaught=[];function exception_addRef(info){info.add_ref()}var uncaughtExceptionCount=0;function ___cxa_begin_catch(ptr){var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);exception_addRef(info);return info.get_exception_ptr()}var exceptionLast=0;function ExceptionInfo(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24;this.set_type=function(type){HEAPU32[this.ptr+4>>2]=type};this.get_type=function(){return HEAPU32[this.ptr+4>>2]};this.set_destructor=function(destructor){HEAPU32[this.ptr+8>>2]=destructor};this.get_destructor=function(){return HEAPU32[this.ptr+8>>2]};this.set_refcount=function(refcount){HEAP32[this.ptr>>2]=refcount};this.set_caught=function(caught){caught=caught?1:0;HEAP8[this.ptr+12>>0]=caught};this.get_caught=function(){return HEAP8[this.ptr+12>>0]!=0};this.set_rethrown=function(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13>>0]=rethrown};this.get_rethrown=function(){return HEAP8[this.ptr+13>>0]!=0};this.init=function(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor);this.set_refcount(0);this.set_caught(false);this.set_rethrown(false)};this.add_ref=function(){var value=HEAP32[this.ptr>>2];HEAP32[this.ptr>>2]=value+1};this.release_ref=function(){var prev=HEAP32[this.ptr>>2];HEAP32[this.ptr>>2]=prev-1;return prev===1};this.set_adjusted_ptr=function(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr};this.get_adjusted_ptr=function(){return HEAPU32[this.ptr+16>>2]};this.get_exception_ptr=function(){var isPointer=___cxa_is_pointer_type(this.get_type());if(isPointer){return HEAPU32[this.excPtr>>2]}var adjusted=this.get_adjusted_ptr();if(adjusted!==0)return adjusted;return this.excPtr}}function ___cxa_free_exception(ptr){return _free(new ExceptionInfo(ptr).ptr)}function getWasmTableEntry(funcPtr){return wasmTable.get(funcPtr)}function exception_decRef(info){if(info.release_ref()&&!info.get_rethrown()){var destructor=info.get_destructor();if(destructor){getWasmTableEntry(destructor)(info.excPtr)}___cxa_free_exception(info.excPtr)}}function ___cxa_end_catch(){_setThrew(0);var info=exceptionCaught.pop();exception_decRef(info);exceptionLast=0}function ___resumeException(ptr){if(!exceptionLast){exceptionLast=ptr}throw ptr}function ___cxa_find_matching_catch_17(){var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var i=0;i{var t=process["hrtime"]();return t[0]*1e3+t[1]/1e6}}else _emscripten_get_now=()=>performance.now();function getHeapMax(){return 2147483648}function emscripten_realloc_buffer(size){try{wasmMemory.grow(size-buffer.byteLength+65535>>>16);updateGlobalBufferAndViews(wasmMemory.buffer);return 1}catch(e){}}function _emscripten_resize_heap(requestedSize){var oldSize=HEAPU8.length;requestedSize=requestedSize>>>0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}let alignUp=(x,multiple)=>x+(multiple-x%multiple)%multiple;for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignUp(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=emscripten_realloc_buffer(newSize);if(replacement){return true}}return false}var ENV={};function getExecutableName(){return thisProgram||"./this.program"}function getEnvStrings(){if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={"USER":"web_user","LOGNAME":"web_user","PATH":"/","PWD":"/","HOME":"/home/web_user","LANG":lang,"_":getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(x+"="+env[x])}getEnvStrings.strings=strings}return getEnvStrings.strings}function writeAsciiToMemory(str,buffer,dontAddNull){for(var i=0;i>0]=str.charCodeAt(i)}if(!dontAddNull)HEAP8[buffer>>0]=0}var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:function(){var paths=Array.prototype.slice.call(arguments);return PATH.normalize(paths.join("/"))},join2:(l,r)=>{return PATH.normalize(l+"/"+r)}};function getRandomDevice(){if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){var randomBuffer=new Uint8Array(1);return()=>{crypto.getRandomValues(randomBuffer);return randomBuffer[0]}}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");return()=>crypto_module["randomBytes"](1)[0]}catch(e){}}return()=>abort("randomDevice")}var PATH_FS={resolve:function(){var resolvedPath="",resolvedAbsolute=false;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?arguments[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var TTY={ttys:[],init:function(){},shutdown:function(){},register:function(dev,ops){TTY.ttys[dev]={input:[],output:[],ops:ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open:function(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close:function(stream){stream.tty.ops.fsync(stream.tty)},fsync:function(stream){stream.tty.ops.fsync(stream.tty)},read:function(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){result=buf.slice(0,bytesRead).toString("utf-8")}else{result=null}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else if(typeof readline=="function"){result=readline();if(result!==null){result+="\n"}}if(!result){return null}tty.input=intArrayFromString(result,true)}return tty.input.shift()},put_char:function(tty,val){if(val===null||val===10){out(UTF8ArrayToString(tty.output,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync:function(tty){if(tty.output&&tty.output.length>0){out(UTF8ArrayToString(tty.output,0));tty.output=[]}}},default_tty1_ops:{put_char:function(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync:function(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output,0));tty.output=[]}}}};function mmapAlloc(size){abort()}var MEMFS={ops_table:null,mount:function(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode:function(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}if(!MEMFS.ops_table){MEMFS.ops_table={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}}}var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray:function(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage:function(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage:function(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr:function(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr:function(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup:function(parent,name){throw FS.genericErrors[44]},mknod:function(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename:function(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp;old_node.parent=new_dir},unlink:function(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir:function(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir:function(node){var entries=[".",".."];for(var key in node.contents){if(!node.contents.hasOwnProperty(key)){continue}entries.push(key)}return entries},symlink:function(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink:function(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read:function(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{assert(arrayBuffer,'Loading data file "'+url+'" failed (no arrayBuffer).');onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},event=>{if(onerror){onerror()}else{throw'Loading data file "'+url+'" failed.'}});if(dep)addRunDependency(dep)}var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:null,genericErrors:{},filesystems:null,syncFSRequests:0,lookupPath:(path,opts={})=>{path=PATH_FS.resolve(FS.cwd(),path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=PATH.normalizeArray(path.split("/").filter(p=>!!p),false);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath:node=>{var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?mount+"/"+path:mount+path}path=path?node.name+"/"+path:node.name;node=node.parent}},hashName:(parentid,name)=>{var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode:node=>{var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode:node=>{var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode:(parent,name)=>{var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode,parent)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode:(parent,name,mode,rdev)=>{var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode:node=>{FS.hashRemoveNode(node)},isRoot:node=>{return node===node.parent},isMountpoint:node=>{return!!node.mounted},isFile:mode=>{return(mode&61440)===32768},isDir:mode=>{return(mode&61440)===16384},isLink:mode=>{return(mode&61440)===40960},isChrdev:mode=>{return(mode&61440)===8192},isBlkdev:mode=>{return(mode&61440)===24576},isFIFO:mode=>{return(mode&61440)===4096},isSocket:mode=>{return(mode&49152)===49152},flagModes:{"r":0,"r+":2,"w":577,"w+":578,"a":1089,"a+":1090},modeStringToFlags:str=>{var flags=FS.flagModes[str];if(typeof flags=="undefined"){throw new Error("Unknown file open mode: "+str)}return flags},flagsToPermissionString:flag=>{var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions:(node,perms)=>{if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup:dir=>{var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate:(dir,name)=>{try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete:(dir,name,isdir)=>{var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen:(node,flags)=>{if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd:(fd_start=0,fd_end=FS.MAX_OPEN_FDS)=>{for(var fd=fd_start;fd<=fd_end;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStream:fd=>FS.streams[fd],createStream:(stream,fd_start,fd_end)=>{if(!FS.FSStream){FS.FSStream=function(){this.shared={}};FS.FSStream.prototype={};Object.defineProperties(FS.FSStream.prototype,{object:{get:function(){return this.node},set:function(val){this.node=val}},isRead:{get:function(){return(this.flags&2097155)!==1}},isWrite:{get:function(){return(this.flags&2097155)!==0}},isAppend:{get:function(){return this.flags&1024}},flags:{get:function(){return this.shared.flags},set:function(val){this.shared.flags=val}},position:{get:function(){return this.shared.position},set:function(val){this.shared.position=val}}})}stream=Object.assign(new FS.FSStream,stream);var fd=FS.nextfd(fd_start,fd_end);stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream:fd=>{FS.streams[fd]=null},chrdev_stream_ops:{open:stream=>{var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;if(stream.stream_ops.open){stream.stream_ops.open(stream)}},llseek:()=>{throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice:(dev,ops)=>{FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts:mount=>{var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push.apply(check,m.mounts)}return mounts},syncfs:(populate,callback)=>{if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err("warning: "+FS.syncFSRequests+" FS.syncfs operations in flight at once, probably just doing extra work")}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount:(type,opts,mountpoint)=>{var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type:type,opts:opts,mountpoint:mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount:mountpoint=>{var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup:(parent,name)=>{return parent.node_ops.lookup(parent,name)},mknod:(path,mode,dev)=>{var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create:(path,mode)=>{mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir:(path,mode)=>{mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree:(path,mode)=>{var dirs=path.split("/");var d="";for(var i=0;i{if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink:(oldpath,newpath)=>{if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename:(old_path,new_path)=>{var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name)}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir:path=>{var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir:path=>{var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node.node_ops.readdir){throw new FS.ErrnoError(54)}return node.node_ops.readdir(node)},unlink:path=>{var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink:path=>{var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return PATH_FS.resolve(FS.getPath(link.parent),link.node_ops.readlink(link))},stat:(path,dontFollow)=>{var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;if(!node){throw new FS.ErrnoError(44)}if(!node.node_ops.getattr){throw new FS.ErrnoError(63)}return node.node_ops.getattr(node)},lstat:path=>{return FS.stat(path,true)},chmod:(path,mode,dontFollow)=>{var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}if(!node.node_ops.setattr){throw new FS.ErrnoError(63)}node.node_ops.setattr(node,{mode:mode&4095|node.mode&~4095,timestamp:Date.now()})},lchmod:(path,mode)=>{FS.chmod(path,mode,true)},fchmod:(fd,mode)=>{var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}FS.chmod(stream.node,mode)},chown:(path,uid,gid,dontFollow)=>{var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}if(!node.node_ops.setattr){throw new FS.ErrnoError(63)}node.node_ops.setattr(node,{timestamp:Date.now()})},lchown:(path,uid,gid)=>{FS.chown(path,uid,gid,true)},fchown:(fd,uid,gid)=>{var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}FS.chown(stream.node,uid,gid)},truncate:(path,len)=>{if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}if(!node.node_ops.setattr){throw new FS.ErrnoError(63)}if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}node.node_ops.setattr(node,{size:len,timestamp:Date.now()})},ftruncate:(fd,len)=>{var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.truncate(stream.node,len)},utime:(path,atime,mtime)=>{var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;node.node_ops.setattr(node,{timestamp:Math.max(atime,mtime)})},open:(path,flags,mode)=>{if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS.modeStringToFlags(flags):flags;mode=typeof mode=="undefined"?438:mode;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;if(typeof path=="object"){node=path}else{path=PATH.normalize(path);try{var lookup=FS.lookupPath(path,{follow:!(flags&131072)});node=lookup.node}catch(e){}}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else{node=FS.mknod(path,mode,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node:node,path:FS.getPath(node),flags:flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(Module["logReadFiles"]&&!(flags&1)){if(!FS.readFiles)FS.readFiles={};if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close:stream=>{if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed:stream=>{return stream.fd===null},llseek:(stream,offset,whence)=>{if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read:(stream,buffer,offset,length,position)=>{if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write:(stream,buffer,offset,length,position,canOwn)=>{if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},allocate:(stream,offset,length)=>{if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(offset<0||length<=0){throw new FS.ErrnoError(28)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(!FS.isFile(stream.node.mode)&&!FS.isDir(stream.node.mode)){throw new FS.ErrnoError(43)}if(!stream.stream_ops.allocate){throw new FS.ErrnoError(138)}stream.stream_ops.allocate(stream,offset,length)},mmap:(stream,length,position,prot,flags)=>{if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync:(stream,buffer,offset,length,mmapFlags)=>{if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},munmap:stream=>0,ioctl:(stream,cmd,arg)=>{if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile:(path,opts={})=>{opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error('Invalid encoding type "'+opts.encoding+'"')}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf,0)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile:(path,data,opts={})=>{opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir:path=>{var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories:()=>{FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices:()=>{FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var random_device=getRandomDevice();FS.createDevice("/dev","random",random_device);FS.createDevice("/dev","urandom",random_device);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories:()=>{FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount:()=>{var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup:(parent,name)=>{var fd=+name;var stream=FS.getStream(fd);if(!stream)throw new FS.ErrnoError(8);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams:()=>{if(Module["stdin"]){FS.createDevice("/dev","stdin",Module["stdin"])}else{FS.symlink("/dev/tty","/dev/stdin")}if(Module["stdout"]){FS.createDevice("/dev","stdout",null,Module["stdout"])}else{FS.symlink("/dev/tty","/dev/stdout")}if(Module["stderr"]){FS.createDevice("/dev","stderr",null,Module["stderr"])}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},ensureErrnoError:()=>{if(FS.ErrnoError)return;FS.ErrnoError=function ErrnoError(errno,node){this.node=node;this.setErrno=function(errno){this.errno=errno};this.setErrno(errno);this.message="FS error"};FS.ErrnoError.prototype=new Error;FS.ErrnoError.prototype.constructor=FS.ErrnoError;[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""})},staticInit:()=>{FS.ensureErrnoError();FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={"MEMFS":MEMFS}},init:(input,output,error)=>{FS.init.initialized=true;FS.ensureErrnoError();Module["stdin"]=input||Module["stdin"];Module["stdout"]=output||Module["stdout"];Module["stderr"]=error||Module["stderr"];FS.createStandardStreams()},quit:()=>{FS.init.initialized=false;for(var i=0;i{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode},findObject:(path,dontResolveLastLink)=>{var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath:(path,dontResolveLastLink)=>{try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath:(parent,path,canRead,canWrite)=>{parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){}parent=current}return current},createFile:(parent,name,properties,canRead,canWrite)=>{var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS.getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile:(parent,name,data,canRead,canWrite,canOwn)=>{var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS.getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;i{var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS.getMode(!!input,!!output);if(!FS.createDevice.major)FS.createDevice.major=64;var dev=FS.makedev(FS.createDevice.major++,0);FS.registerDevice(dev,{open:stream=>{stream.seekable=false},close:stream=>{if(output&&output.buffer&&output.buffer.length){output(10)}},read:(stream,buffer,offset,length,pos)=>{var bytesRead=0;for(var i=0;i{for(var i=0;i{if(obj.isDevice||obj.isFolder||obj.link||obj.contents)return true;if(typeof XMLHttpRequest!="undefined"){throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.")}else if(read_){try{obj.contents=intArrayFromString(read_(obj.url),true);obj.usedBytes=obj.contents.length}catch(e){throw new FS.ErrnoError(29)}}else{throw new Error("Cannot load without read() or XMLHttpRequest.")}},createLazyFile:(parent,name,url,canRead,canWrite)=>{function LazyUint8Array(){this.lengthKnown=false;this.chunks=[]}LazyUint8Array.prototype.get=function LazyUint8Array_get(idx){if(idx>this.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]};LazyUint8Array.prototype.setDataGetter=function LazyUint8Array_setDataGetter(getter){this.getter=getter};LazyUint8Array.prototype.cacheLength=function LazyUint8Array_cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true};if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;Object.defineProperties(lazyArray,{length:{get:function(){if(!this.lengthKnown){this.cacheLength()}return this._length}},chunkSize:{get:function(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}});var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url:url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=function forceLoadLazyFile(){FS.forceLoadFile(node);return fn.apply(null,arguments)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr:ptr,allocated:true}};node.stream_ops=stream_ops;return node},createPreloadedFile:(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency("cp "+fullname);function processData(byteArray){function finish(byteArray){if(preFinish)preFinish();if(!dontCreateFile){FS.createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}if(onload)onload();removeRunDependency(dep)}if(Browser.handledByPreloadPlugin(byteArray,fullname,finish,()=>{if(onerror)onerror();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,byteArray=>processData(byteArray),onerror)}else{processData(url)}},indexedDB:()=>{return window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB},DB_NAME:()=>{return"EM_FS_"+window.location.pathname},DB_VERSION:20,DB_STORE_NAME:"FILE_DATA",saveFilesToDB:(paths,onload,onerror)=>{onload=onload||(()=>{});onerror=onerror||(()=>{});var indexedDB=FS.indexedDB();try{var openRequest=indexedDB.open(FS.DB_NAME(),FS.DB_VERSION)}catch(e){return onerror(e)}openRequest.onupgradeneeded=()=>{out("creating db");var db=openRequest.result;db.createObjectStore(FS.DB_STORE_NAME)};openRequest.onsuccess=()=>{var db=openRequest.result;var transaction=db.transaction([FS.DB_STORE_NAME],"readwrite");var files=transaction.objectStore(FS.DB_STORE_NAME);var ok=0,fail=0,total=paths.length;function finish(){if(fail==0)onload();else onerror()}paths.forEach(path=>{var putRequest=files.put(FS.analyzePath(path).object.contents,path);putRequest.onsuccess=()=>{ok++;if(ok+fail==total)finish()};putRequest.onerror=()=>{fail++;if(ok+fail==total)finish()}});transaction.onerror=onerror};openRequest.onerror=onerror},loadFilesFromDB:(paths,onload,onerror)=>{onload=onload||(()=>{});onerror=onerror||(()=>{});var indexedDB=FS.indexedDB();try{var openRequest=indexedDB.open(FS.DB_NAME(),FS.DB_VERSION)}catch(e){return onerror(e)}openRequest.onupgradeneeded=onerror;openRequest.onsuccess=()=>{var db=openRequest.result;try{var transaction=db.transaction([FS.DB_STORE_NAME],"readonly")}catch(e){onerror(e);return}var files=transaction.objectStore(FS.DB_STORE_NAME);var ok=0,fail=0,total=paths.length;function finish(){if(fail==0)onload();else onerror()}paths.forEach(path=>{var getRequest=files.get(path);getRequest.onsuccess=()=>{if(FS.analyzePath(path).exists){FS.unlink(path)}FS.createDataFile(PATH.dirname(path),PATH.basename(path),getRequest.result,true,true,true);ok++;if(ok+fail==total)finish()};getRequest.onerror=()=>{fail++;if(ok+fail==total)finish()}});transaction.onerror=onerror};openRequest.onerror=onerror}};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt:function(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat:function(func,path,buf){try{var stat=func(path)}catch(e){if(e&&e.node&&PATH.normalize(path)!==PATH.normalize(FS.getPath(e.node))){return-54}throw e}HEAP32[buf>>2]=stat.dev;HEAP32[buf+8>>2]=stat.ino;HEAP32[buf+12>>2]=stat.mode;HEAPU32[buf+16>>2]=stat.nlink;HEAP32[buf+20>>2]=stat.uid;HEAP32[buf+24>>2]=stat.gid;HEAP32[buf+28>>2]=stat.rdev;tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];HEAP32[buf+48>>2]=4096;HEAP32[buf+52>>2]=stat.blocks;tempI64=[Math.floor(stat.atime.getTime()/1e3)>>>0,(tempDouble=Math.floor(stat.atime.getTime()/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];HEAPU32[buf+64>>2]=0;tempI64=[Math.floor(stat.mtime.getTime()/1e3)>>>0,(tempDouble=Math.floor(stat.mtime.getTime()/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];HEAPU32[buf+80>>2]=0;tempI64=[Math.floor(stat.ctime.getTime()/1e3)>>>0,(tempDouble=Math.floor(stat.ctime.getTime()/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];HEAPU32[buf+96>>2]=0;tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+104>>2]=tempI64[0],HEAP32[buf+108>>2]=tempI64[1];return 0},doMsync:function(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},varargs:undefined,get:function(){SYSCALLS.varargs+=4;var ret=HEAP32[SYSCALLS.varargs-4>>2];return ret},getStr:function(ptr){var ret=UTF8ToString(ptr);return ret},getStreamFromFD:function(fd){var stream=FS.getStream(fd);if(!stream)throw new FS.ErrnoError(8);return stream}};function _environ_get(__environ,environ_buf){var bufSize=0;getEnvStrings().forEach(function(string,i){var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;writeAsciiToMemory(string,ptr);bufSize+=string.length+1});return 0}function _environ_sizes_get(penviron_count,penviron_buf_size){var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;strings.forEach(function(string){bufSize+=string.length+1});HEAPU32[penviron_buf_size>>2]=bufSize;return 0}function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e instanceof FS.ErrnoError))throw e;return e.errno}}function doReadv(stream,iov,iovcnt,offset){var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e instanceof FS.ErrnoError))throw e;return e.errno}}function convertI32PairToI53Checked(lo,hi){return hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){try{var offset=convertI32PairToI53Checked(offset_low,offset_high);if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e instanceof FS.ErrnoError))throw e;return e.errno}}function doWritev(stream,iov,iovcnt,offset){var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr}return ret}function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doWritev(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e instanceof FS.ErrnoError))throw e;return e.errno}}function _getentropy(buffer,size){if(!_getentropy.randomDevice){_getentropy.randomDevice=getRandomDevice()}for(var i=0;i>0]=_getentropy.randomDevice()}return 0}function _llvm_eh_typeid_for(type){return type}function __isLeapYear(year){return year%4===0&&(year%100!==0||year%400===0)}function __arraySum(array,index){var sum=0;for(var i=0;i<=index;sum+=array[i++]){}return sum}var __MONTH_DAYS_LEAP=[31,29,31,30,31,30,31,31,30,31,30,31];var __MONTH_DAYS_REGULAR=[31,28,31,30,31,30,31,31,30,31,30,31];function __addDays(date,days){var newDate=new Date(date.getTime());while(days>0){var leap=__isLeapYear(newDate.getFullYear());var currentMonth=newDate.getMonth();var daysInCurrentMonth=(leap?__MONTH_DAYS_LEAP:__MONTH_DAYS_REGULAR)[currentMonth];if(days>daysInCurrentMonth-newDate.getDate()){days-=daysInCurrentMonth-newDate.getDate()+1;newDate.setDate(1);if(currentMonth<11){newDate.setMonth(currentMonth+1)}else{newDate.setMonth(0);newDate.setFullYear(newDate.getFullYear()+1)}}else{newDate.setDate(newDate.getDate()+days);return newDate}}return newDate}function _strftime(s,maxsize,format,tm){var tm_zone=HEAP32[tm+40>>2];var date={tm_sec:HEAP32[tm>>2],tm_min:HEAP32[tm+4>>2],tm_hour:HEAP32[tm+8>>2],tm_mday:HEAP32[tm+12>>2],tm_mon:HEAP32[tm+16>>2],tm_year:HEAP32[tm+20>>2],tm_wday:HEAP32[tm+24>>2],tm_yday:HEAP32[tm+28>>2],tm_isdst:HEAP32[tm+32>>2],tm_gmtoff:HEAP32[tm+36>>2],tm_zone:tm_zone?UTF8ToString(tm_zone):""};var pattern=UTF8ToString(format);var EXPANSION_RULES_1={"%c":"%a %b %d %H:%M:%S %Y","%D":"%m/%d/%y","%F":"%Y-%m-%d","%h":"%b","%r":"%I:%M:%S %p","%R":"%H:%M","%T":"%H:%M:%S","%x":"%m/%d/%y","%X":"%H:%M:%S","%Ec":"%c","%EC":"%C","%Ex":"%m/%d/%y","%EX":"%H:%M:%S","%Ey":"%y","%EY":"%Y","%Od":"%d","%Oe":"%e","%OH":"%H","%OI":"%I","%Om":"%m","%OM":"%M","%OS":"%S","%Ou":"%u","%OU":"%U","%OV":"%V","%Ow":"%w","%OW":"%W","%Oy":"%y"};for(var rule in EXPANSION_RULES_1){pattern=pattern.replace(new RegExp(rule,"g"),EXPANSION_RULES_1[rule])}var WEEKDAYS=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];var MONTHS=["January","February","March","April","May","June","July","August","September","October","November","December"];function leadingSomething(value,digits,character){var str=typeof value=="number"?value.toString():value||"";while(str.length0?1:0}var compare;if((compare=sgn(date1.getFullYear()-date2.getFullYear()))===0){if((compare=sgn(date1.getMonth()-date2.getMonth()))===0){compare=sgn(date1.getDate()-date2.getDate())}}return compare}function getFirstWeekStartDate(janFourth){switch(janFourth.getDay()){case 0:return new Date(janFourth.getFullYear()-1,11,29);case 1:return janFourth;case 2:return new Date(janFourth.getFullYear(),0,3);case 3:return new Date(janFourth.getFullYear(),0,2);case 4:return new Date(janFourth.getFullYear(),0,1);case 5:return new Date(janFourth.getFullYear()-1,11,31);case 6:return new Date(janFourth.getFullYear()-1,11,30)}}function getWeekBasedYear(date){var thisDate=__addDays(new Date(date.tm_year+1900,0,1),date.tm_yday);var janFourthThisYear=new Date(thisDate.getFullYear(),0,4);var janFourthNextYear=new Date(thisDate.getFullYear()+1,0,4);var firstWeekStartThisYear=getFirstWeekStartDate(janFourthThisYear);var firstWeekStartNextYear=getFirstWeekStartDate(janFourthNextYear);if(compareByDay(firstWeekStartThisYear,thisDate)<=0){if(compareByDay(firstWeekStartNextYear,thisDate)<=0){return thisDate.getFullYear()+1}return thisDate.getFullYear()}return thisDate.getFullYear()-1}var EXPANSION_RULES_2={"%a":function(date){return WEEKDAYS[date.tm_wday].substring(0,3)},"%A":function(date){return WEEKDAYS[date.tm_wday]},"%b":function(date){return MONTHS[date.tm_mon].substring(0,3)},"%B":function(date){return MONTHS[date.tm_mon]},"%C":function(date){var year=date.tm_year+1900;return leadingNulls(year/100|0,2)},"%d":function(date){return leadingNulls(date.tm_mday,2)},"%e":function(date){return leadingSomething(date.tm_mday,2," ")},"%g":function(date){return getWeekBasedYear(date).toString().substring(2)},"%G":function(date){return getWeekBasedYear(date)},"%H":function(date){return leadingNulls(date.tm_hour,2)},"%I":function(date){var twelveHour=date.tm_hour;if(twelveHour==0)twelveHour=12;else if(twelveHour>12)twelveHour-=12;return leadingNulls(twelveHour,2)},"%j":function(date){return leadingNulls(date.tm_mday+__arraySum(__isLeapYear(date.tm_year+1900)?__MONTH_DAYS_LEAP:__MONTH_DAYS_REGULAR,date.tm_mon-1),3)},"%m":function(date){return leadingNulls(date.tm_mon+1,2)},"%M":function(date){return leadingNulls(date.tm_min,2)},"%n":function(){return"\n"},"%p":function(date){if(date.tm_hour>=0&&date.tm_hour<12){return"AM"}return"PM"},"%S":function(date){return leadingNulls(date.tm_sec,2)},"%t":function(){return"\t"},"%u":function(date){return date.tm_wday||7},"%U":function(date){var days=date.tm_yday+7-date.tm_wday;return leadingNulls(Math.floor(days/7),2)},"%V":function(date){var val=Math.floor((date.tm_yday+7-(date.tm_wday+6)%7)/7);if((date.tm_wday+371-date.tm_yday-2)%7<=2){val++}if(!val){val=52;var dec31=(date.tm_wday+7-date.tm_yday-1)%7;if(dec31==4||dec31==5&&__isLeapYear(date.tm_year%400-1)){val++}}else if(val==53){var jan1=(date.tm_wday+371-date.tm_yday)%7;if(jan1!=4&&(jan1!=3||!__isLeapYear(date.tm_year)))val=1}return leadingNulls(val,2)},"%w":function(date){return date.tm_wday},"%W":function(date){var days=date.tm_yday+7-(date.tm_wday+6)%7;return leadingNulls(Math.floor(days/7),2)},"%y":function(date){return(date.tm_year+1900).toString().substring(2)},"%Y":function(date){return date.tm_year+1900},"%z":function(date){var off=date.tm_gmtoff;var ahead=off>=0;off=Math.abs(off)/60;off=off/60*100+off%60;return(ahead?"+":"-")+String("0000"+off).slice(-4)},"%Z":function(date){return date.tm_zone},"%%":function(){return"%"}};pattern=pattern.replace(/%%/g,"\0\0");for(var rule in EXPANSION_RULES_2){if(pattern.includes(rule)){pattern=pattern.replace(new RegExp(rule,"g"),EXPANSION_RULES_2[rule](date))}}pattern=pattern.replace(/\0\0/g,"%");var bytes=intArrayFromString(pattern,false);if(bytes.length>maxsize){return 0}writeArrayToMemory(bytes,s);return bytes.length-1}function _strftime_l(s,maxsize,format,tm){return _strftime(s,maxsize,format,tm)}var FSNode=function(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev};var readMode=292|73;var writeMode=146;Object.defineProperties(FSNode.prototype,{read:{get:function(){return(this.mode&readMode)===readMode},set:function(val){val?this.mode|=readMode:this.mode&=~readMode}},write:{get:function(){return(this.mode&writeMode)===writeMode},set:function(val){val?this.mode|=writeMode:this.mode&=~writeMode}},isFolder:{get:function(){return FS.isDir(this.mode)}},isDevice:{get:function(){return FS.isChrdev(this.mode)}}});FS.FSNode=FSNode;FS.staticInit();var asmLibraryArg={"p":___assert_fail,"n":___cxa_allocate_exception,"l":___cxa_begin_catch,"m":___cxa_end_catch,"ca":___cxa_find_matching_catch_17,"a":___cxa_find_matching_catch_2,"e":___cxa_find_matching_catch_3,"w":___cxa_find_matching_catch_4,"ba":___cxa_find_matching_catch_6,"o":___cxa_free_exception,"J":___cxa_rethrow,"v":___cxa_throw,"la":___cxa_uncaught_exceptions,"g":___resumeException,"R":__emscripten_get_now_is_monotonic,"H":_abort,"S":_emscripten_date_now,"Q":_emscripten_get_now,"ma":_emscripten_resize_heap,"ia":_environ_get,"ja":_environ_sizes_get,"oa":_fd_close,"ka":_fd_read,"_":_fd_seek,"na":_fd_write,"ga":_getentropy,"G":invoke_di,"E":invoke_dii,"K":invoke_diii,"L":invoke_diiii,"P":invoke_fi,"ea":invoke_fii,"t":invoke_i,"d":invoke_ii,"fa":invoke_iid,"b":invoke_iii,"M":invoke_iiifii,"i":invoke_iiii,"aa":invoke_iiiidd,"q":invoke_iiiii,"$":invoke_iiiiid,"A":invoke_iiiiii,"x":invoke_iiiiiii,"I":invoke_iiiiiiii,"C":invoke_iiiiiiiiiiii,"T":invoke_iiij,"Z":invoke_iiji,"U":invoke_j,"W":invoke_jiiii,"j":invoke_v,"k":invoke_vi,"D":invoke_vid,"c":invoke_vii,"F":invoke_viid,"N":invoke_viif,"f":invoke_viii,"da":invoke_viiidi,"h":invoke_viiii,"r":invoke_viiiii,"z":invoke_viiiiii,"u":invoke_viiiiiii,"O":invoke_viiiiiiii,"y":invoke_viiiiiiiiii,"B":invoke_viiiiiiiiiiiiiii,"X":invoke_viij,"V":invoke_viiji,"Y":invoke_vij,"s":_llvm_eh_typeid_for,"ha":_strftime_l};var asm=createWasm();var ___wasm_call_ctors=Module["___wasm_call_ctors"]=function(){return(___wasm_call_ctors=Module["___wasm_call_ctors"]=Module["asm"]["qa"]).apply(null,arguments)};var _openmpt_get_library_version=Module["_openmpt_get_library_version"]=function(){return(_openmpt_get_library_version=Module["_openmpt_get_library_version"]=Module["asm"]["ra"]).apply(null,arguments)};var _openmpt_get_core_version=Module["_openmpt_get_core_version"]=function(){return(_openmpt_get_core_version=Module["_openmpt_get_core_version"]=Module["asm"]["sa"]).apply(null,arguments)};var _openmpt_free_string=Module["_openmpt_free_string"]=function(){return(_openmpt_free_string=Module["_openmpt_free_string"]=Module["asm"]["ta"]).apply(null,arguments)};var _free=Module["_free"]=function(){return(_free=Module["_free"]=Module["asm"]["ua"]).apply(null,arguments)};var _openmpt_get_string=Module["_openmpt_get_string"]=function(){return(_openmpt_get_string=Module["_openmpt_get_string"]=Module["asm"]["va"]).apply(null,arguments)};var _openmpt_get_supported_extensions=Module["_openmpt_get_supported_extensions"]=function(){return(_openmpt_get_supported_extensions=Module["_openmpt_get_supported_extensions"]=Module["asm"]["wa"]).apply(null,arguments)};var _openmpt_is_extension_supported=Module["_openmpt_is_extension_supported"]=function(){return(_openmpt_is_extension_supported=Module["_openmpt_is_extension_supported"]=Module["asm"]["xa"]).apply(null,arguments)};var _openmpt_log_func_default=Module["_openmpt_log_func_default"]=function(){return(_openmpt_log_func_default=Module["_openmpt_log_func_default"]=Module["asm"]["ya"]).apply(null,arguments)};var _openmpt_log_func_silent=Module["_openmpt_log_func_silent"]=function(){return(_openmpt_log_func_silent=Module["_openmpt_log_func_silent"]=Module["asm"]["za"]).apply(null,arguments)};var _openmpt_error_is_transient=Module["_openmpt_error_is_transient"]=function(){return(_openmpt_error_is_transient=Module["_openmpt_error_is_transient"]=Module["asm"]["Aa"]).apply(null,arguments)};var _openmpt_error_string=Module["_openmpt_error_string"]=function(){return(_openmpt_error_string=Module["_openmpt_error_string"]=Module["asm"]["Ba"]).apply(null,arguments)};var _openmpt_error_func_default=Module["_openmpt_error_func_default"]=function(){return(_openmpt_error_func_default=Module["_openmpt_error_func_default"]=Module["asm"]["Ca"]).apply(null,arguments)};var _openmpt_error_func_log=Module["_openmpt_error_func_log"]=function(){return(_openmpt_error_func_log=Module["_openmpt_error_func_log"]=Module["asm"]["Da"]).apply(null,arguments)};var _openmpt_error_func_store=Module["_openmpt_error_func_store"]=function(){return(_openmpt_error_func_store=Module["_openmpt_error_func_store"]=Module["asm"]["Ea"]).apply(null,arguments)};var _openmpt_error_func_ignore=Module["_openmpt_error_func_ignore"]=function(){return(_openmpt_error_func_ignore=Module["_openmpt_error_func_ignore"]=Module["asm"]["Fa"]).apply(null,arguments)};var _openmpt_error_func_errno=Module["_openmpt_error_func_errno"]=function(){return(_openmpt_error_func_errno=Module["_openmpt_error_func_errno"]=Module["asm"]["Ga"]).apply(null,arguments)};var _openmpt_error_func_errno_userdata=Module["_openmpt_error_func_errno_userdata"]=function(){return(_openmpt_error_func_errno_userdata=Module["_openmpt_error_func_errno_userdata"]=Module["asm"]["Ha"]).apply(null,arguments)};var _openmpt_could_open_probability=Module["_openmpt_could_open_probability"]=function(){return(_openmpt_could_open_probability=Module["_openmpt_could_open_probability"]=Module["asm"]["Ia"]).apply(null,arguments)};var _openmpt_could_open_probability2=Module["_openmpt_could_open_probability2"]=function(){return(_openmpt_could_open_probability2=Module["_openmpt_could_open_probability2"]=Module["asm"]["Ja"]).apply(null,arguments)};var _openmpt_could_open_propability=Module["_openmpt_could_open_propability"]=function(){return(_openmpt_could_open_propability=Module["_openmpt_could_open_propability"]=Module["asm"]["Ka"]).apply(null,arguments)};var _openmpt_probe_file_header_get_recommended_size=Module["_openmpt_probe_file_header_get_recommended_size"]=function(){return(_openmpt_probe_file_header_get_recommended_size=Module["_openmpt_probe_file_header_get_recommended_size"]=Module["asm"]["La"]).apply(null,arguments)};var _openmpt_probe_file_header=Module["_openmpt_probe_file_header"]=function(){return(_openmpt_probe_file_header=Module["_openmpt_probe_file_header"]=Module["asm"]["Ma"]).apply(null,arguments)};var _openmpt_probe_file_header_without_filesize=Module["_openmpt_probe_file_header_without_filesize"]=function(){return(_openmpt_probe_file_header_without_filesize=Module["_openmpt_probe_file_header_without_filesize"]=Module["asm"]["Na"]).apply(null,arguments)};var _openmpt_probe_file_header_from_stream=Module["_openmpt_probe_file_header_from_stream"]=function(){return(_openmpt_probe_file_header_from_stream=Module["_openmpt_probe_file_header_from_stream"]=Module["asm"]["Oa"]).apply(null,arguments)};var _openmpt_module_create=Module["_openmpt_module_create"]=function(){return(_openmpt_module_create=Module["_openmpt_module_create"]=Module["asm"]["Pa"]).apply(null,arguments)};var _openmpt_module_create2=Module["_openmpt_module_create2"]=function(){return(_openmpt_module_create2=Module["_openmpt_module_create2"]=Module["asm"]["Qa"]).apply(null,arguments)};var _openmpt_module_create_from_memory=Module["_openmpt_module_create_from_memory"]=function(){return(_openmpt_module_create_from_memory=Module["_openmpt_module_create_from_memory"]=Module["asm"]["Ra"]).apply(null,arguments)};var _openmpt_module_create_from_memory2=Module["_openmpt_module_create_from_memory2"]=function(){return(_openmpt_module_create_from_memory2=Module["_openmpt_module_create_from_memory2"]=Module["asm"]["Sa"]).apply(null,arguments)};var _openmpt_module_destroy=Module["_openmpt_module_destroy"]=function(){return(_openmpt_module_destroy=Module["_openmpt_module_destroy"]=Module["asm"]["Ta"]).apply(null,arguments)};var _openmpt_module_set_log_func=Module["_openmpt_module_set_log_func"]=function(){return(_openmpt_module_set_log_func=Module["_openmpt_module_set_log_func"]=Module["asm"]["Ua"]).apply(null,arguments)};var _openmpt_module_set_error_func=Module["_openmpt_module_set_error_func"]=function(){return(_openmpt_module_set_error_func=Module["_openmpt_module_set_error_func"]=Module["asm"]["Va"]).apply(null,arguments)};var _openmpt_module_error_get_last=Module["_openmpt_module_error_get_last"]=function(){return(_openmpt_module_error_get_last=Module["_openmpt_module_error_get_last"]=Module["asm"]["Wa"]).apply(null,arguments)};var _openmpt_module_error_get_last_message=Module["_openmpt_module_error_get_last_message"]=function(){return(_openmpt_module_error_get_last_message=Module["_openmpt_module_error_get_last_message"]=Module["asm"]["Xa"]).apply(null,arguments)};var _openmpt_module_error_set_last=Module["_openmpt_module_error_set_last"]=function(){return(_openmpt_module_error_set_last=Module["_openmpt_module_error_set_last"]=Module["asm"]["Ya"]).apply(null,arguments)};var _openmpt_module_error_clear=Module["_openmpt_module_error_clear"]=function(){return(_openmpt_module_error_clear=Module["_openmpt_module_error_clear"]=Module["asm"]["Za"]).apply(null,arguments)};var _openmpt_module_select_subsong=Module["_openmpt_module_select_subsong"]=function(){return(_openmpt_module_select_subsong=Module["_openmpt_module_select_subsong"]=Module["asm"]["_a"]).apply(null,arguments)};var _openmpt_module_get_selected_subsong=Module["_openmpt_module_get_selected_subsong"]=function(){return(_openmpt_module_get_selected_subsong=Module["_openmpt_module_get_selected_subsong"]=Module["asm"]["$a"]).apply(null,arguments)};var _openmpt_module_set_repeat_count=Module["_openmpt_module_set_repeat_count"]=function(){return(_openmpt_module_set_repeat_count=Module["_openmpt_module_set_repeat_count"]=Module["asm"]["ab"]).apply(null,arguments)};var _openmpt_module_get_repeat_count=Module["_openmpt_module_get_repeat_count"]=function(){return(_openmpt_module_get_repeat_count=Module["_openmpt_module_get_repeat_count"]=Module["asm"]["bb"]).apply(null,arguments)};var _openmpt_module_get_duration_seconds=Module["_openmpt_module_get_duration_seconds"]=function(){return(_openmpt_module_get_duration_seconds=Module["_openmpt_module_get_duration_seconds"]=Module["asm"]["cb"]).apply(null,arguments)};var _openmpt_module_set_position_seconds=Module["_openmpt_module_set_position_seconds"]=function(){return(_openmpt_module_set_position_seconds=Module["_openmpt_module_set_position_seconds"]=Module["asm"]["db"]).apply(null,arguments)};var _openmpt_module_get_position_seconds=Module["_openmpt_module_get_position_seconds"]=function(){return(_openmpt_module_get_position_seconds=Module["_openmpt_module_get_position_seconds"]=Module["asm"]["eb"]).apply(null,arguments)};var _openmpt_module_set_position_order_row=Module["_openmpt_module_set_position_order_row"]=function(){return(_openmpt_module_set_position_order_row=Module["_openmpt_module_set_position_order_row"]=Module["asm"]["fb"]).apply(null,arguments)};var _openmpt_module_get_render_param=Module["_openmpt_module_get_render_param"]=function(){return(_openmpt_module_get_render_param=Module["_openmpt_module_get_render_param"]=Module["asm"]["gb"]).apply(null,arguments)};var _openmpt_module_set_render_param=Module["_openmpt_module_set_render_param"]=function(){return(_openmpt_module_set_render_param=Module["_openmpt_module_set_render_param"]=Module["asm"]["hb"]).apply(null,arguments)};var _openmpt_module_read_mono=Module["_openmpt_module_read_mono"]=function(){return(_openmpt_module_read_mono=Module["_openmpt_module_read_mono"]=Module["asm"]["ib"]).apply(null,arguments)};var _openmpt_module_read_stereo=Module["_openmpt_module_read_stereo"]=function(){return(_openmpt_module_read_stereo=Module["_openmpt_module_read_stereo"]=Module["asm"]["jb"]).apply(null,arguments)};var _openmpt_module_read_quad=Module["_openmpt_module_read_quad"]=function(){return(_openmpt_module_read_quad=Module["_openmpt_module_read_quad"]=Module["asm"]["kb"]).apply(null,arguments)};var _openmpt_module_read_float_mono=Module["_openmpt_module_read_float_mono"]=function(){return(_openmpt_module_read_float_mono=Module["_openmpt_module_read_float_mono"]=Module["asm"]["lb"]).apply(null,arguments)};var _openmpt_module_read_float_stereo=Module["_openmpt_module_read_float_stereo"]=function(){return(_openmpt_module_read_float_stereo=Module["_openmpt_module_read_float_stereo"]=Module["asm"]["mb"]).apply(null,arguments)};var _openmpt_module_read_float_quad=Module["_openmpt_module_read_float_quad"]=function(){return(_openmpt_module_read_float_quad=Module["_openmpt_module_read_float_quad"]=Module["asm"]["nb"]).apply(null,arguments)};var _openmpt_module_read_interleaved_stereo=Module["_openmpt_module_read_interleaved_stereo"]=function(){return(_openmpt_module_read_interleaved_stereo=Module["_openmpt_module_read_interleaved_stereo"]=Module["asm"]["ob"]).apply(null,arguments)};var _openmpt_module_read_interleaved_quad=Module["_openmpt_module_read_interleaved_quad"]=function(){return(_openmpt_module_read_interleaved_quad=Module["_openmpt_module_read_interleaved_quad"]=Module["asm"]["pb"]).apply(null,arguments)};var _openmpt_module_read_interleaved_float_stereo=Module["_openmpt_module_read_interleaved_float_stereo"]=function(){return(_openmpt_module_read_interleaved_float_stereo=Module["_openmpt_module_read_interleaved_float_stereo"]=Module["asm"]["qb"]).apply(null,arguments)};var _openmpt_module_read_interleaved_float_quad=Module["_openmpt_module_read_interleaved_float_quad"]=function(){return(_openmpt_module_read_interleaved_float_quad=Module["_openmpt_module_read_interleaved_float_quad"]=Module["asm"]["rb"]).apply(null,arguments)};var _openmpt_module_get_metadata_keys=Module["_openmpt_module_get_metadata_keys"]=function(){return(_openmpt_module_get_metadata_keys=Module["_openmpt_module_get_metadata_keys"]=Module["asm"]["sb"]).apply(null,arguments)};var _openmpt_module_get_metadata=Module["_openmpt_module_get_metadata"]=function(){return(_openmpt_module_get_metadata=Module["_openmpt_module_get_metadata"]=Module["asm"]["tb"]).apply(null,arguments)};var _openmpt_module_get_current_estimated_bpm=Module["_openmpt_module_get_current_estimated_bpm"]=function(){return(_openmpt_module_get_current_estimated_bpm=Module["_openmpt_module_get_current_estimated_bpm"]=Module["asm"]["ub"]).apply(null,arguments)};var _openmpt_module_get_current_speed=Module["_openmpt_module_get_current_speed"]=function(){return(_openmpt_module_get_current_speed=Module["_openmpt_module_get_current_speed"]=Module["asm"]["vb"]).apply(null,arguments)};var _openmpt_module_get_current_tempo=Module["_openmpt_module_get_current_tempo"]=function(){return(_openmpt_module_get_current_tempo=Module["_openmpt_module_get_current_tempo"]=Module["asm"]["wb"]).apply(null,arguments)};var _openmpt_module_get_current_order=Module["_openmpt_module_get_current_order"]=function(){return(_openmpt_module_get_current_order=Module["_openmpt_module_get_current_order"]=Module["asm"]["xb"]).apply(null,arguments)};var _openmpt_module_get_current_pattern=Module["_openmpt_module_get_current_pattern"]=function(){return(_openmpt_module_get_current_pattern=Module["_openmpt_module_get_current_pattern"]=Module["asm"]["yb"]).apply(null,arguments)};var _openmpt_module_get_current_row=Module["_openmpt_module_get_current_row"]=function(){return(_openmpt_module_get_current_row=Module["_openmpt_module_get_current_row"]=Module["asm"]["zb"]).apply(null,arguments)};var _openmpt_module_get_current_playing_channels=Module["_openmpt_module_get_current_playing_channels"]=function(){return(_openmpt_module_get_current_playing_channels=Module["_openmpt_module_get_current_playing_channels"]=Module["asm"]["Ab"]).apply(null,arguments)};var _openmpt_module_get_current_channel_vu_mono=Module["_openmpt_module_get_current_channel_vu_mono"]=function(){return(_openmpt_module_get_current_channel_vu_mono=Module["_openmpt_module_get_current_channel_vu_mono"]=Module["asm"]["Bb"]).apply(null,arguments)};var _openmpt_module_get_current_channel_vu_left=Module["_openmpt_module_get_current_channel_vu_left"]=function(){return(_openmpt_module_get_current_channel_vu_left=Module["_openmpt_module_get_current_channel_vu_left"]=Module["asm"]["Cb"]).apply(null,arguments)};var _openmpt_module_get_current_channel_vu_right=Module["_openmpt_module_get_current_channel_vu_right"]=function(){return(_openmpt_module_get_current_channel_vu_right=Module["_openmpt_module_get_current_channel_vu_right"]=Module["asm"]["Db"]).apply(null,arguments)};var _openmpt_module_get_current_channel_vu_rear_left=Module["_openmpt_module_get_current_channel_vu_rear_left"]=function(){return(_openmpt_module_get_current_channel_vu_rear_left=Module["_openmpt_module_get_current_channel_vu_rear_left"]=Module["asm"]["Eb"]).apply(null,arguments)};var _openmpt_module_get_current_channel_vu_rear_right=Module["_openmpt_module_get_current_channel_vu_rear_right"]=function(){return(_openmpt_module_get_current_channel_vu_rear_right=Module["_openmpt_module_get_current_channel_vu_rear_right"]=Module["asm"]["Fb"]).apply(null,arguments)};var _openmpt_module_get_num_subsongs=Module["_openmpt_module_get_num_subsongs"]=function(){return(_openmpt_module_get_num_subsongs=Module["_openmpt_module_get_num_subsongs"]=Module["asm"]["Gb"]).apply(null,arguments)};var _openmpt_module_get_num_channels=Module["_openmpt_module_get_num_channels"]=function(){return(_openmpt_module_get_num_channels=Module["_openmpt_module_get_num_channels"]=Module["asm"]["Hb"]).apply(null,arguments)};var _openmpt_module_get_num_orders=Module["_openmpt_module_get_num_orders"]=function(){return(_openmpt_module_get_num_orders=Module["_openmpt_module_get_num_orders"]=Module["asm"]["Ib"]).apply(null,arguments)};var _openmpt_module_get_num_patterns=Module["_openmpt_module_get_num_patterns"]=function(){return(_openmpt_module_get_num_patterns=Module["_openmpt_module_get_num_patterns"]=Module["asm"]["Jb"]).apply(null,arguments)};var _openmpt_module_get_num_instruments=Module["_openmpt_module_get_num_instruments"]=function(){return(_openmpt_module_get_num_instruments=Module["_openmpt_module_get_num_instruments"]=Module["asm"]["Kb"]).apply(null,arguments)};var _openmpt_module_get_num_samples=Module["_openmpt_module_get_num_samples"]=function(){return(_openmpt_module_get_num_samples=Module["_openmpt_module_get_num_samples"]=Module["asm"]["Lb"]).apply(null,arguments)};var _openmpt_module_get_subsong_name=Module["_openmpt_module_get_subsong_name"]=function(){return(_openmpt_module_get_subsong_name=Module["_openmpt_module_get_subsong_name"]=Module["asm"]["Mb"]).apply(null,arguments)};var _openmpt_module_get_channel_name=Module["_openmpt_module_get_channel_name"]=function(){return(_openmpt_module_get_channel_name=Module["_openmpt_module_get_channel_name"]=Module["asm"]["Nb"]).apply(null,arguments)};var _openmpt_module_get_order_name=Module["_openmpt_module_get_order_name"]=function(){return(_openmpt_module_get_order_name=Module["_openmpt_module_get_order_name"]=Module["asm"]["Ob"]).apply(null,arguments)};var _openmpt_module_get_pattern_name=Module["_openmpt_module_get_pattern_name"]=function(){return(_openmpt_module_get_pattern_name=Module["_openmpt_module_get_pattern_name"]=Module["asm"]["Pb"]).apply(null,arguments)};var _openmpt_module_get_instrument_name=Module["_openmpt_module_get_instrument_name"]=function(){return(_openmpt_module_get_instrument_name=Module["_openmpt_module_get_instrument_name"]=Module["asm"]["Qb"]).apply(null,arguments)};var _openmpt_module_get_sample_name=Module["_openmpt_module_get_sample_name"]=function(){return(_openmpt_module_get_sample_name=Module["_openmpt_module_get_sample_name"]=Module["asm"]["Rb"]).apply(null,arguments)};var _openmpt_module_get_order_pattern=Module["_openmpt_module_get_order_pattern"]=function(){return(_openmpt_module_get_order_pattern=Module["_openmpt_module_get_order_pattern"]=Module["asm"]["Sb"]).apply(null,arguments)};var _openmpt_module_get_pattern_num_rows=Module["_openmpt_module_get_pattern_num_rows"]=function(){return(_openmpt_module_get_pattern_num_rows=Module["_openmpt_module_get_pattern_num_rows"]=Module["asm"]["Tb"]).apply(null,arguments)};var _openmpt_module_get_pattern_row_channel_command=Module["_openmpt_module_get_pattern_row_channel_command"]=function(){return(_openmpt_module_get_pattern_row_channel_command=Module["_openmpt_module_get_pattern_row_channel_command"]=Module["asm"]["Ub"]).apply(null,arguments)};var _openmpt_module_format_pattern_row_channel_command=Module["_openmpt_module_format_pattern_row_channel_command"]=function(){return(_openmpt_module_format_pattern_row_channel_command=Module["_openmpt_module_format_pattern_row_channel_command"]=Module["asm"]["Vb"]).apply(null,arguments)};var _openmpt_module_highlight_pattern_row_channel_command=Module["_openmpt_module_highlight_pattern_row_channel_command"]=function(){return(_openmpt_module_highlight_pattern_row_channel_command=Module["_openmpt_module_highlight_pattern_row_channel_command"]=Module["asm"]["Wb"]).apply(null,arguments)};var _openmpt_module_format_pattern_row_channel=Module["_openmpt_module_format_pattern_row_channel"]=function(){return(_openmpt_module_format_pattern_row_channel=Module["_openmpt_module_format_pattern_row_channel"]=Module["asm"]["Xb"]).apply(null,arguments)};var _openmpt_module_highlight_pattern_row_channel=Module["_openmpt_module_highlight_pattern_row_channel"]=function(){return(_openmpt_module_highlight_pattern_row_channel=Module["_openmpt_module_highlight_pattern_row_channel"]=Module["asm"]["Yb"]).apply(null,arguments)};var _openmpt_module_get_ctls=Module["_openmpt_module_get_ctls"]=function(){return(_openmpt_module_get_ctls=Module["_openmpt_module_get_ctls"]=Module["asm"]["Zb"]).apply(null,arguments)};var _openmpt_module_ctl_get=Module["_openmpt_module_ctl_get"]=function(){return(_openmpt_module_ctl_get=Module["_openmpt_module_ctl_get"]=Module["asm"]["_b"]).apply(null,arguments)};var _openmpt_module_ctl_get_boolean=Module["_openmpt_module_ctl_get_boolean"]=function(){return(_openmpt_module_ctl_get_boolean=Module["_openmpt_module_ctl_get_boolean"]=Module["asm"]["$b"]).apply(null,arguments)};var _openmpt_module_ctl_get_integer=Module["_openmpt_module_ctl_get_integer"]=function(){return(_openmpt_module_ctl_get_integer=Module["_openmpt_module_ctl_get_integer"]=Module["asm"]["ac"]).apply(null,arguments)};var _openmpt_module_ctl_get_floatingpoint=Module["_openmpt_module_ctl_get_floatingpoint"]=function(){return(_openmpt_module_ctl_get_floatingpoint=Module["_openmpt_module_ctl_get_floatingpoint"]=Module["asm"]["bc"]).apply(null,arguments)};var _openmpt_module_ctl_get_text=Module["_openmpt_module_ctl_get_text"]=function(){return(_openmpt_module_ctl_get_text=Module["_openmpt_module_ctl_get_text"]=Module["asm"]["cc"]).apply(null,arguments)};var _openmpt_module_ctl_set=Module["_openmpt_module_ctl_set"]=function(){return(_openmpt_module_ctl_set=Module["_openmpt_module_ctl_set"]=Module["asm"]["dc"]).apply(null,arguments)};var _openmpt_module_ctl_set_boolean=Module["_openmpt_module_ctl_set_boolean"]=function(){return(_openmpt_module_ctl_set_boolean=Module["_openmpt_module_ctl_set_boolean"]=Module["asm"]["ec"]).apply(null,arguments)};var _openmpt_module_ctl_set_integer=Module["_openmpt_module_ctl_set_integer"]=function(){return(_openmpt_module_ctl_set_integer=Module["_openmpt_module_ctl_set_integer"]=Module["asm"]["fc"]).apply(null,arguments)};var _openmpt_module_ctl_set_floatingpoint=Module["_openmpt_module_ctl_set_floatingpoint"]=function(){return(_openmpt_module_ctl_set_floatingpoint=Module["_openmpt_module_ctl_set_floatingpoint"]=Module["asm"]["gc"]).apply(null,arguments)};var _openmpt_module_ctl_set_text=Module["_openmpt_module_ctl_set_text"]=function(){return(_openmpt_module_ctl_set_text=Module["_openmpt_module_ctl_set_text"]=Module["asm"]["hc"]).apply(null,arguments)};var _openmpt_module_ext_create=Module["_openmpt_module_ext_create"]=function(){return(_openmpt_module_ext_create=Module["_openmpt_module_ext_create"]=Module["asm"]["ic"]).apply(null,arguments)};var _openmpt_module_ext_create_from_memory=Module["_openmpt_module_ext_create_from_memory"]=function(){return(_openmpt_module_ext_create_from_memory=Module["_openmpt_module_ext_create_from_memory"]=Module["asm"]["jc"]).apply(null,arguments)};var _openmpt_module_ext_destroy=Module["_openmpt_module_ext_destroy"]=function(){return(_openmpt_module_ext_destroy=Module["_openmpt_module_ext_destroy"]=Module["asm"]["kc"]).apply(null,arguments)};var _openmpt_module_ext_get_module=Module["_openmpt_module_ext_get_module"]=function(){return(_openmpt_module_ext_get_module=Module["_openmpt_module_ext_get_module"]=Module["asm"]["lc"]).apply(null,arguments)};var _openmpt_module_ext_get_interface=Module["_openmpt_module_ext_get_interface"]=function(){return(_openmpt_module_ext_get_interface=Module["_openmpt_module_ext_get_interface"]=Module["asm"]["mc"]).apply(null,arguments)};var _malloc=Module["_malloc"]=function(){return(_malloc=Module["_malloc"]=Module["asm"]["nc"]).apply(null,arguments)};var _setThrew=Module["_setThrew"]=function(){return(_setThrew=Module["_setThrew"]=Module["asm"]["pc"]).apply(null,arguments)};var setTempRet0=Module["setTempRet0"]=function(){return(setTempRet0=Module["setTempRet0"]=Module["asm"]["qc"]).apply(null,arguments)};var stackSave=Module["stackSave"]=function(){return(stackSave=Module["stackSave"]=Module["asm"]["rc"]).apply(null,arguments)};var stackRestore=Module["stackRestore"]=function(){return(stackRestore=Module["stackRestore"]=Module["asm"]["sc"]).apply(null,arguments)};var ___cxa_can_catch=Module["___cxa_can_catch"]=function(){return(___cxa_can_catch=Module["___cxa_can_catch"]=Module["asm"]["tc"]).apply(null,arguments)};var ___cxa_is_pointer_type=Module["___cxa_is_pointer_type"]=function(){return(___cxa_is_pointer_type=Module["___cxa_is_pointer_type"]=Module["asm"]["uc"]).apply(null,arguments)};var dynCall_j=Module["dynCall_j"]=function(){return(dynCall_j=Module["dynCall_j"]=Module["asm"]["vc"]).apply(null,arguments)};var dynCall_iiji=Module["dynCall_iiji"]=function(){return(dynCall_iiji=Module["dynCall_iiji"]=Module["asm"]["wc"]).apply(null,arguments)};var dynCall_vij=Module["dynCall_vij"]=function(){return(dynCall_vij=Module["dynCall_vij"]=Module["asm"]["xc"]).apply(null,arguments)};var dynCall_viij=Module["dynCall_viij"]=function(){return(dynCall_viij=Module["dynCall_viij"]=Module["asm"]["yc"]).apply(null,arguments)};var dynCall_jiiii=Module["dynCall_jiiii"]=function(){return(dynCall_jiiii=Module["dynCall_jiiii"]=Module["asm"]["zc"]).apply(null,arguments)};var dynCall_viiji=Module["dynCall_viiji"]=function(){return(dynCall_viiji=Module["dynCall_viiji"]=Module["asm"]["Ac"]).apply(null,arguments)};var dynCall_iiij=Module["dynCall_iiij"]=function(){return(dynCall_iiij=Module["dynCall_iiij"]=Module["asm"]["Bc"]).apply(null,arguments)};function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iid(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fi(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiifii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_di(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viid(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiidd(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiji(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_iiji(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vij(index,a1,a2,a3){var sp=stackSave();try{dynCall_vij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_jiiii(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiji(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return dynCall_j(index)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiij(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_iiij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(args){args=args||arguments_;if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run(); - -Module.UTF8ToString = UTF8ToString; -Module.writeAsciiToMemory = writeAsciiToMemory; -export { Module } diff --git a/packages/frontend/src/scripts/libopenmpt/readme.md b/packages/frontend/src/scripts/libopenmpt/readme.md deleted file mode 100644 index 4b99a6c40f..0000000000 --- a/packages/frontend/src/scripts/libopenmpt/readme.md +++ /dev/null @@ -1,23 +0,0 @@ -modifications made to `libopenmpt.js` (can be taken from https://lib.openmpt.org/libopenmpt/download/): - -at the beginning of the file: -```js -// @ts-nocheck -/* eslint-disable */ -``` - -at the end of the file: -```js -Module.UTF8ToString = UTF8ToString; -Module.writeAsciiToMemory = writeAsciiToMemory; -export { Module } -``` - -replace -``` -wasmBinaryFile="libopenmpt.wasm" -``` -with -``` -wasmBinaryFile=new URL("./libopenmpt.wasm", import.meta.url).href -``` \ No newline at end of file diff --git a/packages/frontend/src/scripts/login-id.ts b/packages/frontend/src/scripts/login-id.ts index b52735caa0..fe0e17e66e 100644 --- a/packages/frontend/src/scripts/login-id.ts +++ b/packages/frontend/src/scripts/login-id.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/admin-lookup.ts b/packages/frontend/src/scripts/lookup-user.ts similarity index 60% rename from packages/frontend/src/scripts/admin-lookup.ts rename to packages/frontend/src/scripts/lookup-user.ts index 1b57b853c9..a35fe898e4 100644 --- a/packages/frontend/src/scripts/admin-lookup.ts +++ b/packages/frontend/src/scripts/lookup-user.ts @@ -1,12 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; export async function lookupUser() { const { canceled, result } = await os.inputText({ @@ -18,8 +17,8 @@ export async function lookupUser() { os.pageWindow(`/admin/user/${user.id}`); }; - const usernamePromise = misskeyApi('users/show', Misskey.acct.parse(result)); - const idPromise = misskeyApi('users/show', { userId: result }); + const usernamePromise = os.api('users/show', Misskey.acct.parse(result)); + const idPromise = os.api('users/show', { userId: result }); let _notFound = false; const notFound = () => { if (_notFound) { @@ -63,26 +62,3 @@ export async function lookupUserByEmail() { } } } - -export async function lookupFile() { - const { canceled, result: q } = await os.inputText({ - title: i18n.ts.fileIdOrUrl, - minLength: 1, - }); - if (canceled) return; - - const show = (file) => { - os.pageWindow(`/admin/file/${file.id}`); - }; - - misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => { - show(file); - }).catch(err => { - if (err.code === 'NO_SUCH_FILE') { - os.alert({ - type: 'error', - text: i18n.ts.notFound, - }); - } - }); -} diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts index e20b23f166..979f40f038 100644 --- a/packages/frontend/src/scripts/lookup.ts +++ b/packages/frontend/src/scripts/lookup.ts @@ -1,13 +1,12 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { mainRouter } from '@/router.js'; import { Router } from '@/nirax.js'; -import { mainRouter } from '@/router/main.js'; export async function lookup(router?: Router) { const _router = router ?? mainRouter; @@ -16,7 +15,7 @@ export async function lookup(router?: Router) { title: i18n.ts.lookup, }); const query = temp ? temp.trim() : ''; - if (canceled || query.length <= 1) return; + if (canceled) return; if (query.startsWith('@') && !query.includes(' ')) { _router.push(`/${query}`); @@ -28,8 +27,8 @@ export async function lookup(router?: Router) { return; } - if (query.startsWith('http://') || query.startsWith('https://')) { - const promise = misskeyApi('ap/show', { + if (query.startsWith('https://')) { + const promise = os.api('ap/show', { uri: query, }); diff --git a/packages/frontend/src/scripts/media-has-audio.ts b/packages/frontend/src/scripts/media-has-audio.ts deleted file mode 100644 index 4bf3ee5d97..0000000000 --- a/packages/frontend/src/scripts/media-has-audio.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export default async function hasAudio(media: HTMLMediaElement) { - const cloned = media.cloneNode() as HTMLMediaElement; - cloned.muted = (cloned as typeof cloned & Partial).playsInline = true; - cloned.play(); - await new Promise((resolve) => cloned.addEventListener('playing', resolve)); - const result = !!(cloned as any).audioTracks?.length || (cloned as any).mozHasAudio || !!(cloned as any).webkitAudioDecodedByteCount; - cloned.remove(); - return result; -} diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index 78eba35ead..559e61211d 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -1,34 +1,53 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { MediaProxy } from '@@/js/media-proxy.js'; -import { url } from '@@/js/config.js'; +import { query } from '@/scripts/url.js'; +import { url } from '@/config.js'; import { instance } from '@/instance.js'; -let _mediaProxy: MediaProxy | null = null; +export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { + const localProxy = `${url}/proxy`; -export function getProxiedImageUrl(...args: Parameters): string { - if (_mediaProxy == null) { - _mediaProxy = new MediaProxy(instance, url); + if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { + // もう既にproxyっぽそうだったらurlを取り出す + imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; } - return _mediaProxy.getProxiedImageUrl(...args); + return `${mustOrigin ? localProxy : instance.mediaProxy}/${ + type === 'preview' ? 'preview.webp' + : 'image.webp' + }?${query({ + url: imageUrl, + ...(!noFallback ? { 'fallback': '1' } : {}), + ...(type ? { [type]: '1' } : {}), + ...(mustOrigin ? { origin: '1' } : {}), + })}`; } -export function getProxiedImageUrlNullable(...args: Parameters): string | null { - if (_mediaProxy == null) { - _mediaProxy = new MediaProxy(instance, url); +export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { + if (imageUrl == null) return null; + return getProxiedImageUrl(imageUrl, type); +} + +export function getStaticImageUrl(baseUrl: string): string { + const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url); + + if (u.href.startsWith(`${url}/emoji/`)) { + // もう既にemojiっぽそうだったらsearchParams付けるだけ + u.searchParams.set('static', '1'); + return u.href; } - return _mediaProxy.getProxiedImageUrlNullable(...args); -} - -export function getStaticImageUrl(...args: Parameters): string { - if (_mediaProxy == null) { - _mediaProxy = new MediaProxy(instance, url); + if (u.href.startsWith(instance.mediaProxy + '/')) { + // もう既にproxyっぽそうだったらsearchParams付けるだけ + u.searchParams.set('static', '1'); + return u.href; } - return _mediaProxy.getStaticImageUrl(...args); + return `${instance.mediaProxy}/static.webp?${query({ + url: u.href, + static: '1', + })}`; } diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts deleted file mode 100644 index 89fdda0cbb..0000000000 --- a/packages/frontend/src/scripts/merge.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { deepClone } from './clone.js'; -import type { Cloneable } from './clone.js'; - -export type DeepPartial = { - [P in keyof T]?: T[P] extends Record ? DeepPartial : T[P]; -}; - -function isPureObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -/** - * valueにないキーをdefからもらう(再帰的)\ - * nullはそのまま、undefinedはdefの値 - **/ -export function deepMerge(value: DeepPartial, def: X): X { - if (isPureObject(value) && isPureObject(def)) { - const result = deepClone(value as Cloneable) as X; - for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) { - if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) { - result[k] = v; - } else if (isPureObject(v) && isPureObject(result[k])) { - const child = deepClone(result[k] as Cloneable) as DeepPartial>; - result[k] = deepMerge(child, v); - } - } - return result; - } - throw new Error('deepMerge: value and def must be pure objects'); -} diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts deleted file mode 100644 index 2911469cdd..0000000000 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Ref, nextTick } from 'vue'; -import * as os from '@/os.js'; -import { i18n } from '@/i18n.js'; -import { MFM_TAGS } from '@@/js/const.js'; -import type { MenuItem } from '@/types/menu.js'; - -/** - * MFMの装飾のリストを表示する - */ -export function mfmFunctionPicker(src: HTMLElement | EventTarget | null, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref) { - os.popupMenu([{ - text: i18n.ts.addMfmFunction, - type: 'label', - }, ...getFunctionList(textArea, textRef)], src); -} - -function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref): MenuItem[] { - return MFM_TAGS.map(tag => ({ - text: tag, - icon: 'ph-brackets-curly ph-bold ph-lg', - action: () => add(textArea, textRef, tag), - })); -} - -function add(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref, type: string) { - const caretStart: number = textArea.selectionStart as number; - const caretEnd: number = textArea.selectionEnd as number; - - MFM_TAGS.forEach(tag => { - if (type === tag) { - if (caretStart === caretEnd) { - // 単純にFunctionを追加 - const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ]${textRef.value.substring(caretEnd)}`; - textRef.value = trimmedText; - } else { - // 選択範囲を囲むようにFunctionを追加 - const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ${textRef.value.substring(caretStart, caretEnd)}]${textRef.value.substring(caretEnd)}`; - textRef.value = trimmedText; - } - } - }); - - const nextCaretStart: number = caretStart + 3 + type.length; - const nextCaretEnd: number = caretEnd + 3 + type.length; - - // キャレットを戻す - nextTick(() => { - textArea.focus(); - textArea.setSelectionRange(nextCaretStart, nextCaretEnd); - }); -} diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts deleted file mode 100644 index 1b1159fd01..0000000000 --- a/packages/frontend/src/scripts/misskey-api.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { ref } from 'vue'; -import { apiUrl } from '@@/js/config.js'; -import { $i } from '@/account.js'; -export const pendingApiRequestsCount = ref(0); - -// Implements Misskey.api.ApiClient.request -export function misskeyApi< - ResT = void, - E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, - P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], - _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType : ResT, ->( - endpoint: E, - data: P = {} as any, - token?: string | null | undefined, - signal?: AbortSignal, -): Promise<_ResT> { - if (endpoint.includes('://')) throw new Error('invalid endpoint'); - pendingApiRequestsCount.value++; - - const onFinally = () => { - pendingApiRequestsCount.value--; - }; - - const promise = new Promise<_ResT>((resolve, reject) => { - // Append a credential - if ($i) (data as any).i = $i.token; - if (token !== undefined) (data as any).i = token; - - // Send request - window.fetch(`${apiUrl}/${endpoint}`, { - method: 'POST', - body: JSON.stringify(data), - credentials: 'omit', - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - }, - signal, - }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(undefined as _ResT); // void -> undefined - } else { - reject(body.error); - } - }).catch(reject); - }); - - promise.then(onFinally, onFinally); - - return promise; -} - -// Implements Misskey.api.ApiClient.request -export function misskeyApiGet< - ResT = void, - E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, - P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], - _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType : ResT, ->( - endpoint: E, - data: P = {} as any, -): Promise<_ResT> { - pendingApiRequestsCount.value++; - - const onFinally = () => { - pendingApiRequestsCount.value--; - }; - - const query = new URLSearchParams(data as any); - - const promise = new Promise<_ResT>((resolve, reject) => { - // Send request - window.fetch(`${apiUrl}/${endpoint}?${query}`, { - method: 'GET', - credentials: 'omit', - cache: 'default', - }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(undefined as _ResT); // void -> undefined - } else { - reject(body.error); - } - }).catch(reject); - }); - - promise.then(onFinally, onFinally); - - return promise; -} diff --git a/packages/frontend/src/scripts/navigator.ts b/packages/frontend/src/scripts/navigator.ts deleted file mode 100644 index ffc0a457f4..0000000000 --- a/packages/frontend/src/scripts/navigator.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function isSupportShare(): boolean { - return 'share' in navigator; -} diff --git a/packages/frontend/src/scripts/nyaize.ts b/packages/frontend/src/scripts/nyaize.ts new file mode 100644 index 0000000000..62833b4de3 --- /dev/null +++ b/packages/frontend/src/scripts/nyaize.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const enRegex1 = /(?<=n)a/gi; +const enRegex2 = /(?<=morn)ing/gi; +const enRegex3 = /(?<=every)one/gi; +const koRegex1 = /[나-낳]/g; +const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm; +const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm; + +export function nyaize(text: string): string { + return text + // ja-JP + .replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ') + // en-US + .replace(enRegex1, x => x === 'A' ? 'YA' : 'ya') + .replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan') + .replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan') + // ko-KR + .replace(koRegex1, match => String.fromCharCode( + match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0), + )) + .replace(koRegex2, '다냥') + .replace(koRegex3, '냥'); +} diff --git a/packages/frontend/src/scripts/page-metadata.ts b/packages/frontend/src/scripts/page-metadata.ts index 0e3b093ecf..330ba8da83 100644 --- a/packages/frontend/src/scripts/page-metadata.ts +++ b/packages/frontend/src/scripts/page-metadata.ts @@ -1,10 +1,13 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import * as Misskey from 'misskey-js'; -import { MaybeRefOrGetter, Ref, inject, isRef, onActivated, onBeforeUnmount, provide, ref, toValue, watch } from 'vue'; +import { ComputedRef, inject, isRef, onActivated, onMounted, provide, ref, Ref } from 'vue'; + +export const setPageMetadata = Symbol('setPageMetadata'); +export const pageMetadataProvider = Symbol('pageMetadataProvider'); export type PageMetadata = { title: string; @@ -12,59 +15,31 @@ export type PageMetadata = { icon?: string | null; avatar?: Misskey.entities.User | null; userName?: Misskey.entities.User | null; - needWideArea?: boolean; }; -type PageMetadataGetter = () => PageMetadata; -type PageMetadataReceiver = (getter: PageMetadataGetter) => void; +export function definePageMetadata(metadata: PageMetadata | null | Ref | ComputedRef): void { + const _metadata = isRef(metadata) ? metadata : ref(metadata); -const RECEIVER_KEY = Symbol('ReceiverKey'); -const setReceiver = (v: PageMetadataReceiver): void => { - provide(RECEIVER_KEY, v); -}; -const getReceiver = (): PageMetadataReceiver | undefined => { - return inject(RECEIVER_KEY); -}; + provide(pageMetadataProvider, _metadata); -const METADATA_KEY = Symbol('MetadataKey'); -const setMetadata = (v: Ref): void => { - provide>(METADATA_KEY, v); -}; -const getMetadata = (): Ref | undefined => { - return inject>(METADATA_KEY); -}; + const set = inject(setPageMetadata) as any; + if (set) { + set(_metadata); -export const definePageMetadata = (maybeRefOrGetterMetadata: MaybeRefOrGetter): void => { - const metadataRef = ref(toValue(maybeRefOrGetterMetadata)); - const metadataGetter = () => metadataRef.value; - const receiver = getReceiver(); + onMounted(() => { + set(_metadata); + }); - // setup handler - receiver?.(metadataGetter); + onActivated(() => { + set(_metadata); + }); + } +} - // update handler - onBeforeUnmount(watch( - () => toValue(maybeRefOrGetterMetadata), - (metadata) => { - metadataRef.value = metadata; - receiver?.(metadataGetter); - }, - { deep: true }, - )); - onActivated(() => { - receiver?.(metadataGetter); - }); -}; +export function provideMetadataReceiver(callback: (info: ComputedRef) => void): void { + provide(setPageMetadata, callback); +} -export const provideMetadataReceiver = (receiver: PageMetadataReceiver): void => { - setReceiver(receiver); -}; - -export const provideReactiveMetadata = (metadataRef: Ref): void => { - setMetadata(metadataRef); -}; - -export const injectReactiveMetadata = (): Ref => { - const metadataRef = getMetadata(); - return isRef(metadataRef) ? metadataRef : ref(null); -}; +export function injectPageMetadata(): PageMetadata | undefined { + return inject(pageMetadataProvider); +} diff --git a/packages/frontend/src/scripts/physics.ts b/packages/frontend/src/scripts/physics.ts index 8a4e9319b3..cf9fad70eb 100644 --- a/packages/frontend/src/scripts/physics.ts +++ b/packages/frontend/src/scripts/physics.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/player-url-transform.ts b/packages/frontend/src/scripts/player-url-transform.ts deleted file mode 100644 index 39c6df6500..0000000000 --- a/packages/frontend/src/scripts/player-url-transform.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { hostname } from '@@/js/config.js'; - -export function transformPlayerUrl(url: string): string { - const urlObj = new URL(url); - if (!['https:', 'http:'].includes(urlObj.protocol)) throw new Error('Invalid protocol'); - - const urlParams = new URLSearchParams(urlObj.search); - - if (urlObj.hostname === 'player.twitch.tv') { - // TwitchはCSPの制約あり - // https://dev.twitch.tv/docs/embed/video-and-clips/ - urlParams.set('parent', hostname); - urlParams.set('allowfullscreen', ''); - urlParams.set('autoplay', 'true'); - } else { - urlParams.set('autoplay', '1'); - urlParams.set('auto_play', '1'); - } - urlObj.search = urlParams.toString(); - - return urlObj.toString(); -} diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts index 18f05bc7f4..e6c08dfbc0 100644 --- a/packages/frontend/src/scripts/please-login.ts +++ b/packages/frontend/src/scripts/please-login.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,57 +8,19 @@ import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { popup } from '@/os.js'; -export type OpenOnRemoteOptions = { - /** - * 外部のMisskey Webで特定のパスを開く - */ - type: 'web'; - - /** - * 内部パス(例: `/settings`) - */ - path: string; -} | { - /** - * 外部のMisskey Webで照会する - */ - type: 'lookup'; - - /** - * 照会したいエンティティのURL - * - * (例: `https://misskey.example.com/notes/abcdexxxxyz`) - */ - url: string; -} | { - /** - * 外部のMisskeyでノートする - */ - type: 'share'; - - /** - * `/share` ページに渡すクエリストリング - * - * @see https://go.misskey-hub.net/spec/share/ - */ - params: Record; -}; - -export function pleaseLogin(path?: string, openOnRemote?: OpenOnRemoteOptions) { +export function pleaseLogin(path?: string) { if ($i) return; - const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { + popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { autoSet: true, - message: openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired, - openOnRemote, + message: i18n.ts.signinRequired, }, { cancelled: () => { if (path) { window.location.href = path; } }, - closed: () => dispose(), - }); + }, 'closed'); throw new Error('signin required'); } diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts index 5b141222e8..0c2ff16992 100644 --- a/packages/frontend/src/scripts/popout.ts +++ b/packages/frontend/src/scripts/popout.ts @@ -1,10 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { appendQuery } from '@@/js/url.js'; -import * as config from '@@/js/config.js'; +import { appendQuery } from './url.js'; +import * as config from '@/config.js'; export function popout(path: string, w?: HTMLElement) { let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path; diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts index be49532cf8..0a799c5665 100644 --- a/packages/frontend/src/scripts/popup-position.ts +++ b/packages/frontend/src/scripts/popup-position.ts @@ -1,10 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ export function calcPopupPosition(el: HTMLElement, props: { - anchorElement?: HTMLElement | null; + anchorElement: HTMLElement | null; innerMargin: number; direction: 'top' | 'bottom' | 'left' | 'right'; align: 'top' | 'bottom' | 'left' | 'right' | 'center'; @@ -15,8 +15,6 @@ export function calcPopupPosition(el: HTMLElement, props: { const contentWidth = el.offsetWidth; const contentHeight = el.offsetHeight; - const HORIZONTAL_MARGIN = 1; - let rect: DOMRect; if (props.anchorElement) { @@ -28,8 +26,8 @@ export function calcPopupPosition(el: HTMLElement, props: { let top: number; if (props.anchorElement) { - left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2); - top = (rect.top + window.scrollY - contentHeight) - props.innerMargin; + left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin; } else { left = props.x; top = (props.y - contentHeight) - props.innerMargin; @@ -37,12 +35,10 @@ export function calcPopupPosition(el: HTMLElement, props: { left -= (el.offsetWidth / 2); - if (left + contentWidth - window.scrollX > window.innerWidth) { - left = window.innerWidth - contentWidth + window.scrollX - HORIZONTAL_MARGIN; + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; } - left = Math.max(HORIZONTAL_MARGIN, left); - return [left, top]; }; @@ -51,8 +47,8 @@ export function calcPopupPosition(el: HTMLElement, props: { let top: number; if (props.anchorElement) { - left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2); - top = (rect.top + window.scrollY + props.anchorElement.offsetHeight) + props.innerMargin; + left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin; } else { left = props.x; top = (props.y) + props.innerMargin; @@ -60,12 +56,10 @@ export function calcPopupPosition(el: HTMLElement, props: { left -= (el.offsetWidth / 2); - if (left + contentWidth - window.scrollX > window.innerWidth) { - left = window.innerWidth - contentWidth + window.scrollX - HORIZONTAL_MARGIN; + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; } - left = Math.max(HORIZONTAL_MARGIN, left); - return [left, top]; }; @@ -74,19 +68,17 @@ export function calcPopupPosition(el: HTMLElement, props: { let top: number; if (props.anchorElement) { - left = (rect.left + window.scrollX - contentWidth) - props.innerMargin; - top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2); + left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin; + top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); } else { left = (props.x - contentWidth) - props.innerMargin; top = props.y; } - left = Math.max(HORIZONTAL_MARGIN, left); - top -= (el.offsetHeight / 2); - if (top + contentHeight - window.scrollY > window.innerHeight) { - top = window.innerHeight - contentHeight + window.scrollY - 1; + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; } return [left, top]; @@ -97,15 +89,15 @@ export function calcPopupPosition(el: HTMLElement, props: { let top: number; if (props.anchorElement) { - left = (rect.left + props.anchorElement.offsetWidth + window.scrollX) + props.innerMargin; + left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin; if (props.align === 'top') { - top = rect.top + window.scrollY; + top = rect.top + window.pageYOffset; if (props.alignOffset != null) top += props.alignOffset; } else if (props.align === 'bottom') { // TODO } else { // center - top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2); + top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); top -= (el.offsetHeight / 2); } } else { @@ -114,10 +106,8 @@ export function calcPopupPosition(el: HTMLElement, props: { top -= (el.offsetHeight / 2); } - left = Math.max(HORIZONTAL_MARGIN, left); - - if (top + contentHeight - window.scrollY > window.innerHeight) { - top = window.innerHeight - contentHeight + window.scrollY - 1; + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; } return [left, top]; @@ -133,7 +123,7 @@ export function calcPopupPosition(el: HTMLElement, props: { const [left, top] = calcPosWhenTop(); // ツールチップを上に向かって表示するスペースがなければ下に向かって出す - if (top - window.scrollY < 0) { + if (top - window.pageYOffset < 0) { const [left, top] = calcPosWhenBottom(); return { left, top, transformOrigin: 'center top' }; } @@ -151,7 +141,7 @@ export function calcPopupPosition(el: HTMLElement, props: { const [left, top] = calcPosWhenLeft(); // ツールチップを左に向かって表示するスペースがなければ右に向かって出す - if (left - window.scrollX < 0) { + if (left - window.pageXOffset < 0) { const [left, top] = calcPosWhenRight(); return { left, top, transformOrigin: 'left center' }; } diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts deleted file mode 100644 index 11b6f52ddd..0000000000 --- a/packages/frontend/src/scripts/post-message.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const postMessageEventTypes = [ - 'misskey:shareForm:shareCompleted', -] as const; - -export type PostMessageEventType = typeof postMessageEventTypes[number]; - -export type MiPostMessageEvent = { - type: PostMessageEventType; - payload?: any; -}; - -/** - * 親フレームにイベントを送信 - */ -export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void { - window.parent.postMessage({ - type, - payload, - }, '*'); -} diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts index 7aec05c0cf..19e1bfba2c 100644 --- a/packages/frontend/src/scripts/reaction-picker.ts +++ b/packages/frontend/src/scripts/reaction-picker.ts @@ -1,17 +1,14 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import * as Misskey from 'misskey-js'; import { defineAsyncComponent, Ref, ref } from 'vue'; import { popup } from '@/os.js'; -import { defaultStore } from '@/store.js'; class ReactionPicker { private src: Ref = ref(null); private manualShowing = ref(false); - private targetNote: Ref = ref(null); private onChosen?: (reaction: string) => void; private onClosed?: () => void; @@ -20,30 +17,26 @@ class ReactionPicker { } public async init() { - const reactionsRef = defaultStore.reactiveState.reactions; await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { src: this.src, - pinnedEmojis: reactionsRef, asReactionPicker: true, - targetNote: this.targetNote, manualShowing: this.manualShowing, }, { done: reaction => { - if (this.onChosen) this.onChosen(reaction); + this.onChosen!(reaction); }, close: () => { this.manualShowing.value = false; }, closed: () => { this.src.value = null; - if (this.onClosed) this.onClosed(); + this.onClosed!(); }, }); } - public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) { + public show(src: HTMLElement, onChosen: ReactionPicker['onChosen'], onClosed: ReactionPicker['onClosed']) { this.src.value = src; - this.targetNote.value = targetNote; this.manualShowing.value = true; this.onChosen = onChosen; this.onClosed = onClosed; diff --git a/packages/frontend/src/scripts/reload-ask.ts b/packages/frontend/src/scripts/reload-ask.ts deleted file mode 100644 index 733d91b85a..0000000000 --- a/packages/frontend/src/scripts/reload-ask.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { i18n } from '@/i18n.js'; -import * as os from '@/os.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; - -let isReloadConfirming = false; - -export async function reloadAsk(opts: { - unison?: boolean; - reason?: string; -}) { - if (isReloadConfirming) { - return; - } - - isReloadConfirming = true; - - const { canceled } = await os.confirm(opts.reason == null ? { - type: 'info', - text: i18n.ts.reloadConfirm, - } : { - type: 'info', - title: i18n.ts.reloadConfirm, - text: opts.reason, - }).finally(() => { - isReloadConfirming = false; - }); - - if (canceled) return; - - if (opts.unison) { - unisonReload(); - } else { - location.reload(); - } -} diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts new file mode 100644 index 0000000000..625d8c34a7 --- /dev/null +++ b/packages/frontend/src/scripts/safe-uri-decode.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} diff --git a/packages/frontend/src/scripts/sanitize-html.ts b/packages/frontend/src/scripts/sanitize-html.ts deleted file mode 100644 index 6e1a46c746..0000000000 --- a/packages/frontend/src/scripts/sanitize-html.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: dakkar and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only -*/ - -import original from 'sanitize-html'; - -export default function sanitizeHtml(str: string | null): string | null { - if (str == null) return str; - return original(str, { - allowedTags: original.defaults.allowedTags.concat(['img', 'audio', 'video', 'center', 'details', 'summary']), - allowedAttributes: { - ...original.defaults.allowedAttributes, - a: original.defaults.allowedAttributes.a.concat(['style']), - img: original.defaults.allowedAttributes.img.concat(['style']), - }, - }); -} diff --git a/packages/frontend-shared/js/scroll.ts b/packages/frontend/src/scripts/scroll.ts similarity index 74% rename from packages/frontend-shared/js/scroll.ts rename to packages/frontend/src/scripts/scroll.ts index 4f2e9105c3..1f626e4c0d 100644 --- a/packages/frontend-shared/js/scroll.ts +++ b/packages/frontend/src/scripts/scroll.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -23,40 +23,24 @@ export function getStickyTop(el: HTMLElement, container: HTMLElement | null = nu return getStickyTop(el.parentElement, container, newTop); } -export function getStickyBottom(el: HTMLElement, container: HTMLElement | null = null, bottom = 0) { - if (!el.parentElement) return bottom; - const data = el.dataset.stickyContainerFooterHeight; - const newBottom = data ? Number(data) + bottom : bottom; - if (el === container) return newBottom; - return getStickyBottom(el.parentElement, container, newBottom); -} - export function getScrollPosition(el: HTMLElement | null): number { const container = getScrollContainer(el); return container == null ? window.scrollY : container.scrollTop; } -export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknown, tolerance = 1, once = false) { +export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { // とりあえず評価してみる - const firstTopVisible = isTopVisible(el); - if (el.isConnected && firstTopVisible) { - cb(firstTopVisible); + if (el.isConnected && isTopVisible(el)) { + cb(); if (once) return null; } const container = getScrollContainer(el) ?? window; - // 以下のケースにおいて、cbが何度も呼び出されてしまって具合が悪いので1回呼んだら以降は無視するようにする - // - スクロールイベントは1回のスクロールで複数回発生することがある - // - toleranceの範囲内に収まる程度の微量なスクロールが発生した - let prevTopVisible = firstTopVisible; - const onScroll = () => { + const onScroll = ev => { if (!document.body.contains(el)) return; - - const topVisible = isTopVisible(el, tolerance); - if (topVisible !== prevTopVisible) { - prevTopVisible = topVisible; - cb(topVisible); + if (isTopVisible(el, tolerance)) { + cb(); if (once) removeListener(); } }; @@ -77,7 +61,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1 } const containerOrWindow = container ?? window; - const onScroll = () => { + const onScroll = ev => { if (!document.body.contains(el)) return; if (isBottomVisible(el, 1, container)) { cb(); @@ -134,7 +118,6 @@ export function scrollToBottom( export function isTopVisible(el: HTMLElement, tolerance = 1): boolean { const scrollTop = getScrollPosition(el); - if (_DEV_) console.log(scrollTop, tolerance, scrollTop <= tolerance); return scrollTop <= tolerance; } diff --git a/packages/frontend/src/scripts/search-emoji.ts b/packages/frontend/src/scripts/search-emoji.ts deleted file mode 100644 index 4192a2df8f..0000000000 --- a/packages/frontend/src/scripts/search-emoji.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type EmojiDef = { - emoji: string; - name: string; - url: string; - aliasOf?: string; -} | { - emoji: string; - name: string; - aliasOf?: string; - isCustomEmoji?: true; -}; -type EmojiScore = { emoji: EmojiDef, score: number }; - -export function searchEmoji(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] { - if (!query) { - return []; - } - - const matched = new Map(); - // 完全一致(エイリアスなし) - emojiDb.some(x => { - if (x.name.toLowerCase() === query && !x.aliasOf) { - matched.set(x.name, { emoji: x, score: query.length + 3 }); - } - return matched.size === max; - }); - - // 完全一致(エイリアス込み) - if (matched.size < max) { - emojiDb.some(x => { - if (x.name.toLowerCase() === query && !matched.has(x.aliasOf ?? x.name)) { - matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 }); - } - return matched.size === max; - }); - } - - // 前方一致(エイリアスなし) - if (matched.size < max) { - emojiDb.some(x => { - if (x.name.toLowerCase().startsWith(query) && !x.aliasOf && !matched.has(x.name)) { - matched.set(x.name, { emoji: x, score: query.length + 1 }); - } - return matched.size === max; - }); - } - - // 前方一致(エイリアス込み) - if (matched.size < max) { - emojiDb.some(x => { - if (x.name.toLowerCase().startsWith(query) && !matched.has(x.aliasOf ?? x.name)) { - matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length }); - } - return matched.size === max; - }); - } - - // 部分一致(エイリアス込み) - if (matched.size < max) { - emojiDb.some(x => { - if (x.name.toLowerCase().includes(query) && !matched.has(x.aliasOf ?? x.name)) { - matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 }); - } - return matched.size === max; - }); - } - - // 簡易あいまい検索(3文字以上) - if (matched.size < max && query.length > 3) { - const queryChars = [...query]; - const hitEmojis = new Map(); - - for (const x of emojiDb) { - // 文字列の位置を進めながら、クエリの文字を順番に探す - - let pos = 0; - let hit = 0; - for (const c of queryChars) { - pos = x.name.indexOf(c, pos); - if (pos <= -1) break; - hit++; - } - - // 半分以上の文字が含まれていればヒットとする - if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) { - hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 }); - } - } - - // ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分) - [...hitEmojis.values()] - .sort((x, y) => y.score - x.score) - .slice(0, 6) - .forEach(it => matched.set(it.emoji.name, it)); - } - - return [...matched.values()] - .sort((x, y) => y.score - x.score) - .slice(0, max) - .map(it => it.emoji); -} diff --git a/packages/frontend/src/scripts/search-engine-map.ts b/packages/frontend/src/scripts/search-engine-map.ts deleted file mode 100644 index 03e5061597..0000000000 --- a/packages/frontend/src/scripts/search-engine-map.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: leah and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -//store the URL and if its none of these its a custom one -export const searchEngineMap = { - //The first one is the default search engine - 'https://www.google.com/search?q={query}': 'Google', - 'https://duckduckgo.com/?q={query}': 'Duckduckgo', - 'https://www.bing.com/search?q={query}': 'Bing', - 'https://search.yahoo.com/search?p={query}': 'Yahoo', - 'https://www.ecosia.org/search?q={query}': 'Ecosia', - 'https://www.qwant.com/?q={query}': 'Qwant', - 'https://search.aol.com/aol/search?q={query}': 'AOL', - 'https://yandex.com/search?text={query}': 'Yandex', -}; diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index 9aa38178b2..674c762fac 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -1,12 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; @@ -66,7 +65,7 @@ export function chooseFileFromUrl(): Promise { } }); - misskeyApi('drive/files/upload-from-url', { + os.api('drive/files/upload-from-url', { url: url, folderId: defaultStore.state.uploadFolder, marker, @@ -93,15 +92,15 @@ function select(src: any, label: string | null, multiple: boolean): Promise chooseFileFromPc(multiple, keepOriginal.value).then(files => res(files)), }, { text: i18n.ts.fromDrive, - icon: 'ti ti-cloud', + icon: 'ph-cloud ph-bold ph-lg', action: () => chooseFileFromDrive(multiple).then(files => res(files)), }, { text: i18n.ts.fromUrl, - icon: 'ti ti-link', + icon: 'ph-link ph-bold ph-lg', action: () => chooseFileFromUrl().then(file => res([file])), }], src); }); diff --git a/packages/frontend/src/scripts/show-moved-dialog.ts b/packages/frontend/src/scripts/show-moved-dialog.ts index 35b3ef79d8..b4defbfe7d 100644 --- a/packages/frontend/src/scripts/show-moved-dialog.ts +++ b/packages/frontend/src/scripts/show-moved-dialog.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/show-suspended-dialog.ts b/packages/frontend/src/scripts/show-suspended-dialog.ts index 8b89dbb936..a2fd5db453 100644 --- a/packages/frontend/src/scripts/show-suspended-dialog.ts +++ b/packages/frontend/src/scripts/show-suspended-dialog.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/show-system-account-dialog.ts b/packages/frontend/src/scripts/show-system-account-dialog.ts deleted file mode 100644 index 3c28d901fc..0000000000 --- a/packages/frontend/src/scripts/show-system-account-dialog.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as os from '@/os.js'; -import { i18n } from '@/i18n.js'; - -export function showSystemAccountDialog(): Promise { - return os.alert({ - type: 'error', - title: i18n.ts.systemAccountTitle, - text: i18n.ts.systemAccountDescription, - }); -} diff --git a/packages/frontend/src/scripts/shuffle.ts b/packages/frontend/src/scripts/shuffle.ts index fed16bc71c..d9d5bb1037 100644 --- a/packages/frontend/src/scripts/shuffle.ts +++ b/packages/frontend/src/scripts/shuffle.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/snowfall-effect.ts b/packages/frontend/src/scripts/snowfall-effect.ts deleted file mode 100644 index d88bdb6660..0000000000 --- a/packages/frontend/src/scripts/snowfall-effect.ts +++ /dev/null @@ -1,490 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SnowfallEffect { - private VERTEX_SOURCE = `#version 300 es - in vec4 a_position; - in vec4 a_color; - in vec3 a_rotation; - in vec3 a_speed; - in float a_size; - out vec4 v_color; - out float v_rotation; - uniform float u_time; - uniform mat4 u_projection; - uniform vec3 u_worldSize; - uniform float u_gravity; - uniform float u_wind; - uniform float u_spin_factor; - uniform float u_turbulence; - - void main() { - v_color = a_color; - v_rotation = a_rotation.x + (u_time * u_spin_factor) * a_rotation.y; - - vec3 pos = a_position.xyz; - - pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x; - pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y; - - pos.x += sin(u_time * a_speed.z * u_turbulence) * a_rotation.z; - pos.z += cos(u_time * a_speed.z * u_turbulence) * a_rotation.z; - - gl_Position = u_projection * vec4(pos.xyz, a_position.w); - gl_PointSize = (a_size / gl_Position.w) * 100.0; - } - `; - - private FRAGMENT_SOURCE = `#version 300 es - precision highp float; - - in vec4 v_color; - in float v_rotation; - uniform sampler2D u_texture; - out vec4 out_color; - - void main() { - vec2 rotated = vec2( - cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5, - cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5 - ); - - vec4 snowflake = texture(u_texture, rotated); - - out_color = vec4(snowflake.rgb * v_color.xyz, snowflake.a * v_color.a); - } - `; - - private gl: WebGLRenderingContext; - private program: WebGLProgram; - private canvas: HTMLCanvasElement; - private buffers: Record; - private uniforms: Record; - private texture: WebGLTexture; - private camera: { - fov: number; - near: number; - far: number; - aspect: number; - z: number; - }; - private wind: { - current: number; - force: number; - target: number; - min: number; - max: number; - easing: number; - }; - private time: { - start: number; - previous: number; - } = { - start: 0, - previous: 0, - }; - private raf = 0; - - private density: number = 1 / 90; - private depth = 100; - private count = 1000; - private gravity = 100; - private speed: number = 1 / 10000; - private color: number[] = [1, 1, 1]; - private opacity = 1; - private size = 4; - private snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAErRJREFUeAHdmgnYlmPax5MShaxRKRElPmXJXpaSsRxDU0bTZ+kt65RloiRDltEMQsxYKmS+zzYjxCCamCzV2LchResMIxFRQ1G93+93Pdf5dL9v7zuf4/hm0fc/jt9znddy3/e1nNd53c/7vHXq/AtVWVnZA/bzkaQjoWG298DeMdvrmP6/EIOqC4fBsbAx7Arz4TaYBPXgWVDnO2jSBrB2T0IMIA9mCmmoE8aonPkR6WPZHlp9xSlfeyeBzq9bHBD5feEdUGfDXBgBqnde+a2wvw/dYdNctvZNAp1PnTaFttA6JgP7eVgBM0CNzgO9HNvy0AcYDda6SaDTdXOnz8X+IkZDugAGQmOYA+ob6Ah/MIOMDRPhJjgJ6uV7pXtWt81/50SnY/Wvwn4ZDHAvwJ9ATYcxyaqsnEnqZCyCPaE80BgYZXG/5A3VyyP/b08LHa11z9KmFUwA5eqruRBHYX1s8WSI1Xcbme8Mt8PWUCU+kF8XbFN+dtH+p06OD4IU8EjD/VOZ5bnezq0XHcHuC2oV7BDlkVIWq56uIX8UjAO31GRIMYW0Vo/xXtSXJyTuXVO6xk1qalRTmQ9AfqzEvog2XYpllnsd6Qr4unCPT7NtByu0uU7vuAaOoy1JuvfXpJdTvSX0gI1gCXwGZdFmEFxoQb7Wid8s7lNu+I8wuHGsTqz2zpQ9DAa5R6HC55A2gvCMXthvwi25bjx26H0M9/9f4Rnok9s0zulFlC2HzzP9cnld8nH/p7DVrbmuIfYs6JLz9U3/z+KGadDeCDsmwre7GyEifn/su8HVSsL2HeBn8CK8AW+B7u9R5yrPgyOjvSn5DWAaXAG2UU7CE9Ayt4k4sR1lX4LaLdd9gn2ftsL+Vtuh1Dp/elH1C8lvCdUj8kDK3gbP8XdhCnSC86rcsNSR9pQvhc/gVlB9bUfqoFNAy/mLrUROrpMwCtpBxBbTtLqkF4K6IF9rf57I9pnYekx5AS0P1VhopXso9pR5buC7+kewU86nFcB+BT4EXdIvNO73sRBubGTXLZtTtgp+DEb++bACdqBuJOlAaMMzLVM3whegNznQDtCb+pW5b8YY76euB5+7pxm0IbzCfS8m3Zf2q4T8/+4JNArXGoptpxz8LqDmQJq0Qnostt/sfIn5GygD4/Zeq7B7wljQO2yjB/QGj0Pjxz4wGdqXrkjXtCT/ISyDa6EPpHrSraFjvnecFpMoMx40Br3xSlD262rYObevddHTs2kYwWUG9uP5It/f1eU5Xw9btwoXPALbwYXcg+unG/KB3Rq8n9ddAOpn4Kr8BAaBcltcDo9D7Ouavig1o34x7F94xqPk74eLQH0MH8HvwS3SLPe9iheEG6f70KiuLpZv6sxG/Va5bFJOabaO7ucAvGEbeAH+AN1hV7iDOidQFz4A2oJb6D1YDhXZHkTqpL8EbqHDYRtwW20AsdIb8syl5N2e6dTAPB2mWYa+hE4Qk7I59iMwFZ70GlJlfyuTVfygs7Hyw7HbwI0w3Tak14BqEtdg7wVdIx8pZbtBUbrjZeA3vUPBANkU+sEehev8O4Db6QpwYm+D8II0KPKHwUFeQ3oLDIMN4WgID1yOPQ+MAXMhNAtju3ztmtuAypiAw7EXwo/Am+0NfUG5mknYc6GfGVIjsoFNuyuoh8COuDcd2LmwA9jWE8bB3Q7N4XrwWAz5XOXR+Tx4n6FgdHeB6sF/w2QwhlSXdXvl/jixx4NH8GW5LDzb7GrR4ES4F5QddB99CieAwStOAPegdUZ2B71F3AXbQSn3vJ1bYaYWrayh3NUPTcbYFExVW3CfXwlvgfoavMbnDAY9dxGo6dCt0LeaB54H4UydDEPA2R4PDlrFLB9XuNmTlO+Xr7X9ZNBr9J4+EN8AMcv6ButpMND9FM6EnTOHkLrSnvtzwbbq3vwMB2ow/qWFSC8ZC++ZQaldbquH2afQWbl8TdcvVtC6LtipifAuOKt6gA9Tzqgzb5R2gP1hX3DVtZVHVvdklY5DA5beIkVPuZn8LOgAnWEfeAaUkxCan/voBNkfF+U5cFu5z5XlxZU20OmZtgm1K45VO4naNCukrcBZVk/CD+E/YBjoYjXJY8Zg9DxsDrbbBHTRotxOrug4eBs+hHgWZtKzfHrdXHBi9gDvqzxFHNA5KVfyBCf0ExgB7nkXStLLEKkniNf0AzUs5+ublkVFKiC9FBZAvGxshT0NnN3zoSUYSJQPcjAvm0HmjcIPemNS96F6E36drFLwugx7EEzNZV/l9IjoEPkW4B7eFtYH9QKcBcfA/aCWgpPQOT+zMbb9fS3nDbYR2MdgV0S5aVlUhLs0w45IHi7sqnnGJ2E7CXqHWgZXgJ1y8KqpDUmfSLmSV5yB/XrpDqVP8ofmehNdOv7I0ShfP4yyJdl2a4SchI1gCXgkHgljYfvc1i3cs/SU1A9jQRpfri/b0Sal1RrtSj4ULyHprY5C6+6E1+EBULq0E+DK7A96iwqX0z4td8B3dCdob5gD3UB3j9fUcNuDKFOvgc+bZAZFf4Zgu/q/AGPMgfm+5ShPWay+k6I31BwAvVDRYL2cuqfUVTkfnTqvVFx5ai7/MXn3tp1UrtRkDWRsaAMjzaD08uJ1irz7+8ps/6ZYj90V3FKrQBkvmubULbN7vs7tZRyJV9w0ePLbQ4PcJspqXnkbhbgoGk/AVptZRxpB0hU7Mpc1x34cdgKPm1dzeTts9XPwlFAO5Au4BDbO7ZycO7J9A/Zh2b4A2+ucALefWpTrflDKVq4kHQBOoi9PO1qvsDeGd6AxXAJbQ5VxlFrW8EnDcJlTsOPcjElxL7WNy7AduC4f2+A/rSN/Hyg7YMBTxgqPUT3F2HAqtIb58GvQW86GqyG+ff4UWz0FBuH4UhaTal1vmAGfg98dfP4d4HPGwmwYAg+D2/J7uU0ap/YaolHZVbBj5d1DaSK8ADsmqiH2JIhgNRhbPZrbhSdZ5heVJGw7477VfYuaagMK2sM8iMloga1HXAt/AeWELgQnR/0Z7k3W6pe3xTn/JamTFPGnPMZSj6p90rA8YOziwHcnH/EgTovJlJ0LPSHkyrTKmZNJ+8KrYKBsCQeB0pWdBFNleieMgzjL44jejTK1CPSY0CiMdyOT09g6ni5O3Ceg51U4VNLaPSA3SDNEwwiKFdgHgANNrpjb7UVejYTYCuZ92DR42HYh8gfDJfAMqBi4dqxk+RrKGkD0YXNsA6AT5qCUXhBe5CR0gPCC4dhqKFwI1m1qX0hr94CotDE4aAd3PCyBX4Jyn+sNL5tBDsRAp3S7b5KVYwa2A0nHaO5AXBeDtnlMxizsW+HomLh8zX9R5sTeBSEn/cqc2Tvak9eDXCyP2PgbYWzn2gefHxT7+0Qu/h18DO7XmPWYcYqSXuHz2myb6G7RNs7meLgeMxXugbiPA3clQx0xtgNPGN819L7+oCzvm6zSx+EkI+Du3Pe0LbOd/jqc7dhG9Wib+mJ5jaJBuL8e4B5aAMpAomKlb8d+KZWUVnw+dgzKSdDtvKaLDyJ1ReZB7O0J2EV5Xwd8OsTJExNpu7Q1SJ8zgy7K93UCX4P4mr4udoyhPGDKygOP+tomIFarMw2d+cfgF2DnDVAGoBvzw33YTHgPDoXQ7Fx/Wy6YkdMrcrmrehO4Pz3WvP90cIVPgonwITg4973yu0XTZK0+ZQaQd+K816twVAwKO71ZRj9zeg7lcVzXHghpVN4n2G3BAHQ1NILx4MBjoppgLwL3Ww8IHZsf6vGk3O8fwx9heK7rhD0o2zdg75JtT6GzQQ8KzcZwElSr3M5J85ktYCzEG+Gx2NNzm/Cm5pSp+K2gfLrZbg3RcB2IQcZN1qPM3+l06SjbAltX/TiXe1wtg7+AdR+AcgIs7xUPw94XxuTrnOD4E1bEoe9Rptw+DWGOGeQi7JOs1SfKKfk+epcakPNxbI8uFVdem8vT6aJdq7jASYjOFPdQDP4Q6t+Em8HVutmbkbYH9Tv4LcQW+H6ujy9Wrtxc6A7vQnznb5TbHUPZ0mw7CeoaOBAegmfBIKw8WZzs34M/oNiPGPzB2KHdrVMUlD29VFLLpw2jMWmnaIbdDNxXur+dWgVumTMglI4zMgbUEV5LmjqW7XnRkDS9qhbu/xZlZ8LWuc3UfM22Of80aVcYDJ/lstdIWxXu0TGXm/TO19vveHWuOglUxOo6iMfyBe7JOEp01ech9puuuBCMA8pVcUUNUB5lqgMYwJyE1oXOGTh9v1gO6kmogKEwHtREMHYofz5zAl3lJ2AWqJfgfohJiKB8HWWfg54YA9Zr1fn5Xmm80SdvHhNwVmq2umF8vWxA+WRwwE9BPNhOulrq0nxz97j6Go6DF8HYcBfYyer6MwWuoINeDG6roq4iE97QCtsJuxWc2JrkCeKEbgX7waOgnLiavxdQEWfohtgRwCrygIoxoQv1K0FNgR7gAKPTB+dr5lAWMliqmbAb7AzbgCs42vYK21NmOiwHJ9atpdxqDlhdA75QdYJT4XUYDfbBiVRe5ySoZTAbBpeekp6T4lo5uFnBz0fpJ6P8E9SJufEdXHipdRA/mw2hzmvfhrfgfjCKPwJnwn2g3igldb4hNaD5a6/fz7eHVuAb2wPwPs+4DB7E/hTagd64BbgoC6Ab9IAfgn+OX0p/ppAaGxZjnw6+Ep8DK8Cj0IDrmHw3GaeN9EZ/AlxFfk1RuVGUYu8K00D9Fa6EvrAUVKzO29gXg9vC1VW3g540w0xBcU2hKJnz+FxYvTCXWaduK/StuTZlLcD6JjnfEvsb6A56m32z78q4FMGw1gA4lEa60WmwMeiSnsljIBSDmEOBE3RdfvggbMuMIbNhItgJtbyUpE9ddjA0Bid1sderXDaQ1OdPAO9zH6hDcpuG2Ml7SQfArHRx6Xpf3JTluySrsrIP6Seg9/iMqsEvF6YZoXIDeAZCRmpneAHEnnLQnaEuXATX53schR3n/e7YyuvOT1bpnyV107Io3xZ6QWs4EirAyXkEqqvK3xa9CQ0c5C5xQ+zN8kWjcr2xZxTsBHfmsipbP671ZmW3wHYA58DdEPobhtwVF2HfBE9H3pT8xjkdja3iiDK4PQBO8Dx4B9wiH8JKeANcKTUW9IITwKNMeYrcArfDhVDsb1pVyty26le5D97/zWzrzVUGXyVjI0WjHUgq4CjoAuGiRuuJkN7mSJX7cn+uaZNyfBBgDHZqXvqsU2cZ6aPwChgE/ap8M9wLbSH+0DKOaw18z8N12GPAyf4BfADbwBmwCbxAHY9NvxQXx2GgVLZXPvurZDE0rqk5+NmAm8U2aIbdH9yDalgpSS80ltlB29fPqW9c8XLUHnsIuGquqt8gN7edwtazrOsAn4MysLryX8BD4Ap3y+0dZROIwPsl9h/hHjgit4lXdrdvHN8dc91wyk7JdvIS7VpF46Jb2ZGz4WJIRyBpBKQW3oR8lZuSvwQMhKtAfQUpYuf27cgbNx6EEeDAzgMHPwYMYi2gEcSfxC7B9qicDMoo/1vQI8p9IG88WAY/yeVpYrJdHpf5vytu4Ky7X46xIamrvjDb52OrG3K+HrZt4xq9wYEZPGPVfp7bhsdE2os2ylV6J1n5mbYPUX4S7AkGX+OAk2t6mm1Iw3PtQ+O4LuooK26RYvW3s7nBLZDiAGlbUHYiRV/S5AWk28DTEFqB4eo+B+n1M55Ivhu4kspj92uYCm6Px0Gv61lor0fcDQNBrQQnOr71lVeYsm894L/bkBuFe/u93eBngJtJMlwTDIDKyfDt6n3se8Dt8jHoNU0o70waq34obZ8lPx4coG+LbifrP6Pt0aQvwn65LFzcAHY8ZUtgAnwExp2WoMpeQLvaA12p7bf/pLPFmS3a/ajr750cfE43wX4YYmU9wi7IddHBCsrc69vm8uuwQydYVhQVvmsUn7s+ebfD0GhXrI+yf2jqA4oPKdo+iHxMwHbYRmgjta4cUTqCWXkg0UHatIR4SxxWKK9PeXhgKiZfxWOthzXuGff4p6b54bH3Y3W3pNxJcK8ebgdI44iys0G0N/8qKGOAGg9Ni50n3yjy2GkxSKtMRtT/21I7Fg/H9lRIX6qK5YX6zSjvDL4BGiBfBnUNmFdzwfKX4Ct40OtJv1sDj0Hlzrk6xbM3tob7uCf4amyk96VHvQg7gltGzQG9wpcwX6BCesfJ3/kJiMmgs+Gm4errUeZqF+Up4IoOzoWLcmqETyLve/2BsKkFpGUvK7VYCz6j06RbQx+ogHhN3Qdb3QF+a/wVKF94OhSHR77sWcXytcKm82usHGW9QE2B3skq/QB7APaqnJ9NuvaufnF1GIhxYH3LSAeA+hM0hMfgNzATdHvjgDHDv+qkP8gW77XW2gwmYsJe2F3zZDgxI7NteTo+/1WD/B9Au3Zjh2RyrgAAAABJRU5ErkJggg=='; - private mode = 'snow'; - - private INITIAL_BUFFERS = () => ({ - position: { size: 3, value: [] }, - color: { size: 4, value: [] }, - size: { size: 1, value: [] }, - rotation: { size: 3, value: [] }, - speed: { size: 3, value: [] }, - }); - - private INITIAL_UNIFORMS = () => ({ - time: { type: 'float', value: 0 }, - worldSize: { type: 'vec3', value: [0, 0, 0] }, - gravity: { type: 'float', value: this.gravity }, - wind: { type: 'float', value: 0 }, - spin_factor: { type: 'float', value: this.mode === 'sakura' ? 8 : 1 }, - turbulence: { type: 'float', value: this.mode === 'sakura' ? 2 : 1 }, - projection: { - type: 'mat4', - value: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], - }, - }); - - private UNIFORM_SETTERS = { - int: 'uniform1i', - float: 'uniform1f', - vec2: 'uniform2fv', - vec3: 'uniform3fv', - vec4: 'uniform4fv', - mat2: 'uniformMatrix2fv', - mat3: 'uniformMatrix3fv', - mat4: 'uniformMatrix4fv', - }; - - private CAMERA = { - fov: 60, - near: 5, - far: 10000, - aspect: 1, - z: 100, - }; - - private WIND = { - current: 0, - force: 0.01, - target: 0.01, - min: 0, - max: 0.125, - easing: 0.0005, - }; - /** - * @throws {Error} - Thrown when it fails to get WebGL context for the canvas - */ - constructor(options: { - sakura?: boolean; - }) { - if (options.sakura) { - this.mode = 'sakura'; - this.snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjQtMDItMDFUMTQ6Mzk6NTYrMDkwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAyNC0wMi0wMVQxNDozOTo1NiswOTAwIgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSI2NCIKICAgZXhpZjpQaXhlbFlEaW1lbnNpb249IjY0IgogICBleGlmOkNvbG9yU3BhY2U9IjEiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iNjQiCiAgIHRpZmY6SW1hZ2VMZW5ndGg9IjY0IgogICB0aWZmOlJlc29sdXRpb25Vbml0PSIyIgogICB0aWZmOlhSZXNvbHV0aW9uPSI3Mi8xIgogICB0aWZmOllSZXNvbHV0aW9uPSI3Mi8xIj4KICAgPHhtcE1NOkhpc3Rvcnk+CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0icHJvZHVjZWQiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFmZmluaXR5IFBob3RvIDIgMi4zLjEiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/PhldI30AAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWRu0sDQRCHP6Mh4oOIWlhYBPHRJBIjiDYWEV+gFjGCr+ZyuUuEJB53JyK2gq2gINr4KvQv0FawFgRFEcTaWtFG5ZwzgQQxs+zst7+dGXZnwRPPqFmrKgzZnG3GRqOB2bn5gO8FDxV46aJRUS1jcnokTln7uJdYsduQW6t83L9Wm9QsFSqqhQdVw7SFx4QnVm3D5R3hZjWtJIXPhIOmXFD4ztUTeX5xOZXnL5fNeGwIPA3CgVQJJ0pYTZtZYXk57dnMilq4j/uSOi03My1rm8xWLGKMEiXAOMMM0UcPA+L7CBGhW3aUyQ//5k+xLLmqeIM1TJZIkcYmKOqKVNdk1UXXZGRYc/v/t6+W3hvJV6+LgvfZcd46wLcN31uO83nkON/HUPkEl7li/vIh9L+LvlXU2g/AvwHnV0UtsQsXm9DyaCim8itVyvToOryeQv0cNN1AzUK+Z4VzTh4gvi5fdQ17+9Ap8f7FHyc6Z8kcDq1+AAAACXBIWXMAAAsTAAALEwEAmpwYAAADwElEQVR4nO2bT4hWVRjGf75TkhoEkhSa/9ocRIIwCsrE1pVnLbkYdFdGgQRS6caVm3CVy2oRuqmQ2yJXKTJh4GqCGs/CJCcLccAJ/yDpnGnxHYeZ4TrNfOc55y78nuWdc3/ve57v+b65f86BgQaqotiE5bEJKxYx7onYhOU1egKwGkViE/YCN4Cx2ITNC4xbDVwAJmMT9tXobVnpArEJe4CvZx0aB7aZdxPzxhkwArw66/Ae8+5Eyf6KJiA2YRPw+bzD64EjLcP3MXfyAMdjEzYWaG1GxRIQmzAEnAVeb/nzFPCSeTeaxj4FBOCZlrEjwBvm3VSJPksm4BPaJw8wBHwXm/BibMIW4HvaJ09ifFygP6BQAtKkfgEeEyHvAy+YdxdFvBmVSsBBdJMnsQ4KeTOSJyA2YT1wCXhcjL4HPG/e/amElkjAAfSTJzEPqKHSBKQLmSvAKiV3lm4BG8y7GyqgOgHvU27yAE+mGjLJEhCbsBL4A3haxXyIJoCN5t0dBUyZgF2UnzypxtsqmNKAt4SsarUkX4F0I3ONOgkAuA48a97FXJAqAa9Qb/IAa4CXFSCVATXjL635yBuQ/RsQm7AWuCroZamaBtaZd3/nQBQJeFPA6EfLFLUVBrwmYPSr7bkAhQHPCRj9al0uQGHAWgGjs9oKA7I/hS5rZ/0XSC86JDclGVph3t3t9+TcBHT56T9QVg+5BnT5/X+grB4GCcgs/sgnYCjzfIWyesg14Hrm+Qpl9ZBrwMT/DymurB4GCeiyuEidGnCN3n15V5pOPfStLAPMu1vAWA4jU7+Zd7dzAIqboREBo7PaCgN+EjA6qz1IQDbAu9/prQeorUvm3eVciOqx+JcizlL0hQKiMuAreiu/amkq1cyWxADz7ipwWsFapH4w7/5SgJRvh+cviCyp4yqQeonMOWCHktmic+bdThVMvUSmyFK2kjWkBph354FTSuY8nTLvflYCSyyT+xD4pwB3EvhADZUbYN5dAfarucB+825cDS25WvwksFuEO2nevSNizVHJ1eLvAoplrePAewJOq4oZYN5NAsPkPTCZBoYTq4iK7hgx734EjmUgjpl3Z1T9tKnGpqlP6e+p0Vg6t6iKG5De3A6ztJul+/Si3/db38WqyrY58+4CcHQJpxxN5xRXFQOSjgCjixg3SvuusiKqZoB59y+964KbCwy7Cew27+7V6apuAkibnhbaEbq3xMaohVTVAADz7hvgMHN/FKeAQ+bdt7X7Kb519mGKTdgKfEbvYucj8+7XLvr4DxAA134c0w/5AAAAAElFTkSuQmCC'; - this.size = 10; - this.density = 1 / 280; - } - - const canvas = this.initCanvas(); - const gl = canvas.getContext('webgl2', { antialias: true }); - if (gl == null) throw new Error('Failed to get WebGL context'); - - document.body.append(canvas); - - this.canvas = canvas; - this.gl = gl; - this.program = this.initProgram(); - this.buffers = this.initBuffers(); - this.uniforms = this.initUniforms(); - this.texture = this.initTexture(); - this.camera = this.initCamera(); - this.wind = this.initWind(); - - this.resize = this.resize.bind(this); - this.update = this.update.bind(this); - - window.addEventListener('resize', () => this.resize()); - } - - private initCanvas(): HTMLCanvasElement { - const canvas = document.createElement('canvas'); - - Object.assign(canvas.style, { - position: 'fixed', - top: 0, - left: 0, - width: '100vw', - height: '100vh', - background: 'transparent', - 'pointer-events': 'none', - 'z-index': 2147483647, - }); - - return canvas; - } - - private initCamera() { - return { ...this.CAMERA }; - } - - private initWind() { - return { ...this.WIND }; - } - - private initShader(type, source): WebGLShader { - const { gl } = this; - const shader = gl.createShader(type); - if (shader == null) throw new Error('Failed to create shader'); - - gl.shaderSource(shader, source); - gl.compileShader(shader); - - return shader; - } - - private initProgram(): WebGLProgram { - const { gl } = this; - const vertex = this.initShader(gl.VERTEX_SHADER, this.VERTEX_SOURCE); - const fragment = this.initShader(gl.FRAGMENT_SHADER, this.FRAGMENT_SOURCE); - const program = gl.createProgram(); - if (program == null) throw new Error('Failed to create program'); - - gl.attachShader(program, vertex); - gl.attachShader(program, fragment); - gl.linkProgram(program); - gl.useProgram(program); - - return program; - } - - private initBuffers(): SnowfallEffect['buffers'] { - const { gl, program } = this; - const buffers = this.INITIAL_BUFFERS() as unknown as SnowfallEffect['buffers']; - - for (const [name, buffer] of Object.entries(buffers)) { - buffer.location = gl.getAttribLocation(program, `a_${name}`); - buffer.ref = gl.createBuffer()!; - - gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref); - gl.enableVertexAttribArray(buffer.location); - gl.vertexAttribPointer( - buffer.location, - buffer.size, - gl.FLOAT, - false, - 0, - 0, - ); - } - - return buffers; - } - - private updateBuffers() { - const { buffers } = this; - - for (const name of Object.keys(buffers)) { - this.setBuffer(name); - } - } - - private setBuffer(name: string, value?) { - const { gl, buffers } = this; - const buffer = buffers[name]; - - buffer.value = new Float32Array(value ?? buffer.value); - - gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref); - gl.bufferData(gl.ARRAY_BUFFER, buffer.value, gl.STATIC_DRAW); - } - - private initUniforms(): SnowfallEffect['uniforms'] { - const { gl, program } = this; - const uniforms = this.INITIAL_UNIFORMS() as unknown as SnowfallEffect['uniforms']; - - for (const [name, uniform] of Object.entries(uniforms)) { - uniform.location = gl.getUniformLocation(program, `u_${name}`)!; - } - - return uniforms; - } - - private updateUniforms() { - const { uniforms } = this; - - for (const name of Object.keys(uniforms)) { - this.setUniform(name); - } - } - - private setUniform(name: string, value?) { - const { gl, uniforms } = this; - const uniform = uniforms[name]; - const setter = this.UNIFORM_SETTERS[uniform.type]; - const isMatrix = /^mat[2-4]$/i.test(uniform.type); - - uniform.value = value ?? uniform.value; - - if (isMatrix) { - gl[setter](uniform.location, false, uniform.value); - } else { - gl[setter](uniform.location, uniform.value); - } - } - - private initTexture() { - const { gl } = this; - const texture = gl.createTexture(); - if (texture == null) throw new Error('Failed to create texture'); - const image = new Image(); - - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - 1, - 1, - 0, - gl.RGBA, - gl.UNSIGNED_BYTE, - new Uint8Array([0, 0, 0, 0]), - ); - - image.onload = () => { - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - image, - ); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - }; - - image.src = this.snowflake; - - return texture; - } - - private initSnowflakes(vw: number, vh: number, dpi: number) { - const position: number[] = []; - const color: number[] = []; - const size: number[] = []; - const rotation: number[] = []; - const speed: number[] = []; - - const height = 1 / this.density; - const width = (vw / vh) * height; - const depth = this.depth; - const count = this.count; - const length = (vw / vh) * count; - - for (let i = 0; i < length; ++i) { - position.push( - -width + Math.random() * width * 2, - -height + Math.random() * height * 2, - Math.random() * depth * 2, - ); - - speed.push(1 + Math.random(), 1 + Math.random(), Math.random() * 10); - - rotation.push( - Math.random() * 2 * Math.PI, - Math.random() * 20, - Math.random() * 10, - ); - - color.push(...this.color, 0.1 + Math.random() * this.opacity); - //size.push((this.size * Math.random() * this.size * vh * dpi) / 1000); - size.push((this.size * vh * dpi) / 1000); - } - - this.setUniform('worldSize', [width, height, depth]); - - this.setBuffer('position', position); - this.setBuffer('color', color); - this.setBuffer('rotation', rotation); - this.setBuffer('size', size); - this.setBuffer('speed', speed); - } - - private setProjection(aspect: number) { - const { camera } = this; - - camera.aspect = aspect; - - const fovRad = (camera.fov * Math.PI) / 180; - const f = Math.tan(Math.PI * 0.5 - 0.5 * fovRad); - const rangeInv = 1.0 / (camera.near - camera.far); - - const m0 = f / camera.aspect; - const m5 = f; - const m10 = (camera.near + camera.far) * rangeInv; - const m11 = -1; - const m14 = camera.near * camera.far * rangeInv * 2 + camera.z; - const m15 = camera.z; - - return [m0, 0, 0, 0, 0, m5, 0, 0, 0, 0, m10, m11, 0, 0, m14, m15]; - } - - public render() { - const { gl } = this; - - gl.enable(gl.BLEND); - gl.enable(gl.CULL_FACE); - gl.blendFunc(gl.SRC_ALPHA, gl.ONE); - gl.disable(gl.DEPTH_TEST); - - this.updateBuffers(); - this.updateUniforms(); - this.resize(true); - - this.time = { - start: window.performance.now(), - previous: window.performance.now(), - }; - - if (this.raf) window.cancelAnimationFrame(this.raf); - this.raf = window.requestAnimationFrame(this.update); - - return this; - } - - private resize(updateSnowflakes = false) { - const { canvas, gl } = this; - const vw = canvas.offsetWidth; - const vh = canvas.offsetHeight; - const aspect = vw / vh; - const dpi = window.devicePixelRatio; - - canvas.width = vw * dpi; - canvas.height = vh * dpi; - - gl.viewport(0, 0, vw * dpi, vh * dpi); - gl.clearColor(0, 0, 0, 0); - - if (updateSnowflakes === true) { - this.initSnowflakes(vw, vh, dpi); - } - - this.setUniform('projection', this.setProjection(aspect)); - } - - private update(timestamp: number) { - const { gl, buffers, wind } = this; - const elapsed = (timestamp - this.time.start) * this.speed; - const delta = timestamp - this.time.previous; - - gl.clear(gl.COLOR_BUFFER_BIT); - gl.drawArrays( - gl.POINTS, - 0, - buffers.position.value.length / buffers.position.size, - ); - - if (Math.random() > 0.995) { - wind.target = - (wind.min + Math.random() * (wind.max - wind.min)) * - (Math.random() > 0.5 ? -1 : 1); - } - - wind.force += (wind.target - wind.force) * wind.easing; - wind.current += wind.force * (delta * 0.2); - - this.setUniform('wind', wind.current); - this.setUniform('time', elapsed); - - this.time.previous = timestamp; - - this.raf = window.requestAnimationFrame(this.update); - } -} diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 05f82fce7d..4b0cd0bb39 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -1,23 +1,15 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import type { SoundStore } from '@/store.js'; import { defaultStore } from '@/store.js'; -let ctx: AudioContext; +const ctx = new AudioContext(); const cache = new Map(); -let canPlay = true; export const soundsTypes = [ - // 音声なし null, - - // ドライブの音声 - '_driveFile_', - - // プリインストール 'syuilo/n-aec', 'syuilo/n-aec-4va', 'syuilo/n-aec-4vb', @@ -46,8 +38,6 @@ export const soundsTypes = [ 'syuilo/waon', 'syuilo/popo', 'syuilo/triple', - 'syuilo/bubble1', - 'syuilo/bubble2', 'syuilo/poi1', 'syuilo/poi2', 'syuilo/pirori', @@ -71,183 +61,46 @@ export const soundsTypes = [ 'noizenecio/kick_gaba7', ] as const; -export const operationTypes = [ - 'noteMy', - 'note', - 'notification', - 'reaction', -] as const; - -/** サウンドの種類 */ -export type SoundType = typeof soundsTypes[number]; - -/** スプライトの種類 */ -export type OperationType = typeof operationTypes[number]; - -/** - * 音声を読み込む - * @param url url - * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする - */ -export async function loadAudio(url: string, options?: { useCache?: boolean; }) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (ctx == null) { - ctx = new AudioContext(); - } - if (options?.useCache ?? true) { - if (cache.has(url)) { - return cache.get(url) as AudioBuffer; - } - } - - let response: Response; - - try { - response = await fetch(url); - } catch (err) { - return; +export async function getAudio(file: string, useCache = true) { + if (useCache && cache.has(file)) { + return cache.get(file)!; } + const response = await fetch(`/client-assets/sounds/${file}.mp3`); const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await ctx.decodeAudioData(arrayBuffer); - if (options?.useCache ?? true) { - cache.set(url, audioBuffer); + if (useCache) { + cache.set(file, audioBuffer); } return audioBuffer; } -/** - * 既定のスプライトを再生する - * @param type スプライトの種類を指定 - */ -export function playMisskeySfx(operationType: OperationType) { - const sound = defaultStore.state[`sound_${operationType}`]; - playMisskeySfxFile(sound).then((succeed) => { - if (!succeed && sound.type === '_driveFile_') { - // ドライブファイルが存在しない場合はデフォルトのサウンドを再生する - const soundName = defaultStore.def[`sound_${operationType}`].default.type as Exclude; - if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`); - playMisskeySfxFileInternal({ - type: soundName, - volume: sound.volume, - }); - } - }); -} - -/** - * サウンド設定形式で指定された音声を再生する - * @param soundStore サウンド設定 - */ -export async function playMisskeySfxFile(soundStore: SoundStore): Promise { - // 連続して再生しない - if (!canPlay) return false; - // ユーザーアクティベーションが必要な場合はそれがない場合は再生しない - if ('userActivation' in navigator && !navigator.userActivation.hasBeenActive) return false; - // サウンドがない場合は再生しない - if (soundStore.type === null || soundStore.type === '_driveFile_' && !soundStore.fileUrl) return false; - - canPlay = false; - return await playMisskeySfxFileInternal(soundStore).finally(() => { - // ごく短時間に音が重複しないように - setTimeout(() => { - canPlay = true; - }, 25); - }); -} - -async function playMisskeySfxFileInternal(soundStore: SoundStore): Promise { - if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { - return false; - } +export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement { const masterVolume = defaultStore.state.sound_masterVolume; - if (isMute() || masterVolume === 0 || soundStore.volume === 0) { - return true; // ミュート時は成功として扱う - } - const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`; - const buffer = await loadAudio(url).catch(() => { - return undefined; - }); - if (!buffer) return false; - const volume = soundStore.volume * masterVolume; - createSourceNode(buffer, { volume }).soundSource.start(); - return true; + audio.volume = masterVolume - ((1 - volume) * masterVolume); + return audio; } -export async function playUrl(url: string, opts: { - volume?: number; - pan?: number; - playbackRate?: number; -}) { - if (opts.volume === 0) { +export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') { + const sound = defaultStore.state[`sound_${type}`]; + if (_DEV_) console.log('play', type, sound); + if (sound.type == null) return; + playFile(sound.type, sound.volume); +} + +export async function playFile(file: string, volume: number) { + const masterVolume = defaultStore.state.sound_masterVolume; + if (masterVolume === 0 || volume === 0) { return; } - const buffer = await loadAudio(url); - if (!buffer) return; - createSourceNode(buffer, opts).soundSource.start(); -} - -export function createSourceNode(buffer: AudioBuffer, opts: { - volume?: number; - pan?: number; - playbackRate?: number; -}): { - soundSource: AudioBufferSourceNode; - panNode: StereoPannerNode; - gainNode: GainNode; -} { - const panNode = ctx.createStereoPanner(); - panNode.pan.value = opts.pan ?? 0; const gainNode = ctx.createGain(); - - gainNode.gain.value = opts.volume ?? 1; + gainNode.gain.value = masterVolume * volume; const soundSource = ctx.createBufferSource(); - soundSource.buffer = buffer; - soundSource.playbackRate.value = opts.playbackRate ?? 1; - soundSource - .connect(panNode) - .connect(gainNode) - .connect(ctx.destination); - - return { soundSource, panNode, gainNode }; -} - -/** - * 音声の長さをミリ秒で取得する - * @param file ファイルのURL(ドライブIDではない) - */ -export async function getSoundDuration(file: string): Promise { - const audioEl = document.createElement('audio'); - audioEl.src = file; - return new Promise((resolve) => { - const si = setInterval(() => { - if (audioEl.readyState > 0) { - resolve(audioEl.duration * 1000); - clearInterval(si); - audioEl.remove(); - } - }, 100); - }); -} - -/** - * ミュートすべきかどうかを判断する - */ -export function isMute(): boolean { - if (defaultStore.state.sound_notUseSound) { - // サウンドを出力しない - return true; - } - - // noinspection RedundantIfStatementJS - if (defaultStore.state.sound_useSoundOnlyWhenActive && document.visibilityState === 'hidden') { - // ブラウザがアクティブな時のみサウンドを出力する - return true; - } - - return false; + soundSource.buffer = await getAudio(file); + soundSource.connect(gainNode).connect(ctx.destination); + soundSource.start(); } diff --git a/packages/frontend/src/scripts/sticky-sidebar.ts b/packages/frontend/src/scripts/sticky-sidebar.ts index 50f1e6ecc8..f233c3648e 100644 --- a/packages/frontend/src/scripts/sticky-sidebar.ts +++ b/packages/frontend/src/scripts/sticky-sidebar.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts deleted file mode 100644 index cb0e607fcb..0000000000 --- a/packages/frontend/src/scripts/stream-mock.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { EventEmitter } from 'eventemitter3'; -import * as Misskey from 'misskey-js'; -import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js'; - -type AnyOf> = T[keyof T]; -type OmitFirst = T extends [any, ...infer R] ? R : never; - -/** - * Websocket無効化時に使うStreamのモック(なにもしない) - */ -export class StreamMock extends EventEmitter implements IStream { - public readonly state = 'initializing'; - - constructor(...args: ConstructorParameters) { - super(); - // do nothing - } - - public useChannel(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock { - return new ChannelConnectionMock(this, channel, name); - } - - public removeSharedConnection(connection: any): void { - // do nothing - } - - public removeSharedConnectionPool(pool: any): void { - // do nothing - } - - public disconnectToChannel(): void { - // do nothing - } - - public send(typeOrPayload: string): void - public send(typeOrPayload: string, payload: any): void - public send(typeOrPayload: Record | any[]): void - public send(typeOrPayload: string | Record | any[], payload?: any): void { - // do nothing - } - - public ping(): void { - // do nothing - } - - public heartbeat(): void { - // do nothing - } - - public close(): void { - // do nothing - } -} - -class ChannelConnectionMock = any> extends EventEmitter implements IChannelConnection { - public id = ''; - public name?: string; // for debug - public inCount = 0; // for debug - public outCount = 0; // for debug - public channel: string; - - constructor(stream: IStream, ...args: OmitFirst>>) { - super(); - - this.channel = args[0]; - this.name = args[1]; - } - - public send(type: T, body: Channel['receives'][T]): void { - // do nothing - } - - public dispose(): void { - // do nothing - } -} diff --git a/packages/frontend/src/scripts/test-utils.ts b/packages/frontend/src/scripts/test-utils.ts index 52bb2d94e0..1b42811faa 100644 --- a/packages/frontend/src/scripts/test-utils.ts +++ b/packages/frontend/src/scripts/test-utils.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/theme-editor.ts b/packages/frontend/src/scripts/theme-editor.ts index 0092af1640..275f4bcdaa 100644 --- a/packages/frontend/src/scripts/theme-editor.ts +++ b/packages/frontend/src/scripts/theme-editor.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index bd3cddde67..d2474bf10f 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -1,15 +1,14 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; -import lightTheme from '@@/themes/_light.json5'; -import darkTheme from '@@/themes/_dark.json5'; import { deepClone } from './clone.js'; -import type { BundledTheme } from 'shiki/themes'; import { globalEvents } from '@/events.js'; +import lightTheme from '@/themes/_light.json5'; +import darkTheme from '@/themes/_dark.json5'; import { miLocalStorage } from '@/local-storage.js'; export type Theme = { @@ -19,13 +18,6 @@ export type Theme = { desc?: string; base?: 'dark' | 'light'; props: Record; - codeHighlighter?: { - base: BundledTheme; - overrides?: Record; - } | { - base: '_none_'; - overrides: Record; - }; }; export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); @@ -50,11 +42,10 @@ export const getBuiltinThemes = () => Promise.all( 'd-green-lime', 'd-green-orange', 'd-cherry', + 'd-transfem-cherry', 'd-ice', 'd-u0', - 'rosepine', - 'rosepine-dawn', - ].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)), + ].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)), ); export const getBuiltinThemesRef = () => { @@ -63,9 +54,7 @@ export const getBuiltinThemesRef = () => { return builtinThemes; }; -const themeFontFaceName = 'sharkey-theme-font-face'; - -let timeout: number | null = null; +let timeout = null; export function applyTheme(theme: Theme, persist = true) { if (timeout) window.clearTimeout(timeout); @@ -78,8 +67,6 @@ export function applyTheme(theme: Theme, persist = true) { const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; - document.documentElement.dataset.colorScheme = colorScheme; - // Deep copy const _theme = deepClone(theme); @@ -97,32 +84,7 @@ export function applyTheme(theme: Theme, persist = true) { } } - let existingFontFace; - document.fonts.forEach( - (fontFace) => { - if (fontFace.family === themeFontFaceName) existingFontFace = fontFace; - }, - ); - if (existingFontFace) document.fonts.delete(existingFontFace); - - const fontFaceSrc = props.fontFaceSrc; - const fontFaceOpts = props.fontFaceOpts || {}; - - if (fontFaceSrc) { - const fontFace = new FontFace( - themeFontFaceName, - fontFaceSrc, fontFaceOpts, - ); - document.fonts.add(fontFace); - fontFace.load().catch( - (failure) => { - console.log(failure); - }, - ); - } - for (const [k, v] of Object.entries(props)) { - if (k.startsWith('font')) continue; document.documentElement.style.setProperty(`--${k}`, v.toString()); } @@ -166,10 +128,6 @@ function compile(theme: Theme): Record { for (const [k, v] of Object.entries(theme.props)) { if (k.startsWith('$')) continue; // ignore const - if (k.startsWith('font')) { // font specs are different - props[k] = v; - continue; - } props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v)); } diff --git a/packages/frontend/src/scripts/time.ts b/packages/frontend/src/scripts/time.ts index 275b67ed00..4479db1081 100644 --- a/packages/frontend/src/scripts/time.ts +++ b/packages/frontend/src/scripts/time.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/timezones.ts b/packages/frontend/src/scripts/timezones.ts index c7582e06da..55f9be393f 100644 --- a/packages/frontend/src/scripts/timezones.ts +++ b/packages/frontend/src/scripts/timezones.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/touch.ts b/packages/frontend/src/scripts/touch.ts index 13c9d648dc..05f379e4aa 100644 --- a/packages/frontend/src/scripts/touch.ts +++ b/packages/frontend/src/scripts/touch.ts @@ -1,9 +1,8 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { ref } from 'vue'; import { deviceKind } from '@/scripts/device-kind.js'; const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0; @@ -17,6 +16,3 @@ if (isTouchSupported && !isTouchUsing) { isTouchUsing = true; }, { passive: true }); } - -/** (MkHorizontalSwipe) 横スワイプ中か? */ -export const isHorizontalSwipeSwiping = ref(false); diff --git a/packages/frontend/src/scripts/unison-reload.ts b/packages/frontend/src/scripts/unison-reload.ts index a24941d02e..65fc090888 100644 --- a/packages/frontend/src/scripts/unison-reload.ts +++ b/packages/frontend/src/scripts/unison-reload.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index 22dce609c6..b896376ec8 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -1,19 +1,17 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { reactive, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { v4 as uuid } from 'uuid'; -import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; +import { readAndCompressImage } from 'browser-image-resizer'; import { getCompressionConfig } from './upload/compress-config.js'; import { defaultStore } from '@/store.js'; -import { apiUrl } from '@@/js/config.js'; +import { apiUrl } from '@/config.js'; import { $i } from '@/account.js'; import { alert } from '@/os.js'; import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; type Uploading = { id: string; @@ -40,26 +38,14 @@ export function uploadFile( if (folder && typeof folder === 'object') folder = folder.id; - if (file.size > instance.maxFileSize) { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, - }); - return Promise.reject(); - } - return new Promise((resolve, reject) => { - const id = uuid(); + const id = Math.random().toString(); const reader = new FileReader(); reader.onload = async (): Promise => { - const filename = name ?? file.name ?? 'untitled'; - const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : ''; - const ctx = reactive({ - id, - name: defaultStore.state.keepOriginalFilename ? filename : id + extension, + id: id, + name: name ?? file.name ?? 'untitled', progressMax: undefined, progressValue: undefined, img: window.URL.createObjectURL(file), diff --git a/packages/frontend/src/scripts/upload/compress-config.ts b/packages/frontend/src/scripts/upload/compress-config.ts index 3046b7f518..8fe64c8b76 100644 --- a/packages/frontend/src/scripts/upload/compress-config.ts +++ b/packages/frontend/src/scripts/upload/compress-config.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import isAnimated from 'is-file-animated'; -import { isWebpSupported } from './isWebpSupported.js'; -import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer'; +import { isWebpSupported } from './isWebpSupported'; +import type { BrowserImageResizerConfig } from 'browser-image-resizer'; const compressTypeMap = { 'image/jpeg': { quality: 0.90, mimeType: 'image/webp' }, @@ -21,7 +21,7 @@ const compressTypeMapFallback = { 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, } as const; -export async function getCompressionConfig(file: File): Promise { +export async function getCompressionConfig(file: File): Promise { const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type]; if (!imgConfig || await isAnimated(file)) { return; diff --git a/packages/frontend/src/scripts/upload/isWebpSupported.ts b/packages/frontend/src/scripts/upload/isWebpSupported.ts index 2511236ecc..185c3e6b40 100644 --- a/packages/frontend/src/scripts/upload/isWebpSupported.ts +++ b/packages/frontend/src/scripts/upload/isWebpSupported.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend-shared/js/url.ts b/packages/frontend/src/scripts/url.ts similarity index 51% rename from packages/frontend-shared/js/url.ts rename to packages/frontend/src/scripts/url.ts index eb830b1eea..625f4ce057 100644 --- a/packages/frontend-shared/js/url.ts +++ b/packages/frontend/src/scripts/url.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,21 +8,16 @@ * 2. プロパティがundefinedの時はクエリを付けない * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) */ -export function query(obj: Record): string { +export function query(obj: Record): string { const params = Object.entries(obj) - .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) // eslint-disable-line @typescript-eslint/no-unnecessary-condition - .reduce>((a, [k, v]) => (a[k] = v, a), {}); + .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) + .reduce((a, [k, v]) => (a[k] = v, a), {} as Record); return Object.entries(params) .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) .join('&'); } -export function appendQuery(url: string, queryString: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${queryString}`; -} - -export function extractDomain(url: string) { - const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im); - return match ? match[1] : null; +export function appendQuery(url: string, query: string): string { + return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; } diff --git a/packages/frontend/src/scripts/use-chart-tooltip.ts b/packages/frontend/src/scripts/use-chart-tooltip.ts index bba64fc6ee..daf915c7e3 100644 --- a/packages/frontend/src/scripts/use-chart-tooltip.ts +++ b/packages/frontend/src/scripts/use-chart-tooltip.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -11,22 +11,22 @@ export function useChartTooltip(opts: { position: 'top' | 'middle' } = { positio const tooltipShowing = ref(false); const tooltipX = ref(0); const tooltipY = ref(0); - const tooltipTitle = ref(null); - const tooltipSeries = ref<{ - backgroundColor: string; - borderColor: string; - text: string; - }[] | null>(null); - const { dispose: disposeTooltipComponent } = os.popup(MkChartTooltip, { + const tooltipTitle = ref(null); + const tooltipSeries = ref(null); + let disposeTooltipComponent; + + os.popup(MkChartTooltip, { showing: tooltipShowing, x: tooltipX, y: tooltipY, title: tooltipTitle, series: tooltipSeries, - }, {}); + }, {}).then(({ dispose }) => { + disposeTooltipComponent = dispose; + }); onUnmounted(() => { - disposeTooltipComponent(); + if (disposeTooltipComponent) disposeTooltipComponent(); }); onDeactivated(() => { @@ -49,11 +49,11 @@ export function useChartTooltip(opts: { position: 'top' | 'middle' } = { positio const rect = context.chart.canvas.getBoundingClientRect(); tooltipShowing.value = true; - tooltipX.value = rect.left + window.scrollX + context.tooltip.caretX; + tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; if (opts.position === 'top') { - tooltipY.value = rect.top + window.scrollY; + tooltipY.value = rect.top + window.pageYOffset; } else if (opts.position === 'middle') { - tooltipY.value = rect.top + window.scrollY + context.tooltip.caretY; + tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; } } diff --git a/packages/frontend-shared/js/use-document-visibility.ts b/packages/frontend/src/scripts/use-document-visibility.ts similarity index 76% rename from packages/frontend-shared/js/use-document-visibility.ts rename to packages/frontend/src/scripts/use-document-visibility.ts index b1197e68da..a9e2512eb3 100644 --- a/packages/frontend-shared/js/use-document-visibility.ts +++ b/packages/frontend/src/scripts/use-document-visibility.ts @@ -1,10 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { onMounted, onUnmounted, ref } from 'vue'; -import type { Ref } from 'vue'; +import { onMounted, onUnmounted, ref, Ref } from 'vue'; export function useDocumentVisibility(): Ref { const visibility = ref(document.visibilityState); diff --git a/packages/frontend/src/scripts/use-form.ts b/packages/frontend/src/scripts/use-form.ts deleted file mode 100644 index 0d505fe466..0000000000 --- a/packages/frontend/src/scripts/use-form.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { computed, Reactive, reactive, watch } from 'vue'; - -function copy(v: T): T { - return JSON.parse(JSON.stringify(v)); -} - -function unwrapReactive(v: Reactive): T { - return JSON.parse(JSON.stringify(v)); -} - -export function useForm>(initialState: T, save: (newState: T) => Promise) { - const currentState = reactive(copy(initialState)); - const previousState = reactive(copy(initialState)); - - const modifiedStates = reactive>({} as any); - for (const key in currentState) { - modifiedStates[key] = false; - } - const modified = computed(() => Object.values(modifiedStates).some(v => v)); - const modifiedCount = computed(() => Object.values(modifiedStates).filter(v => v).length); - - watch([currentState, previousState], () => { - for (const key in modifiedStates) { - modifiedStates[key] = currentState[key] !== previousState[key]; - } - }, { deep: true }); - - async function _save() { - await save(unwrapReactive(currentState)); - for (const key in currentState) { - previousState[key] = copy(currentState[key]); - } - } - - function discard() { - for (const key in currentState) { - currentState[key] = copy(previousState[key]); - } - } - - return { - state: currentState, - savedState: previousState, - modifiedStates, - modified, - modifiedCount, - save: _save, - discard, - }; -} diff --git a/packages/frontend-shared/js/use-interval.ts b/packages/frontend/src/scripts/use-interval.ts similarity index 68% rename from packages/frontend-shared/js/use-interval.ts rename to packages/frontend/src/scripts/use-interval.ts index b50e78c3cc..b8c5431fb6 100644 --- a/packages/frontend-shared/js/use-interval.ts +++ b/packages/frontend/src/scripts/use-interval.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'; +import { onMounted, onUnmounted } from 'vue'; export function useInterval(fn: () => void, interval: number, options: { immediate: boolean; @@ -28,16 +28,6 @@ export function useInterval(fn: () => void, interval: number, options: { intervalId = null; }; - onActivated(() => { - if (intervalId) return; - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - }); - - onDeactivated(() => { - clear(); - }); - onUnmounted(() => { clear(); }); diff --git a/packages/frontend/src/scripts/use-leave-guard.ts b/packages/frontend/src/scripts/use-leave-guard.ts index 5f7e56e8a9..c9750c3923 100644 --- a/packages/frontend/src/scripts/use-leave-guard.ts +++ b/packages/frontend/src/scripts/use-leave-guard.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index 89aa023f23..ab232598cd 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -1,48 +1,30 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -import { onUnmounted, Ref, ShallowRef } from 'vue'; +import { onUnmounted, Ref } from 'vue'; import * as Misskey from 'misskey-js'; import { useStream } from '@/stream.js'; import { $i } from '@/account.js'; import * as os from '@/os.js'; -import { misskeyApi } from './misskey-api.js'; export function useNoteCapture(props: { - rootEl: ShallowRef; + rootEl: Ref; note: Ref; - pureNote?: Ref; + pureNote: Ref; isDeletedRef: Ref; - onReplyCallback?: (replyNote: Misskey.entities.Note) => void | Promise; - onDeleteCallback?: (id: Misskey.entities.Note['id']) => void | Promise; }) { const note = props.note; const pureNote = props.pureNote !== undefined ? props.pureNote : props.note; const connection = $i ? useStream() : null; - async function onStreamNoteUpdated(noteData): Promise { + async function onStreamNoteUpdated(noteData): void { const { type, id, body } = noteData; if ((id !== note.value.id) && (id !== pureNote.value.id)) return; switch (type) { - case 'replied': { - if (!props.onReplyCallback) break; - - // notes/show may throw if the current user can't see the note - try { - const replyNote = await misskeyApi('notes/show', { - noteId: body.id, - }); - - await props.onReplyCallback(replyNote); - } catch { /* empty */ } - - break; - } - case 'reacted': { const reaction = body.reaction; @@ -54,7 +36,6 @@ export function useNoteCapture(props: { const currentCount = (note.value.reactions || {})[reaction] || 0; note.value.reactions[reaction] = currentCount + 1; - note.value.reactionCount += 1; if ($i && (body.userId === $i.id)) { note.value.myReaction = reaction; @@ -69,7 +50,6 @@ export function useNoteCapture(props: { const currentCount = (note.value.reactions || {})[reaction] || 0; note.value.reactions[reaction] = Math.max(0, currentCount - 1); - note.value.reactionCount = Math.max(0, note.value.reactionCount - 1); if (note.value.reactions[reaction] === 0) delete note.value.reactions[reaction]; if ($i && (body.userId === $i.id)) { @@ -81,7 +61,7 @@ export function useNoteCapture(props: { case 'pollVoted': { const choice = body.choice; - const choices = [...note.value.poll!.choices]; + const choices = [...note.value.poll.choices]; choices[choice] = { ...choices[choice], votes: choices[choice].votes + 1, @@ -90,32 +70,27 @@ export function useNoteCapture(props: { } : {}), }; - note.value.poll!.choices = choices; + note.value.poll.choices = choices; break; } case 'deleted': { props.isDeletedRef.value = true; - - if (props.onDeleteCallback) await props.onDeleteCallback(id); break; } case 'updated': { - try { - const editedNote = await misskeyApi('notes/show', { - noteId: id, - }); - - const keys = new Set(); - Object.keys(editedNote) - .concat(Object.keys(note.value)) - .forEach((key) => keys.add(key)); - keys.forEach((key) => { - note.value[key] = editedNote[key]; - }); - } catch { /* empty */ } + const editedNote = await os.api("notes/show", { + noteId: id, + }); + const keys = new Set(); + Object.keys(editedNote) + .concat(Object.keys(note.value)) + .forEach((key) => keys.add(key)); + keys.forEach((key) => { + note.value[key] = editedNote[key]; + }); break; } } @@ -124,7 +99,7 @@ export function useNoteCapture(props: { function capture(withHandler = false): void { if (connection) { // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する - connection.send(document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's', { id: note.value.id }); + connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: note.value.id }); if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id }); if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated); } diff --git a/packages/frontend/src/scripts/use-tooltip.ts b/packages/frontend/src/scripts/use-tooltip.ts index a26d08cce7..aaf0a0285a 100644 --- a/packages/frontend/src/scripts/use-tooltip.ts +++ b/packages/frontend/src/scripts/use-tooltip.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/warning-external-website.ts b/packages/frontend/src/scripts/warning-external-website.ts deleted file mode 100644 index 67158c6438..0000000000 --- a/packages/frontend/src/scripts/warning-external-website.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { instance } from '@/instance.js'; -import { defaultStore } from '@/store.js'; -import * as os from '@/os.js'; -import MkUrlWarningDialog from '@/components/MkUrlWarningDialog.vue'; - -const extractDomain = /^(https?:\/\/|\/\/)?([^@/\s]+@)?(www\.)?([^:/\s]+)/i; -const isRegExp = /^\/(.+)\/(.*)$/; - -export async function warningExternalWebsite(url: string) { - const domain = extractDomain.exec(url)?.[4]; - - if (!domain) return false; - - const isTrustedByInstance = instance.trustedLinkUrlPatterns.some(expression => { - const r = isRegExp.exec(expression); - - if (r) { - return new RegExp(r[1], r[2]).test(url); - } else if (expression.includes(' ')) { - return expression.split(' ').every(keyword => url.includes(keyword)); - } else { - return domain.endsWith(expression); - } - }); - - const isTrustedByUser = defaultStore.reactiveState.trustedDomains.value.includes(domain); - const isDisabledByUser = !defaultStore.reactiveState.warnExternalUrl.value; - - if (!isTrustedByInstance && !isTrustedByUser && !isDisabledByUser) { - const confirm = await new Promise<{ canceled: boolean }>(resolve => { - const { dispose } = os.popup(MkUrlWarningDialog, { - url, - }, { - done: result => { - resolve(result ?? { canceled: true }); - }, - closed: () => dispose(), - }); - }); - - if (confirm.canceled) return false; - - return window.open(url, '_blank', 'nofollow noopener popup=false'); - } - - return window.open(url, '_blank', 'nofollow noopener popup=false'); -} diff --git a/packages/frontend-shared/js/worker-multi-dispatch.ts b/packages/frontend/src/scripts/worker-multi-dispatch.ts similarity index 82% rename from packages/frontend-shared/js/worker-multi-dispatch.ts rename to packages/frontend/src/scripts/worker-multi-dispatch.ts index 5d393ed1ed..7686b687c5 100644 --- a/packages/frontend-shared/js/worker-multi-dispatch.ts +++ b/packages/frontend/src/scripts/worker-multi-dispatch.ts @@ -1,20 +1,18 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -function defaultUseWorkerNumber(prev: number) { +function defaultUseWorkerNumber(prev: number, totalWorkers: number) { return prev + 1; } -type WorkerNumberGetter = (prev: number, totalWorkers: number) => number; - -export class WorkerMultiDispatch { +export class WorkerMultiDispatch { private symbol = Symbol('WorkerMultiDispatch'); private workers: Worker[] = []; private terminated = false; private prevWorkerNumber = 0; - private getUseWorkerNumber: WorkerNumberGetter; + private getUseWorkerNumber = defaultUseWorkerNumber; private finalizationRegistry: FinalizationRegistry; constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) { @@ -31,7 +29,7 @@ export class WorkerMultiDispatch { if (_DEV_) console.log('WorkerMultiDispatch: Created', this); } - public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: WorkerNumberGetter = this.getUseWorkerNumber) { + public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) { let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length); workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length; if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber); @@ -48,14 +46,12 @@ export class WorkerMultiDispatch { return workerNumber; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any public addListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) { this.workers.forEach(worker => { worker.addEventListener('message', callback, options); }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any public removeListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) { this.workers.forEach(worker => { worker.removeEventListener('message', callback, options); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index bbd9873ad8..7844ef0fb5 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -1,17 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { markRaw, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { hemisphere } from '@@/js/intl-const.js'; -import lightTheme from '@@/themes/l-cherry.json5'; -import darkTheme from '@@/themes/d-ice.json5'; import { miLocalStorage } from './local-storage.js'; -import { searchEngineMap } from './scripts/search-engine-map.js'; -import type { SoundType } from '@/scripts/sound.js'; -import { defaultFollowingFeedState } from '@/scripts/following-feed-utils.js'; import { Storage } from '@/pizzax.js'; interface PostFormAction { @@ -41,22 +35,6 @@ interface PageViewInterruptor { handler: (page: Misskey.entities.Page) => unknown; } -/** サウンド設定 */ -export type SoundStore = { - type: Exclude; - volume: number; -} | { - type: '_driveFile_'; - - /** ドライブのファイルID */ - fileId: string; - - /** ファイルURL(こちらが優先される) */ - fileUrl: string; - - volume: number; -} - export const postFormActions: PostFormAction[] = []; export const userActions: UserAction[] = []; export const noteActions: NoteAction[] = []; @@ -92,29 +70,17 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: false, }, - collapseNotesRepliedTo: { - where: 'account', - default: false, - }, collapseFiles: { where: 'account', default: false, }, - uncollapseCW: { - where: 'account', - default: false, - }, - expandLongNote: { - where: 'device', - default: false, - }, rememberNoteVisibility: { where: 'account', default: false, }, defaultNoteVisibility: { where: 'account', - default: 'public' as (typeof Misskey.noteVisibilities)[number], + default: 'public', }, defaultNoteLocalOnly: { where: 'account', @@ -140,10 +106,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], }, - pinnedEmojis: { - where: 'account', - default: [], - }, reactionAcceptance: { where: 'account', default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null, @@ -160,22 +122,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: true, }, - showVisibilitySelectorOnBoost: { - where: 'account', - default: true, - }, - visibilityOnBoost: { - where: 'account', - default: 'public' as 'public' | 'home' | 'followers', - }, - trustedDomains: { - where: 'account', - default: [] as string[], - }, - warnExternalUrl: { - where: 'account', - default: true, - }, menu: { where: 'deviceAccount', @@ -194,7 +140,7 @@ export const defaultStore = markRaw(new Storage('base', { }, visibility: { where: 'deviceAccount', - default: 'public' as (typeof Misskey.noteVisibilities)[number], + default: 'public' as 'public' | 'home' | 'followers' | 'specified', }, localOnly: { where: 'deviceAccount', @@ -227,25 +173,14 @@ export const defaultStore = markRaw(new Storage('base', { tl: { where: 'deviceAccount', default: { - src: 'home' as 'home' | 'local' | 'social' | 'global' | 'bubble' | `list:${string}`, + src: 'home' as 'home' | 'local' | 'social' | 'global' | `list:${string}`, userList: null as Misskey.entities.UserList | null, - filter: { - withReplies: true, - withRenotes: true, - withBots: true, - withSensitive: true, - onlyFiles: false, - }, }, }, pinnedUserLists: { where: 'deviceAccount', default: [] as Misskey.entities.UserList[], }, - followingFeed: { - where: 'account', - default: defaultFollowingFeedState, - }, overridedDeviceKind: { where: 'device', @@ -275,45 +210,29 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, - showReactionsCount: { - where: 'device', - default: false, - }, - enableQuickAddMfmFunction: { - where: 'device', - default: false, - }, loadRawImages: { where: 'device', default: false, }, - warnMissingAltText: { - where: 'device', - default: true, - }, - enableFaviconNotificationDot: { - where: 'device', - default: true, - }, imageNewTab: { where: 'device', default: false, }, + enableDataSaverMode: { + where: 'device', + default: false, + }, disableShowingAnimatedImages: { where: 'device', default: window.matchMedia('(prefers-reduced-motion)').matches, }, - disableCatSpeak: { - where: 'account', - default: false, - }, emojiStyle: { where: 'device', default: 'twemoji', // twemoji / fluentEmoji / native }, - menuStyle: { + disableDrawer: { where: 'device', - default: 'auto' as 'auto' | 'popup' | 'drawer', + default: false, }, useBlurEffectForModal: { where: 'device', @@ -331,18 +250,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - showTickerOnReplies: { - where: 'device', - default: false, - }, - searchEngine: { - where: 'account', - default: Object.keys(searchEngineMap)[0], - }, - noteDesign: { - where: 'device', - default: 'sharkey' as 'sharkey' | 'misskey', - }, enableInfiniteScroll: { where: 'device', default: true, @@ -363,21 +270,21 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'remote' as 'none' | 'remote' | 'always', }, - emojiPickerScale: { + reactionPickerSize: { where: 'device', default: 1, }, - emojiPickerWidth: { + reactionPickerWidth: { where: 'device', default: 1, }, - emojiPickerHeight: { + reactionPickerHeight: { where: 'device', default: 2, }, - emojiPickerStyle: { + reactionPickerUseDrawerForMobile: { where: 'device', - default: 'auto' as 'auto' | 'popup' | 'drawer', + default: true, }, recentlyUsedEmojis: { where: 'device', @@ -423,10 +330,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 3, }, - numberOfReplies: { - where: 'device', - default: 5, - }, showNoteActionsOnlyHover: { where: 'device', default: false, @@ -439,18 +342,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'medium' as 'small' | 'medium' | 'large', }, - limitWidthOfReaction: { - where: 'device', - default: true, - }, forceShowAds: { where: 'device', default: false, }, - oneko: { - where: 'device', - default: false, - }, clickToOpen: { where: 'device', default: true, @@ -475,14 +370,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'horizontal' as 'vertical' | 'horizontal', }, - notificationClickable: { + enableCondensedLineForAcct: { where: 'device', default: false, }, - enableCondensedLine: { - where: 'device', - default: true, - }, additionalUnicodeEmojiIndexes: { where: 'device', default: {} as Record>, @@ -491,6 +382,14 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + tlWithReplies: { + where: 'device', + default: false, + }, + tlWithBots: { + where: 'device', + default: true, + }, defaultWithReplies: { where: 'account', default: false, @@ -503,82 +402,30 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, - dataSaver: { - where: 'device', - default: { - media: false, - avatar: false, - urlPreview: false, - code: false, - } as Record, - }, - enableSeasonalScreenEffect: { - where: 'device', - default: false, - }, - dropAndFusion: { - where: 'device', - default: { - bgmVolume: 0.25, - sfxVolume: 1, - }, - }, - hemisphere: { - where: 'device', - default: hemisphere as 'N' | 'S', - }, - enableHorizontalSwipe: { - where: 'device', - default: true, - }, - useNativeUIForVideoAudioPlayer: { - where: 'device', - default: false, - }, - keepOriginalFilename: { - where: 'device', - default: true, - }, - alwaysConfirmFollow: { - where: 'device', - default: true, - }, - confirmWhenRevealingSensitiveMedia: { - where: 'device', - default: false, - }, - contextMenu: { - where: 'device', - default: 'app' as 'app' | 'appWithShift' | 'native', - }, sound_masterVolume: { where: 'device', default: 0.3, }, - sound_notUseSound: { - where: 'device', - default: false, - }, - sound_useSoundOnlyWhenActive: { - where: 'device', - default: false, - }, sound_note: { where: 'device', - default: { type: 'syuilo/n-aec', volume: 0 } as SoundStore, + default: { type: 'syuilo/n-aec', volume: 0 }, }, sound_noteMy: { where: 'device', - default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore, + default: { type: 'syuilo/n-cea-4va', volume: 1 }, }, sound_notification: { where: 'device', - default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore, + default: { type: 'syuilo/n-ea', volume: 1 }, }, - sound_reaction: { + sound_antenna: { where: 'device', - default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore, + default: { type: 'syuilo/triple', volume: 1 }, + }, + sound_channel: { + where: 'device', + default: { type: 'syuilo/square-pico', volume: 1 }, }, })); @@ -609,6 +456,8 @@ interface Watcher { /** * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ) */ +import lightTheme from '@/themes/l-cherry.json5'; +import darkTheme from '@/themes/d-transfem-cherry.json5'; export class ColdDeviceStorage { public static default = { @@ -645,7 +494,7 @@ export class ColdDeviceStorage { public static set(key: T, value: typeof ColdDeviceStorage.default[T]): void { // 呼び出し側のバグ等で undefined が来ることがある // undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 - + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (value === undefined) { console.error(`attempt to store undefined value for key '${key}'`); return; diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index e63dac951c..5f0826b4e3 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -1,42 +1,23 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import * as Misskey from 'misskey-js'; import { markRaw } from 'vue'; import { $i } from '@/account.js'; -import { wsOrigin } from '@@/js/config.js'; -// TODO: No WebsocketモードでStreamMockが使えそう -//import { StreamMock } from '@/scripts/stream-mock.js'; +import { wsOrigin } from '@/config.js'; -// heart beat interval in ms -const HEART_BEAT_INTERVAL = 1000 * 60; +let stream: Misskey.Stream | null = null; -let stream: Misskey.IStream | null = null; -let timeoutHeartBeat: number | null = null; -let lastHeartbeatCall = 0; - -export function useStream(): Misskey.IStream { +export function useStream(): Misskey.Stream { if (stream) return stream; - // TODO: No Websocketモードもここで判定 stream = markRaw(new Misskey.Stream(wsOrigin, $i ? { token: $i.token, } : null)); - if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat); - timeoutHeartBeat = window.setTimeout(heartbeat, HEART_BEAT_INTERVAL); - - // send heartbeat right now when last send time is over HEART_BEAT_INTERVAL - document.addEventListener('visibilitychange', () => { - if ( - !stream - || document.visibilityState !== 'visible' - || Date.now() - lastHeartbeatCall < HEART_BEAT_INTERVAL - ) return; - heartbeat(); - }); + window.setTimeout(heartbeat, 1000 * 60); return stream; } @@ -45,7 +26,5 @@ function heartbeat(): void { if (stream != null && document.visibilityState === 'visible') { stream.heartbeat(); } - lastHeartbeatCall = Date.now(); - if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat); - timeoutHeartBeat = window.setTimeout(heartbeat, HEART_BEAT_INTERVAL); + window.setTimeout(heartbeat, 1000 * 60); } diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index d990a706b3..8c531fc2cc 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -9,7 +9,7 @@ } /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -33,14 +33,11 @@ --minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px))); --minBottomSpacing: var(--minBottomSpacingMobile); - //--ad: rgb(255 169 0 / 10%); - @media (max-width: 500px) { --margin: var(--marginHalf); } - --avatar: 48px; - --thread-width: 2px; + //--ad: rgb(255 169 0 / 10%); } html.radius-misskey { @@ -65,7 +62,7 @@ html { accent-color: var(--accent); overflow: auto; overflow-wrap: break-word; - font-family: 'sharkey-theme-font-face', 'Lexend', 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; + font-family: 'Lexend', 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; font-size: 14px; line-height: 1.35; text-size-adjust: 100%; @@ -136,10 +133,6 @@ a { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; - &:focus-visible { - outline-offset: 2px; - } - &:hover { text-decoration: underline; } @@ -166,25 +159,12 @@ hr { background: var(--divider); } -rt { - white-space: initial; -} - -:focus-visible { - outline: var(--focus) solid 2px; - outline-offset: -2px; - - &:hover { - text-decoration: none; - } -} - .ph-bold { width: 1.28em; vertical-align: -12%; line-height: 1em; - &::before { + &:before { font-size: 128%; } } @@ -266,8 +246,8 @@ rt { line-height: inherit; max-width: 100%; - &:hover { - text-decoration: none; + &:focus-visible { + outline: none; } &:disabled { @@ -282,11 +262,11 @@ rt { background: var(--accent); &:not(:disabled):hover { - background: hsl(from var(--accent) h s calc(l + 5)); + background: var(--X8); } &:not(:disabled):active { - background: hsl(from var(--accent) h s calc(l - 5)); + background: var(--X9); } } @@ -296,27 +276,23 @@ rt { background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + background: linear-gradient(90deg, var(--X8), var(--X8)); } &:not(:disabled):active { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + background: linear-gradient(90deg, var(--X8), var(--X8)); } } ._help { color: var(--accent); - cursor: help; + cursor: help } ._textButton { @extend ._button; color: var(--accent); - &:focus-visible { - outline-offset: 2px; - } - &:not(:disabled):hover { text-decoration: underline; } @@ -405,16 +381,6 @@ rt { vertical-align: top; } -._modified { - margin-left: 0.7em; - font-size: 65%; - padding: 2px 3px; - color: var(--warn); - border: solid 1px var(--warn); - border-radius: 4px; - vertical-align: top; -} - ._table { > ._row { display: flex; @@ -474,40 +440,6 @@ rt { transition-timing-function: cubic-bezier(0,.5,.5,1); } -._woodenFrame { - padding: 7px; - background: #8C4F26; - box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c; - border-radius: 10px; - - --bg: #F1E8DC; - --fg: #693410; -} - -html[data-color-scheme=dark] ._woodenFrame { - --bg: #1d0c02; - --fg: #F1E8DC; - --panel: #192320; -} - -._woodenFrameH { - display: flex; - gap: 6px; -} - -._woodenFrameInner { - padding: 8px; - margin-top: 8px; - background: var(--bg); - box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410; - border-radius: 6px; - color: var(--fg); - - &:first-child { - margin-top: 0; - } -} - ._transition_zoom-enter-active, ._transition_zoom-leave-active { transition: opacity 0.5s, transform 0.5s !important; } @@ -516,13 +448,13 @@ html[data-color-scheme=dark] ._woodenFrame { transform: scale(0.9); } -@keyframes global-blink { +@keyframes blink { 0% { opacity: 1; transform: scale(1); } 30% { opacity: 1; transform: scale(1); } 90% { opacity: 0; transform: scale(0.5); } } -@keyframes global-tada { +@keyframes tada { from { transform: scale3d(1, 1, 1); } @@ -552,7 +484,7 @@ html[data-color-scheme=dark] ._woodenFrame { ._anime_bounce { will-change: transform; - animation: global-bounce ease 0.7s; + animation: bounce ease 0.7s; animation-iteration-count: 1; transform-origin: 50% 50%; } @@ -564,7 +496,7 @@ html[data-color-scheme=dark] ._woodenFrame { transition: transform 0.1s ease; } -@keyframes global-bounce { +@keyframes bounce { 0% { transform: scaleX(0.90) scaleY(0.90) ; } @@ -581,8 +513,6 @@ html[data-color-scheme=dark] ._woodenFrame { // MFM ----------------------------- -bdi.block { display: block } - ._mfm_blur_ { filter: blur(6px); transition: filter 0.3s; @@ -724,12 +654,3 @@ bdi.block { display: block } 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; - } -} diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts index c41cc17652..f37c01cca1 100644 --- a/packages/frontend/src/theme-store.ts +++ b/packages/frontend/src/theme-store.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Theme, getBuiltinThemes } from '@/scripts/theme.js'; import { miLocalStorage } from '@/local-storage.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { api } from '@/os.js'; import { $i } from '@/account.js'; const lsCacheKey = $i ? `themes:${$i.id}` as const : null; @@ -19,7 +19,7 @@ export async function fetchThemes(): Promise { if ($i == null) return; try { - const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }); + const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' }); miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); } catch (err) { if (err.code === 'NO_SUCH_KEY') return; @@ -35,13 +35,13 @@ export async function addTheme(theme: Theme): Promise { } await fetchThemes(); const themes = getThemes().concat(theme); - await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); + await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); } export async function removeTheme(theme: Theme): Promise { if ($i == null) return; const themes = getThemes().filter(t => t.id !== theme.id); - await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); + await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes)); } diff --git a/packages/frontend-shared/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5 similarity index 89% rename from packages/frontend-shared/themes/_dark.json5 rename to packages/frontend/src/themes/_dark.json5 index e4649311c3..3f5822977a 100644 --- a/packages/frontend-shared/themes/_dark.json5 +++ b/packages/frontend/src/themes/_dark.json5 @@ -13,7 +13,6 @@ accentDarken: ':darken<10<@accent', accentLighten: ':lighten<10<@accent', accentedBg: ':alpha<0.15<@accent', - love: '#dd2e44', focus: ':alpha<0.3<@accent', bg: '#000', acrylicBg: ':alpha<0.5<@bg', @@ -31,7 +30,6 @@ panelHeaderFg: '@fg', panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelBorder: '" solid 1px var(--divider)', - thread: ':lighten<12<@panel', acrylicPanel: ':alpha<0.5<@panel', windowHeader: ':alpha<0.85<@panel', popup: ':lighten<3<@panel', @@ -55,13 +53,11 @@ infoFg: '#fff', infoWarnBg: '#42321c', infoWarnFg: '#ffbd3e', - folderHeaderBg: 'rgba(255, 255, 255, 0.05)', - folderHeaderHoverBg: 'rgba(255, 255, 255, 0.1)', - buttonBg: ':lighten<5<@panel', - buttonHoverBg: ':lighten<10<@panel', + switchBg: 'rgba(255, 255, 255, 0.15)', + buttonBg: 'rgba(255, 255, 255, 0.05)', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', buttonGradateA: '@accent', buttonGradateB: ':hue<20<@accent', - switchBg: 'rgba(255, 255, 255, 0.15)', switchOffBg: 'rgba(255, 255, 255, 0.1)', switchOffFg: ':alpha<0.8<@fg', switchOnBg: '@accentedBg', @@ -81,17 +77,21 @@ codeBoolean: '#c59eff', deckBg: '#000', htmlThemeColor: '@bg', + X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.3)', X12: 'rgba(255, 255, 255, 0.1)', X13: 'rgba(255, 255, 255, 0.15)', - }, - - codeHighlighter: { - base: 'one-dark-pro', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', }, } diff --git a/packages/frontend-shared/themes/_light.json5 b/packages/frontend/src/themes/_light.json5 similarity index 89% rename from packages/frontend-shared/themes/_light.json5 rename to packages/frontend/src/themes/_light.json5 index b6218a5f1d..6ebfcaafeb 100644 --- a/packages/frontend-shared/themes/_light.json5 +++ b/packages/frontend/src/themes/_light.json5 @@ -13,7 +13,6 @@ accentDarken: ':darken<10<@accent', accentLighten: ':lighten<10<@accent', accentedBg: ':alpha<0.15<@accent', - love: '#dd2e44', focus: ':alpha<0.3<@accent', bg: '#fff', acrylicBg: ':alpha<0.5<@bg', @@ -31,7 +30,6 @@ panelHeaderFg: '@fg', panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelBorder: '" solid 1px var(--divider)', - thread: ':darken<12<@panel', acrylicPanel: ':alpha<0.5<@panel', windowHeader: ':alpha<0.85<@panel', popup: ':lighten<3<@panel', @@ -55,13 +53,11 @@ infoFg: '#72818a', infoWarnBg: '#fff0db', infoWarnFg: '#8f6e31', - folderHeaderBg: 'rgba(0, 0, 0, 0.05)', - folderHeaderHoverBg: 'rgba(0, 0, 0, 0.1)', - buttonBg: ':darken<5<@panel', - buttonHoverBg: ':darken<10<@panel', + switchBg: 'rgba(0, 0, 0, 0.15)', + buttonBg: 'rgba(0, 0, 0, 0.05)', + buttonHoverBg: 'rgba(0, 0, 0, 0.1)', buttonGradateA: '@accent', buttonGradateB: ':hue<20<@accent', - switchBg: 'rgba(0, 0, 0, 0.15)', switchOffBg: 'rgba(0, 0, 0, 0.1)', switchOffFg: '@panel', switchOnBg: '@accent', @@ -81,17 +77,21 @@ codeBoolean: '#62b70c', deckBg: ':darken<3<@bg', htmlThemeColor: '@bg', + X2: ':darken<2<@panel', X3: 'rgba(0, 0, 0, 0.05)', X4: 'rgba(0, 0, 0, 0.1)', X5: 'rgba(0, 0, 0, 0.05)', X6: 'rgba(0, 0, 0, 0.25)', X7: 'rgba(0, 0, 0, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.1)', X12: 'rgba(0, 0, 0, 0.1)', X13: 'rgba(0, 0, 0, 0.15)', - }, - - codeHighlighter: { - base: 'catppuccin-latte', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', }, } diff --git a/packages/frontend-shared/themes/d-astro.json5 b/packages/frontend/src/themes/d-astro.json5 similarity index 87% rename from packages/frontend-shared/themes/d-astro.json5 rename to packages/frontend/src/themes/d-astro.json5 index a674a5c5c9..fee25cc4a4 100644 --- a/packages/frontend-shared/themes/d-astro.json5 +++ b/packages/frontend/src/themes/d-astro.json5 @@ -25,6 +25,7 @@ mention: '#ffd152', modalBg: 'rgba(0, 0, 0, 0.5)', success: '#86b300', + buttonBg: 'rgba(255, 255, 255, 0.05)', acrylicBg: ':alpha<0.5<@bg', indicator: '@accent', mentionMe: '#fb5d38', @@ -41,6 +42,7 @@ acrylicPanel: ':alpha<0.5<@panel', navIndicator: '@accent', accentLighten: ':lighten<10<@accent', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', buttonGradateA: '@accent', buttonGradateB: ':hue<-20<@accent', driveFolderBg: ':alpha<0.3<@accent', @@ -55,13 +57,20 @@ wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', panelHeaderDivider: 'rgba(0, 0, 0, 0)', scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', + X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.3)', X12: 'rgba(255, 255, 255, 0.1)', X13: 'rgba(255, 255, 255, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', }, } diff --git a/packages/frontend-shared/themes/d-botanical.json5 b/packages/frontend/src/themes/d-botanical.json5 similarity index 100% rename from packages/frontend-shared/themes/d-botanical.json5 rename to packages/frontend/src/themes/d-botanical.json5 diff --git a/packages/frontend-shared/themes/d-cherry.json5 b/packages/frontend/src/themes/d-cherry.json5 similarity index 100% rename from packages/frontend-shared/themes/d-cherry.json5 rename to packages/frontend/src/themes/d-cherry.json5 diff --git a/packages/frontend-shared/themes/d-dark.json5 b/packages/frontend/src/themes/d-dark.json5 similarity index 100% rename from packages/frontend-shared/themes/d-dark.json5 rename to packages/frontend/src/themes/d-dark.json5 diff --git a/packages/frontend-shared/themes/d-future.json5 b/packages/frontend/src/themes/d-future.json5 similarity index 100% rename from packages/frontend-shared/themes/d-future.json5 rename to packages/frontend/src/themes/d-future.json5 diff --git a/packages/frontend-shared/themes/d-green-lime.json5 b/packages/frontend/src/themes/d-green-lime.json5 similarity index 100% rename from packages/frontend-shared/themes/d-green-lime.json5 rename to packages/frontend/src/themes/d-green-lime.json5 diff --git a/packages/frontend-shared/themes/d-green-orange.json5 b/packages/frontend/src/themes/d-green-orange.json5 similarity index 100% rename from packages/frontend-shared/themes/d-green-orange.json5 rename to packages/frontend/src/themes/d-green-orange.json5 diff --git a/packages/frontend-shared/themes/d-ice.json5 b/packages/frontend/src/themes/d-ice.json5 similarity index 100% rename from packages/frontend-shared/themes/d-ice.json5 rename to packages/frontend/src/themes/d-ice.json5 diff --git a/packages/frontend-shared/themes/d-persimmon.json5 b/packages/frontend/src/themes/d-persimmon.json5 similarity index 100% rename from packages/frontend-shared/themes/d-persimmon.json5 rename to packages/frontend/src/themes/d-persimmon.json5 diff --git a/packages/frontend/src/themes/d-transfem-cherry.json5 b/packages/frontend/src/themes/d-transfem-cherry.json5 new file mode 100644 index 0000000000..a6fdcec77b --- /dev/null +++ b/packages/frontend/src/themes/d-transfem-cherry.json5 @@ -0,0 +1,20 @@ +{ + id: '679b3b87-a4e9-4289-8693-b56c15cc33b5', + + name: 'Transfem Mi Cherry Dark', + author: 'Amelia and syuilo', + + base: 'dark', + + props: { + accent: 'rgb(245, 169, 184)', + bg: 'rgb(28, 28, 37)', + fg: 'rgb(236, 239, 244)', + panel: 'rgb(35, 35, 47)', + renote: '@accent', + link: '@accent', + mention: '@accent', + hashtag: '@accent', + divider: 'rgb(63, 63, 80)', + }, +} diff --git a/packages/frontend-shared/themes/d-u0.json5 b/packages/frontend/src/themes/d-u0.json5 similarity index 93% rename from packages/frontend-shared/themes/d-u0.json5 rename to packages/frontend/src/themes/d-u0.json5 index 32ac9ec5cf..3bd0b9483c 100644 --- a/packages/frontend-shared/themes/d-u0.json5 +++ b/packages/frontend/src/themes/d-u0.json5 @@ -3,11 +3,14 @@ base: 'dark', name: 'Mi U0 Dark', props: { + X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', bg: '#172426', fg: '#dadada', X10: ':alpha<0.4<@accent', @@ -38,6 +41,7 @@ mention: '@accent', modalBg: 'rgba(0, 0, 0, 0.5)', success: '#86b300', + buttonBg: 'rgba(255, 255, 255, 0.05)', switchBg: 'rgba(255, 255, 255, 0.15)', acrylicBg: ':alpha<0.5<@bg', indicator: '@accent', @@ -60,6 +64,7 @@ acrylicPanel: ':alpha<0.5<@panel', navIndicator: '@indicator', accentLighten: ':lighten<10<@accent', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':lighten<3<@fg', fgTransparent: ':alpha<0.5<@fg', diff --git a/packages/frontend-shared/themes/l-apricot.json5 b/packages/frontend/src/themes/l-apricot.json5 similarity index 100% rename from packages/frontend-shared/themes/l-apricot.json5 rename to packages/frontend/src/themes/l-apricot.json5 diff --git a/packages/frontend-shared/themes/l-botanical.json5 b/packages/frontend/src/themes/l-botanical.json5 similarity index 100% rename from packages/frontend-shared/themes/l-botanical.json5 rename to packages/frontend/src/themes/l-botanical.json5 diff --git a/packages/frontend-shared/themes/l-cherry.json5 b/packages/frontend/src/themes/l-cherry.json5 similarity index 100% rename from packages/frontend-shared/themes/l-cherry.json5 rename to packages/frontend/src/themes/l-cherry.json5 diff --git a/packages/frontend-shared/themes/l-coffee.json5 b/packages/frontend/src/themes/l-coffee.json5 similarity index 100% rename from packages/frontend-shared/themes/l-coffee.json5 rename to packages/frontend/src/themes/l-coffee.json5 diff --git a/packages/frontend-shared/themes/l-light.json5 b/packages/frontend/src/themes/l-light.json5 similarity index 100% rename from packages/frontend-shared/themes/l-light.json5 rename to packages/frontend/src/themes/l-light.json5 diff --git a/packages/frontend-shared/themes/l-rainy.json5 b/packages/frontend/src/themes/l-rainy.json5 similarity index 100% rename from packages/frontend-shared/themes/l-rainy.json5 rename to packages/frontend/src/themes/l-rainy.json5 diff --git a/packages/frontend-shared/themes/l-sushi.json5 b/packages/frontend/src/themes/l-sushi.json5 similarity index 92% rename from packages/frontend-shared/themes/l-sushi.json5 rename to packages/frontend/src/themes/l-sushi.json5 index f1523b698c..e787d63734 100644 --- a/packages/frontend-shared/themes/l-sushi.json5 +++ b/packages/frontend/src/themes/l-sushi.json5 @@ -14,6 +14,6 @@ renote: '@accent', link: '@accent', mention: '@accent', - hashtag: '@accent', + hashtag: '#229e82', }, } diff --git a/packages/frontend-shared/themes/l-u0.json5 b/packages/frontend/src/themes/l-u0.json5 similarity index 96% rename from packages/frontend-shared/themes/l-u0.json5 rename to packages/frontend/src/themes/l-u0.json5 index 0b952b003a..dbc777d493 100644 --- a/packages/frontend-shared/themes/l-u0.json5 +++ b/packages/frontend/src/themes/l-u0.json5 @@ -3,11 +3,14 @@ base: 'light', name: 'Mi U0 Light', props: { + X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', bg: '#e7e7eb', fg: '#5f5f5f', X10: ':alpha<0.4<@accent', diff --git a/packages/frontend-shared/themes/l-vivid.json5 b/packages/frontend/src/themes/l-vivid.json5 similarity index 86% rename from packages/frontend-shared/themes/l-vivid.json5 rename to packages/frontend/src/themes/l-vivid.json5 index f1c63dde6e..3368855b5e 100644 --- a/packages/frontend-shared/themes/l-vivid.json5 +++ b/packages/frontend/src/themes/l-vivid.json5 @@ -28,6 +28,7 @@ mention: '@accent', modalBg: 'rgba(0, 0, 0, 0.3)', success: '#86b300', + buttonBg: 'rgba(0, 0, 0, 0.05)', acrylicBg: ':alpha<0.5<@bg', indicator: '@accent', mentionMe: '@mention', @@ -44,6 +45,7 @@ acrylicPanel: ':alpha<0.5<@panel', navIndicator: '@accent', accentLighten: ':lighten<10<@accent', + buttonHoverBg: 'rgba(0, 0, 0, 0.1)', driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':darken<3<@fg', fgTransparent: ':alpha<0.5<@fg', @@ -58,13 +60,21 @@ fgTransparentWeak: ':alpha<0.75<@fg', panelHeaderDivider: '@divider', scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', + X2: ':darken<2<@panel', X3: 'rgba(0, 0, 0, 0.05)', X4: 'rgba(0, 0, 0, 0.1)', X5: 'rgba(0, 0, 0, 0.05)', X6: 'rgba(0, 0, 0, 0.25)', X7: 'rgba(0, 0, 0, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.1)', X12: 'rgba(0, 0, 0, 0.1)', X13: 'rgba(0, 0, 0, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', }, } diff --git a/packages/frontend/src/timelines.ts b/packages/frontend/src/timelines.ts deleted file mode 100644 index 5080ef4b96..0000000000 --- a/packages/frontend/src/timelines.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { $i } from '@/account.js'; -import { instance } from '@/instance.js'; - -export const basicTimelineTypes = [ - 'home', - 'local', - 'social', - 'bubble', - 'global', -] as const; - -export type BasicTimelineType = typeof basicTimelineTypes[number]; - -export function isBasicTimeline(timeline: string): timeline is BasicTimelineType { - return basicTimelineTypes.includes(timeline as BasicTimelineType); -} - -export function basicTimelineIconClass(timeline: BasicTimelineType): string { - switch (timeline) { - case 'home': - return 'ti ti-home'; - case 'local': - return 'ti ti-planet'; - case 'social': - return 'ti ti-universe'; - case 'bubble': - return 'ph-drop ph-bold ph-lg'; - case 'global': - return 'ti ti-whirl'; - } -} - -export function isAvailableBasicTimeline(timeline: BasicTimelineType | undefined | null): boolean { - switch (timeline) { - case 'home': - return $i != null; - case 'local': - return ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); - case 'social': - return $i != null && $i.policies.ltlAvailable; - case 'bubble': - return ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable); - case 'global': - return ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); - default: - return false; - } -} - -export function availableBasicTimelines(): BasicTimelineType[] { - return basicTimelineTypes.filter(isAvailableBasicTimeline); -} - -export function hasWithReplies(timeline: BasicTimelineType | undefined | null): boolean { - return timeline === 'local' || timeline === 'social'; -} diff --git a/packages/frontend/src/type.ts b/packages/frontend/src/type.ts deleted file mode 100644 index 5ff27158d2..0000000000 --- a/packages/frontend/src/type.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type WithRequired = T & { [P in K]-?: T[P] }; - -export type WithNonNullable = T & { [P in K]-?: NonNullable }; diff --git a/packages/frontend/src/types/date-separated-list.ts b/packages/frontend/src/types/date-separated-list.ts index af685cff12..678193ca98 100644 --- a/packages/frontend/src/types/date-separated-list.ts +++ b/packages/frontend/src/types/date-separated-list.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts index 138eb7dd62..66061fcd70 100644 --- a/packages/frontend/src/types/menu.ts +++ b/packages/frontend/src/types/menu.ts @@ -1,30 +1,26 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import * as Misskey from 'misskey-js'; -import { ComputedRef, Ref } from 'vue'; - -interface MenuRadioOptionsDef extends Record { } +import { Ref } from 'vue'; export type MenuAction = (ev: MouseEvent) => void; -export type MenuDivider = { type: 'divider' }; +export type MenuDivider = null; export type MenuNull = undefined; export type MenuLabel = { type: 'label', text: string }; export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean }; export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; -export type MenuSwitch = { type: 'switch', ref: Ref, text: string, icon?: string, disabled?: boolean | Ref }; -export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef, avatar?: Misskey.entities.User; action: MenuAction }; -export type MenuRadio = { type: 'radio', text: string, icon?: string, ref: Ref, options: MenuRadioOptionsDef, disabled?: boolean | Ref }; -export type MenuRadioOption = { type: 'radioOption', text: string, action: MenuAction; active?: boolean | ComputedRef }; +export type MenuSwitch = { type: 'switch', ref: Ref, text: string, disabled?: boolean }; +export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction }; export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise | MenuItem[]) }; export type MenuPending = { type: 'pending' }; -type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuParent; +type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent; type OuterPromiseMenuItem = Promise; export type MenuItem = OuterMenuItem | OuterPromiseMenuItem; -export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuParent; +export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent; diff --git a/packages/frontend/src/types/page-header.ts b/packages/frontend/src/types/page-header.ts deleted file mode 100644 index e9807a2939..0000000000 --- a/packages/frontend/src/types/page-header.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type PageHeaderItem = { - text: string; - icon: string; - highlighted?: boolean; - handler: (ev: MouseEvent) => void; -}; diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue index 374bc20b54..913fa35cc2 100644 --- a/packages/frontend/src/ui/_common_/announcements.vue +++ b/packages/frontend/src/ui/_common_/announcements.vue @@ -1,5 +1,5 @@ @@ -9,13 +9,13 @@ SPDX-License-Identifier: AGPL-3.0-only v-for="announcement in $i.unreadAnnouncements.filter(x => x.display === 'banner')" :key="announcement.id" :class="$style.item" - :to="`/announcements/${announcement.id}`" + to="/announcements" > - - - - + + + + {{ announcement.title }} {{ announcement.text }} diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index 8355ae3061..9baf5861d7 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,7 @@ import { defineAsyncComponent } from 'vue'; import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; -import { host } from '@@/js/config.js'; +import { host } from '@/config.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; @@ -16,17 +16,17 @@ function toolsMenuItems(): MenuItem[] { type: 'link', to: '/scratchpad', text: i18n.ts.scratchpad, - icon: 'ti ti-terminal-2', + icon: 'ph-terminal-window ph-bold ph-lg-2', }, { type: 'link', to: '/api-console', text: 'API Console', - icon: 'ti ti-terminal-2', + icon: 'ph-terminal-window ph-bold ph-lg-2', }, { type: 'link', to: '/clicker', text: '🍪👈', - icon: 'ti ti-cookie', + icon: 'ph-cookie ph-bold ph-lg', }, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? { type: 'link', to: '/custom-emojis-manager', @@ -34,22 +34,20 @@ function toolsMenuItems(): MenuItem[] { icon: 'ph-smiley ph-bold ph-lg', } : undefined, ($i && ($i.isAdmin || $i.policies.canManageAvatarDecorations)) ? { type: 'link', - to: '/avatar-decorations', + to: '/admin/avatar-decorations', text: i18n.ts.manageAvatarDecorations, - icon: 'ti ti-sparkles', + icon: 'ph-sparkle ph-bold ph-lg', } : undefined]; } export function openInstanceMenu(ev: MouseEvent) { - const menuItems: MenuItem[] = []; - - menuItems.push({ + os.popupMenu([{ text: instance.name ?? host, type: 'label', }, { type: 'link', text: i18n.ts.instanceInfo, - icon: 'ti ti-info-circle', + icon: 'ph-info ph-bold ph-lg', to: '/about', }, { type: 'link', @@ -59,113 +57,63 @@ export function openInstanceMenu(ev: MouseEvent) { }, { type: 'link', text: i18n.ts.federation, - icon: 'ti ti-whirl', + icon: 'ph-globe-hemisphere-west ph-bold ph-lg', to: '/about#federation', }, { type: 'link', text: i18n.ts.charts, - icon: 'ti ti-chart-line', + icon: 'ph-chart-line ph-bold ph-lg', to: '/about#charts', - }, { type: 'divider' }, { + }, null, { type: 'link', text: i18n.ts.ads, - icon: 'ti ti-ad', + icon: 'ph-flag ph-bold ph-lg', to: '/ads', - }); - - if ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) { - menuItems.push({ - type: 'link', - to: '/invite', - text: i18n.ts.invite, - icon: 'ti ti-user-plus', - }); - } - - menuItems.push({ + }, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? { + type: 'link', + to: '/invite', + text: i18n.ts.invite, + icon: 'ph-user-plus ph-bold ph-lg', + } : undefined, { type: 'parent', text: i18n.ts.tools, - icon: 'ti ti-tool', + icon: 'ph-toolbox ph-bold ph-lg', children: toolsMenuItems(), - }, { type: 'divider' }, { - type: 'link', - text: i18n.ts.inquiry, - icon: 'ti ti-help-circle', - to: '/contact', - }); - - if (instance.impressumUrl) { - menuItems.push({ - type: 'a', - text: i18n.ts.impressum, - icon: 'ti ti-file-invoice', - href: instance.impressumUrl, - target: '_blank', - }); - } - - if (instance.tosUrl) { - menuItems.push({ - type: 'a', - text: i18n.ts.termsOfService, - icon: 'ti ti-notebook', - href: instance.tosUrl, - target: '_blank', - }); - } - - if (instance.privacyPolicyUrl) { - menuItems.push({ - type: 'a', - text: i18n.ts.privacyPolicy, - icon: 'ti ti-shield-lock', - href: instance.privacyPolicyUrl, - target: '_blank', - }); - } - - if (instance.donationUrl) { - menuItems.push({ - type: 'a', - text: i18n.ts.donation, - icon: 'ph-hand-coins ph-bold ph-lg', - href: instance.donationUrl, - target: '_blank', - }); - } - - if (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) { - menuItems.push({ type: 'divider' }); - } - - menuItems.push({ - type: 'a', - text: i18n.ts.document, - icon: 'ti ti-bulb', - href: 'https://misskey-hub.net/docs/for-users/', - target: '_blank', - }); - - if ($i) { - menuItems.push({ - text: i18n.ts._initialTutorial.launchTutorial, - icon: 'ti ti-presentation', - action: () => { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, { - closed: () => dispose(), - }); - }, - }); - } - - menuItems.push({ + }, null, (instance.impressumUrl) ? { + text: i18n.ts.impressum, + icon: 'ph-newspaper-clipping ph-bold ph-lg', + action: () => { + window.open(instance.impressumUrl, '_blank'); + }, + } : undefined, (instance.tosUrl) ? { + text: i18n.ts.termsOfService, + icon: 'ph-notebook ph-bold ph-lg', + action: () => { + window.open(instance.tosUrl, '_blank'); + }, + } : undefined, (instance.privacyPolicyUrl) ? { + text: i18n.ts.privacyPolicy, + icon: 'ph-shield ph-bold ph-lg', + action: () => { + window.open(instance.privacyPolicyUrl, '_blank'); + }, + } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, { + text: i18n.ts.help, + icon: 'ph-question ph-bold ph-lg', + action: () => { + window.open('https://misskey-hub.net/help.html', '_blank'); + }, + }, ($i) ? { + text: i18n.ts._initialTutorial.launchTutorial, + icon: 'ph-presentation ph-bold ph-lg', + action: () => { + os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {}, 'closed'); + }, + } : undefined, { type: 'link', text: i18n.ts.aboutMisskey, - icon: 'sk-icons sk-shark sk-icons-lg', to: '/about-sharkey', - }); - - os.popupMenu(menuItems, ev.currentTarget ?? ev.target, { + }], ev.currentTarget ?? ev.target, { align: 'left', }); } diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index a8ff2a4c8d..7f8556d8d2 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -1,5 +1,5 @@ @@ -30,11 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only :enterFromClass="defaultStore.state.animation ? $style.transition_notification_enterFrom : ''" :leaveToClass="defaultStore.state.animation ? $style.transition_notification_leaveTo : ''" > -
+
@@ -43,36 +39,31 @@ SPDX-License-Identifier: AGPL-3.0-only
-
DEV BUILD
+
DEV BUILD
{{ i18n.ts.loggedInAsBot }}
- - diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index f3244b5697..346e5976ba 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -1,5 +1,5 @@ @@ -8,12 +8,12 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.timeline }} + {{ i18n.ts.timeline }}
- {{ i18n.ts.controlPanel }} + {{ i18n.ts.controlPanel }} - {{ i18n.ts.settings }} + {{ i18n.ts.settings }}