th: Merge remote-tracking branch 'glitch/main'
This commit is contained in:
commit
9d32bdbcde
548 changed files with 12947 additions and 3951 deletions
|
@ -4,10 +4,6 @@ FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye
|
||||||
# Install Rails
|
# Install Rails
|
||||||
# RUN gem install rails webdrivers
|
# RUN gem install rails webdrivers
|
||||||
|
|
||||||
# Default value to allow debug server to serve content over GitHub Codespace's port forwarding service
|
|
||||||
# The value is a comma-separated list of allowed domains
|
|
||||||
ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev"
|
|
||||||
|
|
||||||
ARG NODE_VERSION="16"
|
ARG NODE_VERSION="16"
|
||||||
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
|
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
|
||||||
|
|
||||||
|
|
49
.devcontainer/codespaces/devcontainer.json
Normal file
49
.devcontainer/codespaces/devcontainer.json
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"name": "Mastodon on GitHub Codespaces",
|
||||||
|
"dockerComposeFile": "../docker-compose.yml",
|
||||||
|
"service": "app",
|
||||||
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
|
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/sshd:1": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
"runServices": ["app", "db", "redis"],
|
||||||
|
|
||||||
|
"forwardPorts": [3000, 4000],
|
||||||
|
|
||||||
|
"portsAttributes": {
|
||||||
|
"3000": {
|
||||||
|
"label": "web",
|
||||||
|
"onAutoForward": "notify"
|
||||||
|
},
|
||||||
|
"4000": {
|
||||||
|
"label": "stream",
|
||||||
|
"onAutoForward": "silent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"otherPortsAttributes": {
|
||||||
|
"onAutoForward": "silent"
|
||||||
|
},
|
||||||
|
|
||||||
|
"remoteEnv": {
|
||||||
|
"LOCAL_DOMAIN": "${localEnv:CODESPACE_NAME}-3000.app.github.dev",
|
||||||
|
"LOCAL_HTTPS": "true",
|
||||||
|
"STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev",
|
||||||
|
"DISABLE_FORGERY_REQUEST_PROTECTION": "true",
|
||||||
|
"ES_ENABLED": "",
|
||||||
|
"LIBRE_TRANSLATE_ENDPOINT": ""
|
||||||
|
},
|
||||||
|
|
||||||
|
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
|
||||||
|
"postCreateCommand": ".devcontainer/post-create.sh",
|
||||||
|
"waitFor": "postCreateCommand",
|
||||||
|
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"settings": {},
|
||||||
|
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "Mastodon",
|
"name": "Mastodon on local machine",
|
||||||
"dockerComposeFile": "docker-compose.yml",
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
"service": "app",
|
"service": "app",
|
||||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
|
@ -8,13 +8,23 @@
|
||||||
"ghcr.io/devcontainers/features/sshd:1": {}
|
"ghcr.io/devcontainers/features/sshd:1": {}
|
||||||
},
|
},
|
||||||
|
|
||||||
"runServices": ["app", "db", "redis"],
|
|
||||||
|
|
||||||
"forwardPorts": [3000, 4000],
|
"forwardPorts": [3000, 4000],
|
||||||
|
|
||||||
"containerEnv": {
|
"portsAttributes": {
|
||||||
"ES_ENABLED": "",
|
"3000": {
|
||||||
"LIBRE_TRANSLATE_ENDPOINT": ""
|
"label": "web",
|
||||||
|
"onAutoForward": "notify",
|
||||||
|
"requireLocalPort": true
|
||||||
|
},
|
||||||
|
"4000": {
|
||||||
|
"label": "stream",
|
||||||
|
"onAutoForward": "silent",
|
||||||
|
"requireLocalPort": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"otherPortsAttributes": {
|
||||||
|
"onAutoForward": "silent"
|
||||||
},
|
},
|
||||||
|
|
||||||
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
|
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
|
||||||
|
|
|
@ -25,6 +25,7 @@ services:
|
||||||
command: sleep infinity
|
command: sleep infinity
|
||||||
ports:
|
ports:
|
||||||
- '127.0.0.1:3000:3000'
|
- '127.0.0.1:3000:3000'
|
||||||
|
- '127.0.0.1:3035:3035'
|
||||||
- '127.0.0.1:4000:4000'
|
- '127.0.0.1:4000:4000'
|
||||||
networks:
|
networks:
|
||||||
- external_network
|
- external_network
|
||||||
|
|
|
@ -4,6 +4,6 @@ ALTERNATE_DOMAINS=mastodon.internal
|
||||||
DB_HOST=$PWD/data/postgres
|
DB_HOST=$PWD/data/postgres
|
||||||
DB_USER=mastodon
|
DB_USER=mastodon
|
||||||
DB_NAME=mastodon_dev
|
DB_NAME=mastodon_dev
|
||||||
REDIS_URL=unix://./data/redis/redis-dev.sock
|
REDIS_URL=./data/redis/redis-dev.sock
|
||||||
|
|
||||||
TH_USE_INVITE_QUOTA=1
|
TH_USE_INVITE_QUOTA=1
|
||||||
|
|
19
.github/workflows/build-container-image.yml
vendored
19
.github/workflows/build-container-image.yml
vendored
|
@ -4,11 +4,16 @@ on:
|
||||||
platforms:
|
platforms:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
cache:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
use_native_arm64_builder:
|
use_native_arm64_builder:
|
||||||
type: boolean
|
type: boolean
|
||||||
push_to_images:
|
push_to_images:
|
||||||
type: string
|
type: string
|
||||||
version_suffix:
|
version_prerelease:
|
||||||
|
type: string
|
||||||
|
version_metadata:
|
||||||
type: string
|
type: string
|
||||||
flavor:
|
flavor:
|
||||||
type: string
|
type: string
|
||||||
|
@ -22,7 +27,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v2
|
- uses: docker/setup-qemu-action@v2
|
||||||
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
|
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
|
||||||
|
@ -74,8 +79,6 @@ jobs:
|
||||||
if: ${{ inputs.push_to_images != '' }}
|
if: ${{ inputs.push_to_images != '' }}
|
||||||
with:
|
with:
|
||||||
images: ${{ inputs.push_to_images }}
|
images: ${{ inputs.push_to_images }}
|
||||||
# Only tag with latest when ran against the latest stable branch
|
|
||||||
# This needs to be updated after each minor version release
|
|
||||||
flavor: ${{ inputs.flavor }}
|
flavor: ${{ inputs.flavor }}
|
||||||
tags: ${{ inputs.tags }}
|
tags: ${{ inputs.tags }}
|
||||||
labels: ${{ inputs.labels }}
|
labels: ${{ inputs.labels }}
|
||||||
|
@ -83,12 +86,14 @@ jobs:
|
||||||
- uses: docker/build-push-action@v4
|
- uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
build-args: MASTODON_VERSION_SUFFIX=${{ inputs.version_suffix }}
|
build-args: |
|
||||||
|
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
|
||||||
|
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
|
||||||
platforms: ${{ inputs.platforms }}
|
platforms: ${{ inputs.platforms }}
|
||||||
provenance: false
|
provenance: false
|
||||||
builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}
|
||||||
push: ${{ inputs.push_to_images != '' }}
|
push: ${{ inputs.push_to_images != '' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: ${{ inputs.cache && 'type=gha' || '' }}
|
||||||
cache-to: type=gha,mode=max
|
cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }}
|
||||||
|
|
10
.github/workflows/build-nightly.yml
vendored
10
.github/workflows/build-nightly.yml
vendored
|
@ -16,9 +16,9 @@ jobs:
|
||||||
env:
|
env:
|
||||||
TZ: Etc/UTC
|
TZ: Etc/UTC
|
||||||
run: |
|
run: |
|
||||||
echo mastodon_version_suffix=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
|
echo mastodon_version_prerelease=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
|
||||||
outputs:
|
outputs:
|
||||||
suffix: ${{ steps.version_vars.outputs.mastodon_version_suffix }}
|
prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }}
|
||||||
|
|
||||||
build-image:
|
build-image:
|
||||||
needs: compute-suffix
|
needs: compute-suffix
|
||||||
|
@ -26,10 +26,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
use_native_arm64_builder: false
|
use_native_arm64_builder: false
|
||||||
|
cache: false
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
ghcr.io/${{ github.repository_owner }}/mastodon
|
ghcr.io/${{ github.repository_owner }}/mastodon
|
||||||
# The `-` is important here, result will be v4.1.2-nightly.2022-03-05
|
version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
|
||||||
version_suffix: -${{ needs.compute-suffix.outputs.suffix }}
|
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.description=Nightly build image used for testing purposes
|
org.opencontainers.image.description=Nightly build image used for testing purposes
|
||||||
flavor: |
|
flavor: |
|
||||||
|
@ -37,5 +37,5 @@ jobs:
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=edge
|
type=raw,value=edge
|
||||||
type=raw,value=nightly
|
type=raw,value=nightly
|
||||||
type=schedule,pattern=${{ needs.compute-suffix.outputs.suffix }}
|
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
8
.github/workflows/build-push-pr.yml
vendored
8
.github/workflows/build-push-pr.yml
vendored
|
@ -18,12 +18,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
# Repository needs to be cloned so `git rev-parse` below works
|
# Repository needs to be cloned so `git rev-parse` below works
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- id: version_vars
|
- id: version_vars
|
||||||
run: |
|
run: |
|
||||||
echo mastodon_version_suffix=+pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
|
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
|
||||||
outputs:
|
outputs:
|
||||||
suffix: ${{ steps.version_vars.outputs.mastodon_version_suffix }}
|
metadata: ${{ steps.version_vars.outputs.mastodon_version_metadata }}
|
||||||
|
|
||||||
build-image:
|
build-image:
|
||||||
needs: compute-suffix
|
needs: compute-suffix
|
||||||
|
@ -33,7 +33,7 @@ jobs:
|
||||||
use_native_arm64_builder: false
|
use_native_arm64_builder: false
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
ghcr.io/${{ github.repository_owner }}/mastodon
|
ghcr.io/${{ github.repository_owner }}/mastodon
|
||||||
version_suffix: ${{ needs.compute-suffix.outputs.suffix }}
|
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=auto
|
latest=auto
|
||||||
tags: |
|
tags: |
|
||||||
|
|
4
.github/workflows/build-releases.yml
vendored
4
.github/workflows/build-releases.yml
vendored
|
@ -16,6 +16,10 @@ jobs:
|
||||||
use_native_arm64_builder: false
|
use_native_arm64_builder: false
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
ghcr.io/${{ github.repository_owner }}/mastodon
|
ghcr.io/${{ github.repository_owner }}/mastodon
|
||||||
|
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
|
||||||
|
cache: false
|
||||||
|
# Only tag with latest when ran against the latest stable branch
|
||||||
|
# This needs to be updated after each minor version release
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }}
|
latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }}
|
||||||
tags: |
|
tags: |
|
||||||
|
|
2
.github/workflows/bundler-audit.yml
vendored
2
.github/workflows/bundler-audit.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
- name: Install native Ruby dependencies
|
||||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
run: sudo apt-get install -y libicu-dev libidn11-dev
|
||||||
|
|
2
.github/workflows/check-i18n.yml
vendored
2
.github/workflows/check-i18n.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|
2
.github/workflows/crowdin-download.yml
vendored
2
.github/workflows/crowdin-download.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Increase Git http.postBuffer
|
- name: Increase Git http.postBuffer
|
||||||
# This is needed due to a bug in Ubuntu's cURL version?
|
# This is needed due to a bug in Ubuntu's cURL version?
|
||||||
|
|
2
.github/workflows/crowdin-upload.yml
vendored
2
.github/workflows/crowdin-upload.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@v1
|
uses: crowdin/github-action@v1
|
||||||
|
|
2
.github/workflows/lint-css.yml
vendored
2
.github/workflows/lint-css.yml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
2
.github/workflows/lint-haml.yml
vendored
2
.github/workflows/lint-haml.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
- name: Install native Ruby dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
2
.github/workflows/lint-js.yml
vendored
2
.github/workflows/lint-js.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
2
.github/workflows/lint-json.yml
vendored
2
.github/workflows/lint-json.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
2
.github/workflows/lint-md.yml
vendored
2
.github/workflows/lint-md.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
2
.github/workflows/lint-ruby.yml
vendored
2
.github/workflows/lint-ruby.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
- name: Install native Ruby dependencies
|
||||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
run: sudo apt-get install -y libicu-dev libidn11-dev
|
||||||
|
|
2
.github/workflows/lint-yml.yml
vendored
2
.github/workflows/lint-yml.yml
vendored
|
@ -31,7 +31,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
2
.github/workflows/test-js.yml
vendored
2
.github/workflows/test-js.yml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
|
@ -70,7 +70,7 @@ jobs:
|
||||||
BUNDLE_RETRY: 3
|
BUNDLE_RETRY: 3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
- name: Install native Ruby dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -69,7 +69,7 @@ jobs:
|
||||||
BUNDLE_RETRY: 3
|
BUNDLE_RETRY: 3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install native Ruby dependencies
|
- name: Install native Ruby dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
119
.github/workflows/test-ruby.yml
vendored
119
.github/workflows/test-ruby.yml
vendored
|
@ -32,7 +32,7 @@ jobs:
|
||||||
SECRET_KEY_BASE: precompile_placeholder
|
SECRET_KEY_BASE: precompile_placeholder
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
@ -127,7 +127,7 @@ jobs:
|
||||||
- 3
|
- 3
|
||||||
- 4
|
- 4
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
@ -202,7 +202,7 @@ jobs:
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
@ -250,3 +250,116 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: e2e-screenshots
|
name: e2e-screenshots
|
||||||
path: tmp/screenshots/
|
path: tmp/screenshots/
|
||||||
|
|
||||||
|
test-search:
|
||||||
|
name: Testing search
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:14-alpine
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
options: >-
|
||||||
|
--health-cmd "redis-cli ping"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
|
||||||
|
elasticsearch:
|
||||||
|
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.13
|
||||||
|
env:
|
||||||
|
discovery.type: single-node
|
||||||
|
xpack.security.enabled: false
|
||||||
|
options: >-
|
||||||
|
--health-cmd "curl http://localhost:9200/_cluster/health"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 10
|
||||||
|
ports:
|
||||||
|
- 9200:9200
|
||||||
|
|
||||||
|
env:
|
||||||
|
DB_HOST: localhost
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASS: postgres
|
||||||
|
DISABLE_SIMPLECOV: true
|
||||||
|
RAILS_ENV: test
|
||||||
|
BUNDLE_WITH: test
|
||||||
|
ES_ENABLED: true
|
||||||
|
ES_HOST: localhost
|
||||||
|
ES_PORT: 9200
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
ruby-version:
|
||||||
|
- '3.0'
|
||||||
|
- '3.1'
|
||||||
|
- '.ruby-version'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: './public'
|
||||||
|
name: ${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Update package index
|
||||||
|
run: sudo apt-get update
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: yarn
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Install native Ruby dependencies
|
||||||
|
run: sudo apt-get install -y libicu-dev libidn11-dev
|
||||||
|
|
||||||
|
- name: Install additional system dependencies
|
||||||
|
run: sudo apt-get install -y ffmpeg imagemagick
|
||||||
|
|
||||||
|
- name: Set up bundler cache
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: ${{ matrix.ruby-version}}
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- run: yarn --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Load database schema
|
||||||
|
run: './bin/rails db:create db:schema:load db:seed'
|
||||||
|
|
||||||
|
- run: bundle exec rake spec:search
|
||||||
|
|
||||||
|
- name: Archive logs
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: test-search-logs-${{ matrix.ruby-version }}
|
||||||
|
path: log/
|
||||||
|
|
||||||
|
- name: Archive test screenshots
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: test-search-screenshots
|
||||||
|
path: tmp/screenshots/
|
||||||
|
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
||||||
16.20
|
20.6
|
||||||
|
|
|
@ -37,7 +37,7 @@ Layout/HashAlignment:
|
||||||
Layout/LeadingCommentSpace:
|
Layout/LeadingCommentSpace:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/application.rb'
|
- 'config/application.rb'
|
||||||
- 'config/initializers/omniauth.rb'
|
- 'config/initializers/3_omniauth.rb'
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
|
||||||
|
@ -86,7 +86,7 @@ Lint/UnusedBlockArgument:
|
||||||
Lint/UselessAssignment:
|
Lint/UselessAssignment:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/services/activitypub/process_status_update_service.rb'
|
- 'app/services/activitypub/process_status_update_service.rb'
|
||||||
- 'config/initializers/omniauth.rb'
|
- 'config/initializers/3_omniauth.rb'
|
||||||
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
|
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
|
||||||
- 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb'
|
- 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb'
|
||||||
- 'spec/controllers/api/v1/favourites_controller_spec.rb'
|
- 'spec/controllers/api/v1/favourites_controller_spec.rb'
|
||||||
|
@ -576,11 +576,11 @@ Style/FetchEnvVar:
|
||||||
- 'config/environments/development.rb'
|
- 'config/environments/development.rb'
|
||||||
- 'config/environments/production.rb'
|
- 'config/environments/production.rb'
|
||||||
- 'config/initializers/2_limited_federation_mode.rb'
|
- 'config/initializers/2_limited_federation_mode.rb'
|
||||||
|
- 'config/initializers/3_omniauth.rb'
|
||||||
- 'config/initializers/blacklists.rb'
|
- 'config/initializers/blacklists.rb'
|
||||||
- 'config/initializers/cache_buster.rb'
|
- 'config/initializers/cache_buster.rb'
|
||||||
- 'config/initializers/content_security_policy.rb'
|
- 'config/initializers/content_security_policy.rb'
|
||||||
- 'config/initializers/devise.rb'
|
- 'config/initializers/devise.rb'
|
||||||
- 'config/initializers/omniauth.rb'
|
|
||||||
- 'config/initializers/paperclip.rb'
|
- 'config/initializers/paperclip.rb'
|
||||||
- 'config/initializers/vapid.rb'
|
- 'config/initializers/vapid.rb'
|
||||||
- 'lib/mastodon/premailer_webpack_strategy.rb'
|
- 'lib/mastodon/premailer_webpack_strategy.rb'
|
||||||
|
@ -814,7 +814,7 @@ Style/StringLiterals:
|
||||||
# AllowedMethods: define_method, mail, respond_to
|
# AllowedMethods: define_method, mail, respond_to
|
||||||
Style/SymbolProc:
|
Style/SymbolProc:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/initializers/omniauth.rb'
|
- 'config/initializers/3_omniauth.rb'
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
|
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
|
||||||
|
|
79
CHANGELOG.md
79
CHANGELOG.md
|
@ -8,16 +8,23 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- **Add full-text search of opted-in public posts and rework search operators** ([Gargron](https://github.com/mastodon/mastodon/pull/26485), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26344), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26657), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26650), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26659), [Gargron](https://github.com/mastodon/mastodon/pull/26660), [Gargron](https://github.com/mastodon/mastodon/pull/26663), [Gargron](https://github.com/mastodon/mastodon/pull/26688), [Gargron](https://github.com/mastodon/mastodon/pull/26689), [Gargron](https://github.com/mastodon/mastodon/pull/26686), [Gargron](https://github.com/mastodon/mastodon/pull/26687), [Gargron](https://github.com/mastodon/mastodon/pull/26692), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26697), [Gargron](https://github.com/mastodon/mastodon/pull/26699), [Gargron](https://github.com/mastodon/mastodon/pull/26701), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26710), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26739), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26754), [Gargron](https://github.com/mastodon/mastodon/pull/26662), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26755), [Gargron](https://github.com/mastodon/mastodon/pull/26781), [Gargron](https://github.com/mastodon/mastodon/pull/26782), [Gargron](https://github.com/mastodon/mastodon/pull/26760), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26756), [Gargron](https://github.com/mastodon/mastodon/pull/26784), [Gargron](https://github.com/mastodon/mastodon/pull/26807), [Gargron](https://github.com/mastodon/mastodon/pull/26835), [Gargron](https://github.com/mastodon/mastodon/pull/26847), [Gargron](https://github.com/mastodon/mastodon/pull/26834), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26893), [tribela](https://github.com/mastodon/mastodon/pull/26896))
|
||||||
|
This introduces a new `public_statuses` Elasticsearch index for public posts by users who have opted in to their posts being searchable (`toot#indexable` flag).
|
||||||
|
This also revisits the other indexes to provide more useful indexing, and adds new search operators such as `from:me`, `before:2022-11-01`, `after:2022-11-01`, `during:2022-11-01`, `language:fr`, `has:poll`, or `in:library` (for searching only in posts you have written or interacted with).
|
||||||
|
Results are now ordered chronologically.
|
||||||
|
- **Add admin notifications for new Mastodon versions** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26582))
|
||||||
|
This is done by querying `https://api.joinmastodon.org/update-check` every 30 minutes in a background job.
|
||||||
|
That URL can be changed using the `UPDATE_CHECK_URL` environment variable, and the feature outright disabled by setting that variable to an empty string (`UPDATE_CHECK_URL=`).
|
||||||
- **Add “Privacy and reach” tab in profile settings** ([Gargron](https://github.com/mastodon/mastodon/pull/26484), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26508))
|
- **Add “Privacy and reach” tab in profile settings** ([Gargron](https://github.com/mastodon/mastodon/pull/26484), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26508))
|
||||||
This reorganized scattered privacy and reach settings to a single place, as well as improve their wording.
|
This reorganized scattered privacy and reach settings to a single place, as well as improve their wording.
|
||||||
- **Add display of out-of-band hashtags in the web interface** ([Gargron](https://github.com/mastodon/mastodon/pull/26492), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26497), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26506), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26525))
|
- **Add display of out-of-band hashtags in the web interface** ([Gargron](https://github.com/mastodon/mastodon/pull/26492), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26497), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26506), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26525), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26606), [Gargron](https://github.com/mastodon/mastodon/pull/26666))
|
||||||
- **Add role badges to the web interface** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25649), [Gargron](https://github.com/mastodon/mastodon/pull/26281))
|
- **Add role badges to the web interface** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25649), [Gargron](https://github.com/mastodon/mastodon/pull/26281))
|
||||||
- **Add ability to pick domains to forward reports to using the `forward_to_domains` parameter in `POST /api/v1/reports`** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25866))
|
- **Add ability to pick domains to forward reports to using the `forward_to_domains` parameter in `POST /api/v1/reports`** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25866), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26636))
|
||||||
The `forward_to_domains` REST API parameter is a list of strings. If it is empty or omitted, the previous behavior is maintained.
|
The `forward_to_domains` REST API parameter is a list of strings. If it is empty or omitted, the previous behavior is maintained.
|
||||||
The `forward` parameter still needs to be set for `forward_to_domains` to be taken into account.
|
The `forward` parameter still needs to be set for `forward_to_domains` to be taken into account.
|
||||||
The forwarded-to domains can only include that of the original author and people being replied to.
|
The forwarded-to domains can only include that of the original author and people being replied to.
|
||||||
- **Add forwarding of reported replies to servers being replied to** ([Gargron](https://github.com/mastodon/mastodon/pull/25341), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26189))
|
- **Add forwarding of reported replies to servers being replied to** ([Gargron](https://github.com/mastodon/mastodon/pull/25341), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26189))
|
||||||
- Add direct link to the Single-Sign On provider if there is only one sign up method available ([CSDUMMI](https://github.com/mastodon/mastodon/pull/26083), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26368))
|
- Add `ONE_CLICK_SSO_LOGIN` environment variable to directly link to the Single-Sign On provider if there is only one sign up method available ([CSDUMMI](https://github.com/mastodon/mastodon/pull/26083), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26368), [CSDUMMI](https://github.com/mastodon/mastodon/pull/26857), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26901))
|
||||||
- **Add webhook templating** ([Gargron](https://github.com/mastodon/mastodon/pull/23289))
|
- **Add webhook templating** ([Gargron](https://github.com/mastodon/mastodon/pull/23289))
|
||||||
- **Add webhooks for local `status.created`, `status.updated`, `account.updated` and `report.updated`** ([VyrCossont](https://github.com/mastodon/mastodon/pull/24133), [VyrCossont](https://github.com/mastodon/mastodon/pull/24243), [VyrCossont](https://github.com/mastodon/mastodon/pull/24211))
|
- **Add webhooks for local `status.created`, `status.updated`, `account.updated` and `report.updated`** ([VyrCossont](https://github.com/mastodon/mastodon/pull/24133), [VyrCossont](https://github.com/mastodon/mastodon/pull/24243), [VyrCossont](https://github.com/mastodon/mastodon/pull/24211))
|
||||||
- **Add exclusive lists** ([dariusk](https://github.com/mastodon/mastodon/pull/22048), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25324))
|
- **Add exclusive lists** ([dariusk](https://github.com/mastodon/mastodon/pull/22048), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25324))
|
||||||
|
@ -28,29 +35,38 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
- **Add new onboarding flow to web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24619), [Gargron](https://github.com/mastodon/mastodon/pull/24646), [Gargron](https://github.com/mastodon/mastodon/pull/24705), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24872), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24883), [Gargron](https://github.com/mastodon/mastodon/pull/24954), [stevenjlm](https://github.com/mastodon/mastodon/pull/24959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25010), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25275), [Gargron](https://github.com/mastodon/mastodon/pull/25559), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25561))
|
- **Add new onboarding flow to web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24619), [Gargron](https://github.com/mastodon/mastodon/pull/24646), [Gargron](https://github.com/mastodon/mastodon/pull/24705), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24872), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24883), [Gargron](https://github.com/mastodon/mastodon/pull/24954), [stevenjlm](https://github.com/mastodon/mastodon/pull/24959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25010), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25275), [Gargron](https://github.com/mastodon/mastodon/pull/25559), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25561))
|
||||||
- **Add `S3_DISABLE_CHECKSUM_MODE` environment variable for compatibility with some S3-compatible providers** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26435))
|
- **Add `S3_DISABLE_CHECKSUM_MODE` environment variable for compatibility with some S3-compatible providers** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26435))
|
||||||
- **Add auto-refresh of accounts we get new messages/edits of** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26510))
|
- **Add auto-refresh of accounts we get new messages/edits of** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26510))
|
||||||
- **Add Elasticsearch cluster health check and indexes mismatch check to dashboard** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26448))
|
- **Add Elasticsearch cluster health check and indexes mismatch check to dashboard** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26448), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26658))
|
||||||
- Add support for `indexable` attribute on remote actors ([Gargron](https://github.com/mastodon/mastodon/pull/26485))
|
- Add admin API for managing tags ([rrgeorge](https://github.com/mastodon/mastodon/pull/26872))
|
||||||
|
- Add a link to hashtag timelines from the Trending hashtags moderation interface ([gunchleoc](https://github.com/mastodon/mastodon/pull/26724))
|
||||||
|
- Add timezone to datetimes in e-mails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26822))
|
||||||
|
- Add `authorized_fetch` server setting in addition to env var ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25798))
|
||||||
|
- Add avatar image to webfinger responses ([tvler](https://github.com/mastodon/mastodon/pull/26558))
|
||||||
|
- Add debug logging on signature verification failure ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26637), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26812))
|
||||||
|
- Add explicit error messages when DeepL quota is exceeded ([lutoma](https://github.com/mastodon/mastodon/pull/26704))
|
||||||
|
- Add Elasticsearch/OpenSearch version to “Software” in admin dashboard ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26652))
|
||||||
|
- Add `data-nosnippet` attribute to remote posts and local posts with `noindex` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26648))
|
||||||
|
- Add support for federating `memorial` attribute ([rrgeorge](https://github.com/mastodon/mastodon/pull/26583))
|
||||||
|
- Add Cherokee and Kalmyk to languages dropdown ([gunchleoc](https://github.com/mastodon/mastodon/pull/26012), [gunchleoc](https://github.com/mastodon/mastodon/pull/26013))
|
||||||
- Add `DELETE /api/v1/profile/avatar` and `DELETE /api/v1/profile/header` to the REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25124), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26573))
|
- Add `DELETE /api/v1/profile/avatar` and `DELETE /api/v1/profile/header` to the REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25124), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26573))
|
||||||
- Add `ES_PRESET` option to customize numbers of shards and replicas ([Gargron](https://github.com/mastodon/mastodon/pull/26483), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26489))
|
- Add `ES_PRESET` option to customize numbers of shards and replicas ([Gargron](https://github.com/mastodon/mastodon/pull/26483), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26489))
|
||||||
This can have a value of `single_node_cluster` (default), `small_cluster` (uses one replica) or `large_cluster` (uses one replica and a higher number of shards).
|
This can have a value of `single_node_cluster` (default), `small_cluster` (uses one replica) or `large_cluster` (uses one replica and a higher number of shards).
|
||||||
- Add missing `instances` option to `tootctl search deploy` ([tribela](https://github.com/mastodon/mastodon/pull/26461))
|
|
||||||
- Add `CACHE_BUSTER_HTTP_METHOD` environment variable ([renchap](https://github.com/mastodon/mastodon/pull/26528), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26542))
|
- Add `CACHE_BUSTER_HTTP_METHOD` environment variable ([renchap](https://github.com/mastodon/mastodon/pull/26528), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26542))
|
||||||
- Add support for `DB_PASS` when using `DATABASE_URL` ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26295))
|
- Add support for `DB_PASS` when using `DATABASE_URL` ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26295))
|
||||||
- Add `GET /api/v1/instance/languages` to REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24443))
|
- Add `GET /api/v1/instance/languages` to REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24443))
|
||||||
- Add primary key to `preview_cards_statuses` join table ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25243), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26384), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26447))
|
- Add primary key to `preview_cards_statuses` join table ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25243), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26384), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26447), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26737))
|
||||||
- Add client-side timeout on resend confirmation button ([Gargron](https://github.com/mastodon/mastodon/pull/26300))
|
- Add client-side timeout on resend confirmation button ([Gargron](https://github.com/mastodon/mastodon/pull/26300))
|
||||||
- Add published date and author to news on the explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26155))
|
- Add published date and author to news on the explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26155))
|
||||||
- Add `lang` attribute to various UI components ([c960657](https://github.com/mastodon/mastodon/pull/23869), [c960657](https://github.com/mastodon/mastodon/pull/23891), [c960657](https://github.com/mastodon/mastodon/pull/26111), [c960657](https://github.com/mastodon/mastodon/pull/26149))
|
- Add `lang` attribute to various UI components ([c960657](https://github.com/mastodon/mastodon/pull/23869), [c960657](https://github.com/mastodon/mastodon/pull/23891), [c960657](https://github.com/mastodon/mastodon/pull/26111), [c960657](https://github.com/mastodon/mastodon/pull/26149))
|
||||||
- Add stricter protocol fields validation for accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25937))
|
- Add stricter protocol fields validation for accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25937))
|
||||||
- Add support for Azure blob storage ([mistydemeo](https://github.com/mastodon/mastodon/pull/23607), [mistydemeo](https://github.com/mastodon/mastodon/pull/26080))
|
- Add support for Azure blob storage ([mistydemeo](https://github.com/mastodon/mastodon/pull/23607), [mistydemeo](https://github.com/mastodon/mastodon/pull/26080))
|
||||||
- Add toast with option to open post after publishing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25564), [Signez](https://github.com/mastodon/mastodon/pull/25919))
|
- Add toast with option to open post after publishing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25564), [Signez](https://github.com/mastodon/mastodon/pull/25919), [Gargron](https://github.com/mastodon/mastodon/pull/26664))
|
||||||
- Add canonical link tags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25715))
|
- Add canonical link tags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25715))
|
||||||
- Add button to see results for polls in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25726))
|
- Add button to see results for polls in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25726))
|
||||||
- Add at-symbol prepended to mention span title ([forsamori](https://github.com/mastodon/mastodon/pull/25684))
|
- Add at-symbol prepended to mention span title ([forsamori](https://github.com/mastodon/mastodon/pull/25684))
|
||||||
- Add users index on `unconfirmed_email` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25672), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25702))
|
- Add users index on `unconfirmed_email` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25672), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25702))
|
||||||
- Add superapp index on `oauth_applications` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25670))
|
- Add superapp index on `oauth_applications` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25670))
|
||||||
- Add index to backups on `user_id` column ([mjankowski](https://github.com/mastodon/mastodon/pull/25647))
|
- Add index to backups on `user_id` column ([mjankowski](https://github.com/mastodon/mastodon/pull/25647))
|
||||||
- Add onboarding prompt when home feed too slow in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25267), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25556), [Gargron](https://github.com/mastodon/mastodon/pull/25579), [renchap](https://github.com/mastodon/mastodon/pull/25580), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25581), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25617), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25917))
|
- Add onboarding prompt when home feed too slow in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25267), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25556), [Gargron](https://github.com/mastodon/mastodon/pull/25579), [renchap](https://github.com/mastodon/mastodon/pull/25580), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25581), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25617), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25917), [Gargron](https://github.com/mastodon/mastodon/pull/26829))
|
||||||
- Add `POST /api/v1/conversations/:id/unread` API endpoint to mark a conversation as unread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25509))
|
- Add `POST /api/v1/conversations/:id/unread` API endpoint to mark a conversation as unread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25509))
|
||||||
- Add `translate="no"` to outgoing mentions and links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25524))
|
- Add `translate="no"` to outgoing mentions and links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25524))
|
||||||
- Add unsubscribe link and headers to e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/25378), [c960657](https://github.com/mastodon/mastodon/pull/26085))
|
- Add unsubscribe link and headers to e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/25378), [c960657](https://github.com/mastodon/mastodon/pull/26085))
|
||||||
|
@ -93,24 +109,31 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- **Change hashtags to be displayed separately when they are the last line of a post** ([renchap](https://github.com/mastodon/mastodon/pull/26499))
|
- **Change hashtags to be displayed separately when they are the last line of a post** ([renchap](https://github.com/mastodon/mastodon/pull/26499), [renchap](https://github.com/mastodon/mastodon/pull/26614), [renchap](https://github.com/mastodon/mastodon/pull/26615))
|
||||||
- **Change reblogs to be excluded from "Posts and replies" tab in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26302))
|
- **Change reblogs to be excluded from "Posts and replies" tab in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26302))
|
||||||
- **Change interaction modal in web interface** ([Gargron, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26075), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26269), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26268), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26267), [mgmn](https://github.com/mastodon/mastodon/pull/26459))
|
- **Change interaction modal in web interface** ([Gargron, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26075), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26269), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26268), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26267), [mgmn](https://github.com/mastodon/mastodon/pull/26459), [tribela](https://github.com/mastodon/mastodon/pull/26461), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26593), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26795))
|
||||||
- **Change design of link previews in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26136), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26151), [Gargron](https://github.com/mastodon/mastodon/pull/26153), [Gargron](https://github.com/mastodon/mastodon/pull/26250), [Gargron](https://github.com/mastodon/mastodon/pull/26287), [Gargron](https://github.com/mastodon/mastodon/pull/26286), [c960657](https://github.com/mastodon/mastodon/pull/26184))
|
- **Change design of link previews in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26136), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26151), [Gargron](https://github.com/mastodon/mastodon/pull/26153), [Gargron](https://github.com/mastodon/mastodon/pull/26250), [Gargron](https://github.com/mastodon/mastodon/pull/26287), [Gargron](https://github.com/mastodon/mastodon/pull/26286), [c960657](https://github.com/mastodon/mastodon/pull/26184))
|
||||||
- **Change "direct message" nomenclature to "private mention" in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24248))
|
- **Change "direct message" nomenclature to "private mention" in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24248))
|
||||||
- **Change translation feature to cover Content Warnings, poll options and media descriptions** ([c960657](https://github.com/mastodon/mastodon/pull/24175), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25251), [c960657](https://github.com/mastodon/mastodon/pull/26168), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26452))
|
- **Change translation feature to cover Content Warnings, poll options and media descriptions** ([c960657](https://github.com/mastodon/mastodon/pull/24175), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25251), [c960657](https://github.com/mastodon/mastodon/pull/26168), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26452))
|
||||||
- **Change account search to match by text when opted-in** ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25599), [Gargron](https://github.com/mastodon/mastodon/pull/26378))
|
- **Change account search to match by text when opted-in** ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25599), [Gargron](https://github.com/mastodon/mastodon/pull/26378))
|
||||||
- **Change import feature to be clearer, less error-prone and more reliable** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21054), [mgmn](https://github.com/mastodon/mastodon/pull/24874))
|
- **Change import feature to be clearer, less error-prone and more reliable** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21054), [mgmn](https://github.com/mastodon/mastodon/pull/24874))
|
||||||
- **Change local and federated timelines to be in a single “Live feeds” column** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25641), [Gargron](https://github.com/mastodon/mastodon/pull/25683), [mgmn](https://github.com/mastodon/mastodon/pull/25694), [Plastikmensch](https://github.com/mastodon/mastodon/pull/26247))
|
- **Change local and federated timelines to be tabs of a single “Live feeds” column** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25641), [Gargron](https://github.com/mastodon/mastodon/pull/25683), [mgmn](https://github.com/mastodon/mastodon/pull/25694), [Plastikmensch](https://github.com/mastodon/mastodon/pull/26247), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26633))
|
||||||
- **Change user archive export to be faster and more reliable, and export `.zip` archives instead of `.tar.gz` ones** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23360), [TheEssem](https://github.com/mastodon/mastodon/pull/25034))
|
- **Change user archive export to be faster and more reliable, and export `.zip` archives instead of `.tar.gz` ones** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23360), [TheEssem](https://github.com/mastodon/mastodon/pull/25034))
|
||||||
- **Change `mastodon-streaming` systemd unit files to be templated** ([e-nomem](https://github.com/mastodon/mastodon/pull/24751))
|
- **Change `mastodon-streaming` systemd unit files to be templated** ([e-nomem](https://github.com/mastodon/mastodon/pull/24751))
|
||||||
- **Change `statsd` integration to disable sidekiq metrics by default** ([mjankowski](https://github.com/mastodon/mastodon/pull/25265), [mjankowski](https://github.com/mastodon/mastodon/pull/25336), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26310))
|
- **Change `statsd` integration to disable sidekiq metrics by default** ([mjankowski](https://github.com/mastodon/mastodon/pull/25265), [mjankowski](https://github.com/mastodon/mastodon/pull/25336), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26310))
|
||||||
This deprecates `statsd` support and disables the sidekiq integration unless `STATSD_SIDEKIQ` is set to `true`.
|
This deprecates `statsd` support and disables the sidekiq integration unless `STATSD_SIDEKIQ` is set to `true`.
|
||||||
This is because the `nsa` gem is unmaintained, and its sidekiq integration is known to add very significant overhead.
|
This is because the `nsa` gem is unmaintained, and its sidekiq integration is known to add very significant overhead.
|
||||||
Later versions of Mastodon will have other ways to get the same metrics.
|
Later versions of Mastodon will have other ways to get the same metrics.
|
||||||
- **Change replica support to native Rails adapter** ([krainboltgreene](https://github.com/mastodon/mastodon/pull/25693), [Gargron](https://github.com/mastodon/mastodon/pull/25849), [Gargron](https://github.com/mastodon/mastodon/pull/25874), [Gargron](https://github.com/mastodon/mastodon/pull/25851), [Gargron](https://github.com/mastodon/mastodon/pull/25977), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26074), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26326), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26386))
|
- **Change replica support to native Rails adapter** ([krainboltgreene](https://github.com/mastodon/mastodon/pull/25693), [Gargron](https://github.com/mastodon/mastodon/pull/25849), [Gargron](https://github.com/mastodon/mastodon/pull/25874), [Gargron](https://github.com/mastodon/mastodon/pull/25851), [Gargron](https://github.com/mastodon/mastodon/pull/25977), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26074), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26326), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26386), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26856))
|
||||||
This is a breaking change, dropping `makara` support, and requiring you to update your database configuration if you are using replicas.
|
This is a breaking change, dropping `makara` support, and requiring you to update your database configuration if you are using replicas.
|
||||||
To tell Mastodon to use a read replica, you can either set the `REPLICA_DB_NAME` environment variable (along with `REPLICA_DB_USER`, `REPLICA_DB_PASS`, `REPLICA_DB_HOST`, and `REPLICA_DB_PORT`, if they differ from the primary database), or the `REPLICA_DATABASE_URL` environment variable if your configuration is based on `DATABASE_URL`.
|
To tell Mastodon to use a read replica, you can either set the `REPLICA_DB_NAME` environment variable (along with `REPLICA_DB_USER`, `REPLICA_DB_PASS`, `REPLICA_DB_HOST`, and `REPLICA_DB_PORT`, if they differ from the primary database), or the `REPLICA_DATABASE_URL` environment variable if your configuration is based on `DATABASE_URL`.
|
||||||
|
- Change DCT method used for JPEG encoding to float ([electroCutie](https://github.com/mastodon/mastodon/pull/26675))
|
||||||
|
- Change from `node-redis` to `ioredis` for streaming ([gmemstr](https://github.com/mastodon/mastodon/pull/26581))
|
||||||
|
- Change private statuses index to index without crutches ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26713))
|
||||||
|
- Change video compression parameters ([Gargron](https://github.com/mastodon/mastodon/pull/26631), [Gargron](https://github.com/mastodon/mastodon/pull/26745), [Gargron](https://github.com/mastodon/mastodon/pull/26766))
|
||||||
|
- Change admin e-mail notification settings to be their own settings group ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26596))
|
||||||
|
- Change opacity of the delete icon in the search field to be more visible ([AntoninDelFabbro](https://github.com/mastodon/mastodon/pull/26449))
|
||||||
|
- Change Account Search to prioritize username over display name ([jsgoldstein](https://github.com/mastodon/mastodon/pull/26623))
|
||||||
- Change follow recommendation materialized view to be faster in most cases ([renchap, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26545))
|
- Change follow recommendation materialized view to be faster in most cases ([renchap, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26545))
|
||||||
- Change `robots.txt` to block GPTBot ([Foritus](https://github.com/mastodon/mastodon/pull/26396))
|
- Change `robots.txt` to block GPTBot ([Foritus](https://github.com/mastodon/mastodon/pull/26396))
|
||||||
- Change header of hashtag timelines in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26362), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26416))
|
- Change header of hashtag timelines in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26362), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26416))
|
||||||
|
@ -120,9 +143,9 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
- Change poll form element colors to fit with the rest of the ui ([teeerevor](https://github.com/mastodon/mastodon/pull/26139), [teeerevor](https://github.com/mastodon/mastodon/pull/26162), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26164))
|
- Change poll form element colors to fit with the rest of the ui ([teeerevor](https://github.com/mastodon/mastodon/pull/26139), [teeerevor](https://github.com/mastodon/mastodon/pull/26162), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26164))
|
||||||
- Change 'favourite' to 'favorite' for American English ([marekr](https://github.com/mastodon/mastodon/pull/24667), [gunchleoc](https://github.com/mastodon/mastodon/pull/26009), [nabijaczleweli](https://github.com/mastodon/mastodon/pull/26109))
|
- Change 'favourite' to 'favorite' for American English ([marekr](https://github.com/mastodon/mastodon/pull/24667), [gunchleoc](https://github.com/mastodon/mastodon/pull/26009), [nabijaczleweli](https://github.com/mastodon/mastodon/pull/26109))
|
||||||
- Change ActivityStreams representation of suspended accounts to not use a blank `name` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25276))
|
- Change ActivityStreams representation of suspended accounts to not use a blank `name` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25276))
|
||||||
- Change focus UI for keyboard only input ([teeerevor](https://github.com/mastodon/mastodon/pull/25935), [Gargron](https://github.com/mastodon/mastodon/pull/26125))
|
- Change focus UI for keyboard only input ([teeerevor](https://github.com/mastodon/mastodon/pull/25935), [Gargron](https://github.com/mastodon/mastodon/pull/26125), [Gargron](https://github.com/mastodon/mastodon/pull/26767))
|
||||||
- Change thread view to scroll to the selected post rather than the post being replied to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24685))
|
- Change thread view to scroll to the selected post rather than the post being replied to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24685))
|
||||||
- Change links in multi-column mode so tabs are open in single-column mode ([Signez](https://github.com/mastodon/mastodon/pull/25893), [Signez](https://github.com/mastodon/mastodon/pull/26070), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25973))
|
- Change links in multi-column mode so tabs are open in single-column mode ([Signez](https://github.com/mastodon/mastodon/pull/25893), [Signez](https://github.com/mastodon/mastodon/pull/26070), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25973), [Signez](https://github.com/mastodon/mastodon/pull/26019), [Signez](https://github.com/mastodon/mastodon/pull/26759))
|
||||||
- Change searching with `#` to include account index ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25638))
|
- Change searching with `#` to include account index ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25638))
|
||||||
- Change label and design of sensitive and unavailable media in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25712), [Gargron](https://github.com/mastodon/mastodon/pull/26135), [Gargron](https://github.com/mastodon/mastodon/pull/26330))
|
- Change label and design of sensitive and unavailable media in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25712), [Gargron](https://github.com/mastodon/mastodon/pull/26135), [Gargron](https://github.com/mastodon/mastodon/pull/26330))
|
||||||
- Change button colors to increase hover/focus contrast and consistency ([teeerevor](https://github.com/mastodon/mastodon/pull/25677), [Gargron](https://github.com/mastodon/mastodon/pull/25679))
|
- Change button colors to increase hover/focus contrast and consistency ([teeerevor](https://github.com/mastodon/mastodon/pull/25677), [Gargron](https://github.com/mastodon/mastodon/pull/25679))
|
||||||
|
@ -141,7 +164,7 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
- Change vacuum scheduler to also delete expired tokens and unused application records ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24871))
|
- Change vacuum scheduler to also delete expired tokens and unused application records ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24871))
|
||||||
- Change "Sign in" to "Login" ([Gargron](https://github.com/mastodon/mastodon/pull/24942))
|
- Change "Sign in" to "Login" ([Gargron](https://github.com/mastodon/mastodon/pull/24942))
|
||||||
- Change domain suspensions to also be checked before trying to fetch unknown remote resources ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24535))
|
- Change domain suspensions to also be checked before trying to fetch unknown remote resources ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24535))
|
||||||
- Change media components to use aspect-ratio rather than compute height themselves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24686), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24943))
|
- Change media components to use aspect-ratio rather than compute height themselves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24686), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24943), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26801))
|
||||||
- Change logo version in header based on screen size in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24707))
|
- Change logo version in header based on screen size in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24707))
|
||||||
- Change label from "For you" to "People" on explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24706))
|
- Change label from "For you" to "People" on explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24706))
|
||||||
- Change logged-out WebUI HTML pages to be cached for a few seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24708))
|
- Change logged-out WebUI HTML pages to be cached for a few seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24708))
|
||||||
|
@ -152,7 +175,7 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
- Change account search in moderation interface to allow searching by username including the leading `@` ([HeitorMC](https://github.com/mastodon/mastodon/pull/24242))
|
- Change account search in moderation interface to allow searching by username including the leading `@` ([HeitorMC](https://github.com/mastodon/mastodon/pull/24242))
|
||||||
- Change all components to use the same error page in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24512))
|
- Change all components to use the same error page in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24512))
|
||||||
- Change search pop-out in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24305))
|
- Change search pop-out in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24305))
|
||||||
- Change user settings to be stored in a more optimal way ([Gargron](https://github.com/mastodon/mastodon/pull/23630), [c960657](https://github.com/mastodon/mastodon/pull/24321), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24460), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24558), [Gargron](https://github.com/mastodon/mastodon/pull/24761), [Gargron](https://github.com/mastodon/mastodon/pull/24783), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25508), [jsgoldstein](https://github.com/mastodon/mastodon/pull/25340))
|
- Change user settings to be stored in a more optimal way ([Gargron](https://github.com/mastodon/mastodon/pull/23630), [c960657](https://github.com/mastodon/mastodon/pull/24321), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24460), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24558), [Gargron](https://github.com/mastodon/mastodon/pull/24761), [Gargron](https://github.com/mastodon/mastodon/pull/24783), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25508), [jsgoldstein](https://github.com/mastodon/mastodon/pull/25340), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26884))
|
||||||
- Change media upload limits and remove client-side resizing ([Gargron](https://github.com/mastodon/mastodon/pull/23726))
|
- Change media upload limits and remove client-side resizing ([Gargron](https://github.com/mastodon/mastodon/pull/23726))
|
||||||
- Change design of account rows in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24247), [Gargron](https://github.com/mastodon/mastodon/pull/24343), [Gargron](https://github.com/mastodon/mastodon/pull/24956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25131))
|
- Change design of account rows in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24247), [Gargron](https://github.com/mastodon/mastodon/pull/24343), [Gargron](https://github.com/mastodon/mastodon/pull/24956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25131))
|
||||||
- Change log-out to use Single Logout when using external log-in through OIDC ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24020))
|
- Change log-out to use Single Logout when using external log-in through OIDC ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24020))
|
||||||
|
@ -175,6 +198,8 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
- **Remove support for Ruby 2.7** ([nschonni](https://github.com/mastodon/mastodon/pull/24237))
|
- **Remove support for Ruby 2.7** ([nschonni](https://github.com/mastodon/mastodon/pull/24237))
|
||||||
- **Remove clustering from streaming API** ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24655))
|
- **Remove clustering from streaming API** ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24655))
|
||||||
- **Remove anonymous access to the streaming API** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23989))
|
- **Remove anonymous access to the streaming API** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23989))
|
||||||
|
- Remove obfuscation of reply count in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26768))
|
||||||
|
- Remove `kmr` from language selection, as it was a duplicate for `ku` ([gunchleoc](https://github.com/mastodon/mastodon/pull/26014), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26787))
|
||||||
- Remove 16:9 cropping from web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26132))
|
- Remove 16:9 cropping from web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26132))
|
||||||
- Remove back button from bookmarks, favourites and lists screens in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26126))
|
- Remove back button from bookmarks, favourites and lists screens in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26126))
|
||||||
- Remove display name input from sign-up form ([Gargron](https://github.com/mastodon/mastodon/pull/24704))
|
- Remove display name input from sign-up form ([Gargron](https://github.com/mastodon/mastodon/pull/24704))
|
||||||
|
@ -189,6 +214,26 @@ The following changelog entries focus on changes visible to users, administrator
|
||||||
- **Fix log-in flow when involving both OAuth and external authentication** ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24073))
|
- **Fix log-in flow when involving both OAuth and external authentication** ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24073))
|
||||||
- **Fix broken links in account gallery** ([c960657](https://github.com/mastodon/mastodon/pull/24218))
|
- **Fix broken links in account gallery** ([c960657](https://github.com/mastodon/mastodon/pull/24218))
|
||||||
- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392))
|
- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392))
|
||||||
|
- **Fix migration handler not updating lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24808))
|
||||||
|
- Fix paragraph margins resulting in irregular read-more cut-off in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26828))
|
||||||
|
- Fix notification permissions being requested immediately after login ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26472))
|
||||||
|
- Fix performances of profile directory ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26840), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26842))
|
||||||
|
- Fix mute button and volume slider feeling disconnected in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26827), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26860))
|
||||||
|
- Fix “Scoped order is ignored, it's forced to be batch order.” warnings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26793))
|
||||||
|
- Fix blocked domain appearing in account feeds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26823))
|
||||||
|
- Fix moderator rights inconsistencies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26729))
|
||||||
|
- Fix crash when encountering invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26814))
|
||||||
|
- Fix invalid `Content-Type` header for WebP images ([c960657](https://github.com/mastodon/mastodon/pull/26773))
|
||||||
|
- Fix minor inefficiencies in `tootctl search deploy` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26721))
|
||||||
|
- Fix filter form in profiles directory overflowing instead of wrapping ([arbolitoloco1](https://github.com/mastodon/mastodon/pull/26682))
|
||||||
|
- Fix `/api/v1/timelines/tag/:hashtag` allowing for unauthenticated access when public preview is disabled ([danielmbrasil](https://github.com/mastodon/mastodon/pull/26237))
|
||||||
|
- Fix inefficiencies in `PlainTextFormatter` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26727))
|
||||||
|
- Fix sign up steps progress layout in right-to-left locales ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26728))
|
||||||
|
- Fix bug with “favorited by” and “reblogged by“ view on posts only showing up to 40 items ([timothyjrogers](https://github.com/mastodon/mastodon/pull/26577), [timothyjrogers](https://github.com/mastodon/mastodon/pull/26574))
|
||||||
|
- Fix bad search type heuristic ([Gargron](https://github.com/mastodon/mastodon/pull/26673))
|
||||||
|
- Fix not being able to negate prefix clauses in search ([Gargron](https://github.com/mastodon/mastodon/pull/26672))
|
||||||
|
- Fix timeout on invalid set of exclusionary parameters in `/api/v1/timelines/public` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/26239))
|
||||||
|
- Fix unexpected audio stream transcoding when uploaded video is eligible to passthrough ([yufushiro](https://github.com/mastodon/mastodon/pull/26608))
|
||||||
- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500))
|
- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500))
|
||||||
- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409))
|
- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409))
|
||||||
- Fix adding column with default value taking longer on Postgres >= 11 ([Gargron](https://github.com/mastodon/mastodon/pull/26375))
|
- Fix adding column with default value taking longer on Postgres >= 11 ([Gargron](https://github.com/mastodon/mastodon/pull/26375))
|
||||||
|
|
29
Dockerfile
29
Dockerfile
|
@ -1,6 +1,6 @@
|
||||||
# syntax=docker/dockerfile:1.4
|
# syntax=docker/dockerfile:1.4
|
||||||
# This needs to be bullseye-slim because the Ruby image is built on bullseye-slim
|
# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim
|
||||||
ARG NODE_IMAGE=node:18.16-bullseye-slim
|
ARG NODE_VERSION="20.6-bookworm-slim"
|
||||||
ARG RUBY_IMAGE=ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim
|
ARG RUBY_IMAGE=ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim
|
||||||
|
|
||||||
# hadolint ignore=DL3006
|
# hadolint ignore=DL3006
|
||||||
|
@ -25,6 +25,7 @@ RUN --mount=type=cache,id=apt,target=/var/cache/apt,sharing=private \
|
||||||
rm -f /etc/apt/apt.conf.d/docker-clean && \
|
rm -f /etc/apt/apt.conf.d/docker-clean && \
|
||||||
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache && \
|
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
|
apt-get -yq dist-upgrade && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
@ -32,7 +33,7 @@ RUN --mount=type=cache,id=apt,target=/var/cache/apt,sharing=private \
|
||||||
libgdbm-dev \
|
libgdbm-dev \
|
||||||
libgmp-dev \
|
libgmp-dev \
|
||||||
libicu-dev \
|
libicu-dev \
|
||||||
libidn11-dev \
|
libidn-dev \
|
||||||
libjemalloc-dev \
|
libjemalloc-dev \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libreadline8 \
|
libreadline8 \
|
||||||
|
@ -56,7 +57,7 @@ RUN --mount=type=cache,id=bundle,target=/opt/bundle/cache,sharing=private \
|
||||||
bundle config set cache_path /opt/bundle/cache && \
|
bundle config set cache_path /opt/bundle/cache && \
|
||||||
bundle config set silence_root_warning 'true' && \
|
bundle config set silence_root_warning 'true' && \
|
||||||
bundle cache --no-install && \
|
bundle cache --no-install && \
|
||||||
bundle config set --local deployment 'true' && \
|
bundle config set --local deployment true && \
|
||||||
bundle install --local -j"$(nproc)" && \
|
bundle install --local -j"$(nproc)" && \
|
||||||
yarn install --immutable
|
yarn install --immutable
|
||||||
|
|
||||||
|
@ -68,8 +69,8 @@ COPY --link . /opt/mastodon
|
||||||
# build
|
# build
|
||||||
FROM build-base AS build
|
FROM build-base AS build
|
||||||
|
|
||||||
ENV RAILS_ENV="production" \
|
ENV RAILS_ENV=production \
|
||||||
NODE_ENV="production"
|
NODE_ENV=production
|
||||||
|
|
||||||
ENV NODE_OPTIONS=--openssl-legacy-provider \
|
ENV NODE_OPTIONS=--openssl-legacy-provider \
|
||||||
YARN_GLOBAL_FOLDER=/opt/yarn \
|
YARN_GLOBAL_FOLDER=/opt/yarn \
|
||||||
|
@ -110,12 +111,12 @@ RUN --mount=type=cache,id=apt,target=/var/cache/apt,sharing=private \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
file \
|
file \
|
||||||
imagemagick \
|
imagemagick \
|
||||||
libicu67 \
|
libicu72 \
|
||||||
libidn11 \
|
libidn12 \
|
||||||
libjemalloc2 \
|
libjemalloc2 \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
libreadline8 \
|
libreadline8 \
|
||||||
libssl1.1 \
|
libssl3 \
|
||||||
libyaml-0-2 \
|
libyaml-0-2 \
|
||||||
procps \
|
procps \
|
||||||
tini \
|
tini \
|
||||||
|
@ -128,8 +129,8 @@ FROM output-base as output
|
||||||
|
|
||||||
# Use those args to specify your own version flags & suffixes
|
# Use those args to specify your own version flags & suffixes
|
||||||
ARG SOURCE_TAG=""
|
ARG SOURCE_TAG=""
|
||||||
ARG MASTODON_VERSION_FLAGS=""
|
ARG MASTODON_VERSION_PRERELEASE=""
|
||||||
ARG MASTODON_VERSION_SUFFIX=""
|
ARG MASTODON_VERSION_METADATA=""
|
||||||
|
|
||||||
ARG UID="991"
|
ARG UID="991"
|
||||||
ARG GID="991"
|
ARG GID="991"
|
||||||
|
@ -141,7 +142,7 @@ ENV PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin"
|
||||||
# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use
|
# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use
|
||||||
# hadolint ignore=DL3008,DL3009
|
# hadolint ignore=DL3008,DL3009
|
||||||
RUN groupadd -g "${GID}" mastodon && \
|
RUN groupadd -g "${GID}" mastodon && \
|
||||||
useradd -l -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \
|
useradd -l -u "${UID}" -g "${GID}" -m -d /opt/mastodon mastodon && \
|
||||||
ln -s /opt/mastodon /mastodon
|
ln -s /opt/mastodon /mastodon
|
||||||
|
|
||||||
# Note: no, cleaning here since Debian does this automatically
|
# Note: no, cleaning here since Debian does this automatically
|
||||||
|
@ -155,8 +156,8 @@ ENV RAILS_ENV="production" \
|
||||||
RAILS_SERVE_STATIC_FILES="true" \
|
RAILS_SERVE_STATIC_FILES="true" \
|
||||||
BIND="0.0.0.0" \
|
BIND="0.0.0.0" \
|
||||||
SOURCE_TAG="${SOURCE_TAG}" \
|
SOURCE_TAG="${SOURCE_TAG}" \
|
||||||
MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \
|
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
|
||||||
MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}"
|
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}"
|
||||||
|
|
||||||
# override this at will
|
# override this at will
|
||||||
ENV BOOTSNAP_READONLY=1
|
ENV BOOTSNAP_READONLY=1
|
||||||
|
|
|
@ -27,4 +27,5 @@ More information on HTTP Signatures, as well as examples, can be found here: htt
|
||||||
|
|
||||||
- Linked-Data Signatures: https://docs.joinmastodon.org/spec/security/#ld
|
- Linked-Data Signatures: https://docs.joinmastodon.org/spec/security/#ld
|
||||||
- Bearcaps: https://docs.joinmastodon.org/spec/bearcaps/
|
- Bearcaps: https://docs.joinmastodon.org/spec/bearcaps/
|
||||||
- Followers collection synchronization: https://git.activitypub.dev/ActivityPubDev/Fediverse-Enhancement-Proposals/src/branch/main/feps/fep-8fcf.md
|
- Followers collection synchronization: https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md
|
||||||
|
- Search indexing consent for actors: https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md
|
||||||
|
|
138
Gemfile.lock
138
Gemfile.lock
|
@ -39,47 +39,47 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.0.7.2)
|
actioncable (7.0.8)
|
||||||
actionpack (= 7.0.7.2)
|
actionpack (= 7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (7.0.7.2)
|
actionmailbox (7.0.8)
|
||||||
actionpack (= 7.0.7.2)
|
actionpack (= 7.0.8)
|
||||||
activejob (= 7.0.7.2)
|
activejob (= 7.0.8)
|
||||||
activerecord (= 7.0.7.2)
|
activerecord (= 7.0.8)
|
||||||
activestorage (= 7.0.7.2)
|
activestorage (= 7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.0.7.2)
|
actionmailer (7.0.8)
|
||||||
actionpack (= 7.0.7.2)
|
actionpack (= 7.0.8)
|
||||||
actionview (= 7.0.7.2)
|
actionview (= 7.0.8)
|
||||||
activejob (= 7.0.7.2)
|
activejob (= 7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (7.0.7.2)
|
actionpack (7.0.8)
|
||||||
actionview (= 7.0.7.2)
|
actionview (= 7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
rack (~> 2.0, >= 2.2.4)
|
rack (~> 2.0, >= 2.2.4)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (7.0.7.2)
|
actiontext (7.0.8)
|
||||||
actionpack (= 7.0.7.2)
|
actionpack (= 7.0.8)
|
||||||
activerecord (= 7.0.7.2)
|
activerecord (= 7.0.8)
|
||||||
activestorage (= 7.0.7.2)
|
activestorage (= 7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.0.7.2)
|
actionview (7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
|
@ -89,27 +89,27 @@ GEM
|
||||||
activemodel (>= 4.1, < 7.1)
|
activemodel (>= 4.1, < 7.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (7.0.7.2)
|
activejob (7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.0.7.2)
|
activemodel (7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
activerecord (7.0.7.2)
|
activerecord (7.0.8)
|
||||||
activemodel (= 7.0.7.2)
|
activemodel (= 7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
activestorage (7.0.7.2)
|
activestorage (7.0.8)
|
||||||
actionpack (= 7.0.7.2)
|
actionpack (= 7.0.8)
|
||||||
activejob (= 7.0.7.2)
|
activejob (= 7.0.8)
|
||||||
activerecord (= 7.0.7.2)
|
activerecord (= 7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (7.0.7.2)
|
activesupport (7.0.8)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.4)
|
addressable (2.8.5)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
aes_key_wrap (1.1.0)
|
aes_key_wrap (1.1.0)
|
||||||
airbrussh (1.4.1)
|
airbrussh (1.4.1)
|
||||||
|
@ -124,8 +124,8 @@ GEM
|
||||||
attr_required (1.0.1)
|
attr_required (1.0.1)
|
||||||
awrence (1.2.1)
|
awrence (1.2.1)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.793.0)
|
aws-partitions (1.809.0)
|
||||||
aws-sdk-core (3.180.3)
|
aws-sdk-core (3.181.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
|
@ -133,8 +133,8 @@ GEM
|
||||||
aws-sdk-kms (1.71.0)
|
aws-sdk-kms (1.71.0)
|
||||||
aws-sdk-core (~> 3, >= 3.177.0)
|
aws-sdk-core (~> 3, >= 3.177.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.132.1)
|
aws-sdk-s3 (1.133.0)
|
||||||
aws-sdk-core (~> 3, >= 3.179.0)
|
aws-sdk-core (~> 3, >= 3.181.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.6)
|
aws-sigv4 (~> 1.6)
|
||||||
aws-sigv4 (1.6.0)
|
aws-sigv4 (1.6.0)
|
||||||
|
@ -203,7 +203,7 @@ GEM
|
||||||
activesupport
|
activesupport
|
||||||
cbor (0.5.9.6)
|
cbor (0.5.9.6)
|
||||||
charlock_holmes (0.7.7)
|
charlock_holmes (0.7.7)
|
||||||
chewy (7.3.3)
|
chewy (7.3.4)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
elasticsearch (>= 7.12.0, < 7.14.0)
|
elasticsearch (>= 7.12.0, < 7.14.0)
|
||||||
elasticsearch-dsl
|
elasticsearch-dsl
|
||||||
|
@ -325,7 +325,7 @@ GEM
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (1.1.0)
|
globalid (1.1.0)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 5.0)
|
||||||
haml (6.1.1)
|
haml (6.1.2)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
|
@ -334,7 +334,7 @@ GEM
|
||||||
activesupport (>= 5.1)
|
activesupport (>= 5.1)
|
||||||
haml (>= 4.0.6)
|
haml (>= 4.0.6)
|
||||||
railties (>= 5.1)
|
railties (>= 5.1)
|
||||||
haml_lint (0.49.3)
|
haml_lint (0.50.0)
|
||||||
haml (>= 4.0, < 6.2)
|
haml (>= 4.0, < 6.2)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
|
@ -410,7 +410,7 @@ GEM
|
||||||
activerecord
|
activerecord
|
||||||
kaminari-core (= 1.2.2)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-core (1.2.2)
|
kaminari-core (1.2.2)
|
||||||
kt-paperclip (7.2.0)
|
kt-paperclip (7.2.1)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
marcel (~> 1.0.1)
|
marcel (~> 1.0.1)
|
||||||
|
@ -483,7 +483,7 @@ GEM
|
||||||
nokogiri (1.15.4)
|
nokogiri (1.15.4)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.0)
|
oj (3.16.1)
|
||||||
omniauth (2.1.1)
|
omniauth (2.1.1)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
|
@ -520,8 +520,8 @@ GEM
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.5.3)
|
pg (1.5.4)
|
||||||
pghero (3.3.3)
|
pghero (3.3.4)
|
||||||
activerecord (>= 6)
|
activerecord (>= 6)
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
premailer (1.21.0)
|
premailer (1.21.0)
|
||||||
|
@ -557,20 +557,20 @@ GEM
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (7.0.7.2)
|
rails (7.0.8)
|
||||||
actioncable (= 7.0.7.2)
|
actioncable (= 7.0.8)
|
||||||
actionmailbox (= 7.0.7.2)
|
actionmailbox (= 7.0.8)
|
||||||
actionmailer (= 7.0.7.2)
|
actionmailer (= 7.0.8)
|
||||||
actionpack (= 7.0.7.2)
|
actionpack (= 7.0.8)
|
||||||
actiontext (= 7.0.7.2)
|
actiontext (= 7.0.8)
|
||||||
actionview (= 7.0.7.2)
|
actionview (= 7.0.8)
|
||||||
activejob (= 7.0.7.2)
|
activejob (= 7.0.8)
|
||||||
activemodel (= 7.0.7.2)
|
activemodel (= 7.0.8)
|
||||||
activerecord (= 7.0.7.2)
|
activerecord (= 7.0.8)
|
||||||
activestorage (= 7.0.7.2)
|
activestorage (= 7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.0.7.2)
|
railties (= 7.0.8)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
actionview (>= 5.0.1.rc1)
|
actionview (>= 5.0.1.rc1)
|
||||||
|
@ -585,9 +585,9 @@ GEM
|
||||||
rails-i18n (7.0.7)
|
rails-i18n (7.0.7)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
railties (7.0.7.2)
|
railties (7.0.8)
|
||||||
actionpack (= 7.0.7.2)
|
actionpack (= 7.0.8)
|
||||||
activesupport (= 7.0.7.2)
|
activesupport (= 7.0.8)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
|
@ -641,7 +641,7 @@ GEM
|
||||||
sidekiq (>= 5, < 8)
|
sidekiq (>= 5, < 8)
|
||||||
rspec-support (3.12.1)
|
rspec-support (3.12.1)
|
||||||
rspec_chunked (0.6)
|
rspec_chunked (0.6)
|
||||||
rubocop (1.56.1)
|
rubocop (1.56.3)
|
||||||
base64 (~> 0.1.1)
|
base64 (~> 0.1.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
|
@ -732,7 +732,7 @@ GEM
|
||||||
net-ssh (>= 2.8.0)
|
net-ssh (>= 2.8.0)
|
||||||
stackprof (0.2.25)
|
stackprof (0.2.25)
|
||||||
statsd-ruby (1.5.0)
|
statsd-ruby (1.5.0)
|
||||||
stoplight (3.0.1)
|
stoplight (3.0.2)
|
||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
strong_migrations (0.8.0)
|
strong_migrations (0.8.0)
|
||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
|
@ -746,7 +746,7 @@ GEM
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
terrapin (0.6.0)
|
terrapin (0.6.0)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
test-prof (1.2.2)
|
test-prof (1.2.3)
|
||||||
thor (1.2.2)
|
thor (1.2.2)
|
||||||
tilt (2.2.0)
|
tilt (2.2.0)
|
||||||
timeout (0.4.0)
|
timeout (0.4.0)
|
||||||
|
@ -796,7 +796,7 @@ GEM
|
||||||
webfinger (1.2.0)
|
webfinger (1.2.0)
|
||||||
activesupport
|
activesupport
|
||||||
httpclient (>= 2.4)
|
httpclient (>= 2.4)
|
||||||
webmock (3.18.1)
|
webmock (3.19.1)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
|
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
|
||||||
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
|
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
|
||||||
stream: env PORT=4000 yarn run start
|
stream: env PORT=4000 yarn run start
|
||||||
webpack: env RAILS_ENV=development NODE_ENV=development NODE_OPTIONS=--openssl-legacy-provider ./bin/webpack-dev-server --listen-host 0.0.0.0
|
webpack: bin/webpack-dev-server
|
||||||
|
|
|
@ -14,8 +14,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | --------- |
|
| ------- | ---------------- |
|
||||||
| 4.1.x | Yes |
|
| 4.1.x | Yes |
|
||||||
| 4.0.x | Yes |
|
| 4.0.x | Until 2023-10-31 |
|
||||||
| 3.5.x | Yes |
|
| 3.5.x | Until 2023-12-31 |
|
||||||
| < 3.5 | No |
|
| < 3.5 | No |
|
||||||
|
|
3
Vagrantfile
vendored
3
Vagrantfile
vendored
|
@ -76,7 +76,8 @@ path.logs: /var/log/elasticsearch
|
||||||
network.host: 0.0.0.0
|
network.host: 0.0.0.0
|
||||||
http.port: 9200
|
http.port: 9200
|
||||||
discovery.seed_hosts: ["localhost"]
|
discovery.seed_hosts: ["localhost"]
|
||||||
cluster.initial_master_nodes: ["node-1"]' > /etc/elasticsearch/elasticsearch.yml
|
cluster.initial_master_nodes: ["node-1"]
|
||||||
|
xpack.security.enabled: false' > /etc/elasticsearch/elasticsearch.yml
|
||||||
|
|
||||||
sudo systemctl restart elasticsearch
|
sudo systemctl restart elasticsearch
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,13 @@ class AccountsIndex < Chewy::Index
|
||||||
|
|
||||||
analyzer: {
|
analyzer: {
|
||||||
natural: {
|
natural: {
|
||||||
tokenizer: 'uax_url_email',
|
tokenizer: 'standard',
|
||||||
filter: %w(
|
filter: %w(
|
||||||
english_possessive_stemmer
|
|
||||||
lowercase
|
lowercase
|
||||||
asciifolding
|
asciifolding
|
||||||
cjk_width
|
cjk_width
|
||||||
|
elision
|
||||||
|
english_possessive_stemmer
|
||||||
english_stop
|
english_stop
|
||||||
english_stemmer
|
english_stemmer
|
||||||
),
|
),
|
||||||
|
@ -62,6 +63,6 @@ class AccountsIndex < Chewy::Index
|
||||||
field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
|
field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
|
||||||
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
||||||
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
||||||
field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
|
field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
67
app/chewy/public_statuses_index.rb
Normal file
67
app/chewy/public_statuses_index.rb
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PublicStatusesIndex < Chewy::Index
|
||||||
|
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
||||||
|
filter: {
|
||||||
|
english_stop: {
|
||||||
|
type: 'stop',
|
||||||
|
stopwords: '_english_',
|
||||||
|
},
|
||||||
|
|
||||||
|
english_stemmer: {
|
||||||
|
type: 'stemmer',
|
||||||
|
language: 'english',
|
||||||
|
},
|
||||||
|
|
||||||
|
english_possessive_stemmer: {
|
||||||
|
type: 'stemmer',
|
||||||
|
language: 'possessive_english',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
analyzer: {
|
||||||
|
verbatim: {
|
||||||
|
tokenizer: 'uax_url_email',
|
||||||
|
filter: %w(lowercase),
|
||||||
|
},
|
||||||
|
|
||||||
|
content: {
|
||||||
|
tokenizer: 'standard',
|
||||||
|
filter: %w(
|
||||||
|
lowercase
|
||||||
|
asciifolding
|
||||||
|
cjk_width
|
||||||
|
elision
|
||||||
|
english_possessive_stemmer
|
||||||
|
english_stop
|
||||||
|
english_stemmer
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
hashtag: {
|
||||||
|
tokenizer: 'keyword',
|
||||||
|
filter: %w(
|
||||||
|
word_delimiter_graph
|
||||||
|
lowercase
|
||||||
|
asciifolding
|
||||||
|
cjk_width
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
index_scope ::Status.unscoped
|
||||||
|
.kept
|
||||||
|
.indexable
|
||||||
|
.includes(:media_attachments, :preloadable_poll, :preview_cards, :tags)
|
||||||
|
|
||||||
|
root date_detection: false do
|
||||||
|
field(:id, type: 'long')
|
||||||
|
field(:account_id, type: 'long')
|
||||||
|
field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
|
||||||
|
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
|
||||||
|
field(:language, type: 'keyword')
|
||||||
|
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
|
||||||
|
field(:created_at, type: 'date')
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,75 +1,65 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class StatusesIndex < Chewy::Index
|
class StatusesIndex < Chewy::Index
|
||||||
include FormattingHelper
|
|
||||||
|
|
||||||
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
||||||
filter: {
|
filter: {
|
||||||
english_stop: {
|
english_stop: {
|
||||||
type: 'stop',
|
type: 'stop',
|
||||||
stopwords: '_english_',
|
stopwords: '_english_',
|
||||||
},
|
},
|
||||||
|
|
||||||
english_stemmer: {
|
english_stemmer: {
|
||||||
type: 'stemmer',
|
type: 'stemmer',
|
||||||
language: 'english',
|
language: 'english',
|
||||||
},
|
},
|
||||||
|
|
||||||
english_possessive_stemmer: {
|
english_possessive_stemmer: {
|
||||||
type: 'stemmer',
|
type: 'stemmer',
|
||||||
language: 'possessive_english',
|
language: 'possessive_english',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
analyzer: {
|
analyzer: {
|
||||||
content: {
|
verbatim: {
|
||||||
tokenizer: 'uax_url_email',
|
tokenizer: 'uax_url_email',
|
||||||
|
filter: %w(lowercase),
|
||||||
|
},
|
||||||
|
|
||||||
|
content: {
|
||||||
|
tokenizer: 'standard',
|
||||||
filter: %w(
|
filter: %w(
|
||||||
english_possessive_stemmer
|
|
||||||
lowercase
|
lowercase
|
||||||
asciifolding
|
asciifolding
|
||||||
cjk_width
|
cjk_width
|
||||||
|
elision
|
||||||
|
english_possessive_stemmer
|
||||||
english_stop
|
english_stop
|
||||||
english_stemmer
|
english_stemmer
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hashtag: {
|
||||||
|
tokenizer: 'keyword',
|
||||||
|
filter: %w(
|
||||||
|
word_delimiter_graph
|
||||||
|
lowercase
|
||||||
|
asciifolding
|
||||||
|
cjk_width
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# We do not use delete_if option here because it would call a method that we
|
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preview_cards, :local_mentioned, :local_favorited, :local_reblogged, :local_bookmarked, :tags, preloadable_poll: :local_voters), delete_if: ->(status) { status.searchable_by.empty? }
|
||||||
# expect to be called with crutches without crutches, causing n+1 queries
|
|
||||||
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
|
|
||||||
|
|
||||||
crutch :mentions do |collection|
|
|
||||||
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
|
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
|
||||||
end
|
|
||||||
|
|
||||||
crutch :favourites do |collection|
|
|
||||||
data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
|
||||||
end
|
|
||||||
|
|
||||||
crutch :reblogs do |collection|
|
|
||||||
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
|
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
|
||||||
end
|
|
||||||
|
|
||||||
crutch :bookmarks do |collection|
|
|
||||||
data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
|
||||||
end
|
|
||||||
|
|
||||||
crutch :votes do |collection|
|
|
||||||
data = ::PollVote.joins(:poll).where(poll: { status_id: collection.map(&:id) }).where(account: Account.local).pluck(:status_id, :account_id)
|
|
||||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
|
||||||
end
|
|
||||||
|
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
field :id, type: 'long'
|
field(:id, type: 'long')
|
||||||
field :account_id, type: 'long'
|
field(:account_id, type: 'long')
|
||||||
|
field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
|
||||||
field :text, type: 'text', value: ->(status) { status.searchable_text } do
|
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
|
||||||
field :stemmed, type: 'text', analyzer: 'content'
|
field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by })
|
||||||
end
|
field(:language, type: 'keyword')
|
||||||
|
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
|
||||||
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
|
field(:created_at, type: 'date')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,12 +5,21 @@ class TagsIndex < Chewy::Index
|
||||||
analyzer: {
|
analyzer: {
|
||||||
content: {
|
content: {
|
||||||
tokenizer: 'keyword',
|
tokenizer: 'keyword',
|
||||||
filter: %w(lowercase asciifolding cjk_width),
|
filter: %w(
|
||||||
|
word_delimiter_graph
|
||||||
|
lowercase
|
||||||
|
asciifolding
|
||||||
|
cjk_width
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
edge_ngram: {
|
edge_ngram: {
|
||||||
tokenizer: 'edge_ngram',
|
tokenizer: 'edge_ngram',
|
||||||
filter: %w(lowercase asciifolding cjk_width),
|
filter: %w(
|
||||||
|
lowercase
|
||||||
|
asciifolding
|
||||||
|
cjk_width
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -30,12 +39,9 @@ class TagsIndex < Chewy::Index
|
||||||
end
|
end
|
||||||
|
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
field :name, type: 'text', analyzer: 'content' do
|
field(:name, type: 'text', analyzer: 'content', value: :display_name) { field(:edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content') }
|
||||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
field(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? })
|
||||||
end
|
field(:usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts })
|
||||||
|
field(:last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at })
|
||||||
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
|
||||||
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
|
|
||||||
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
18
app/controllers/admin/software_updates_controller.rb
Normal file
18
app/controllers/admin/software_updates_controller.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class SoftwareUpdatesController < BaseController
|
||||||
|
before_action :check_enabled!
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :software_update, :index?
|
||||||
|
@software_updates = SoftwareUpdate.all.sort_by(&:gem_version)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_enabled!
|
||||||
|
not_found unless SoftwareUpdate.check_enabled?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -30,6 +30,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
||||||
:bot,
|
:bot,
|
||||||
:discoverable,
|
:discoverable,
|
||||||
:hide_collections,
|
:hide_collections,
|
||||||
|
:indexable,
|
||||||
fields_attributes: [:name, :value]
|
fields_attributes: [:name, :value]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
74
app/controllers/api/v1/admin/tags_controller.rb
Normal file
74
app/controllers/api/v1/admin/tags_controller.rb
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::TagsController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show]
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:write' }, only: :update
|
||||||
|
|
||||||
|
before_action :set_tags, only: :index
|
||||||
|
before_action :set_tag, except: :index
|
||||||
|
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
after_action :verify_authorized
|
||||||
|
|
||||||
|
LIMIT = 100
|
||||||
|
PAGINATION_PARAMS = %i(limit).freeze
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :tag, :index?
|
||||||
|
render json: @tags, each_serializer: REST::Admin::TagSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize @tag, :show?
|
||||||
|
render json: @tag, serializer: REST::Admin::TagSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @tag, :update?
|
||||||
|
@tag.update!(tag_params.merge(reviewed_at: Time.now.utc))
|
||||||
|
render json: @tag, serializer: REST::Admin::TagSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_tag
|
||||||
|
@tag = Tag.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_tags
|
||||||
|
@tags = Tag.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_params
|
||||||
|
params.permit(:display_name, :trendable, :usable, :listable)
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_admin_tags_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@tags.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@tags.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@tags.size == limit_param(LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
|
||||||
|
end
|
||||||
|
end
|
|
@ -16,8 +16,10 @@ class Api::V1::DirectoriesController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_accounts
|
def set_accounts
|
||||||
|
with_read_replica do
|
||||||
@accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
@accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def accounts_scope
|
def accounts_scope
|
||||||
Account.discoverable.tap do |scope|
|
Account.discoverable.tap do |scope|
|
||||||
|
|
|
@ -41,5 +41,7 @@ class Api::V1::Peers::SearchController < Api::BaseController
|
||||||
domain = TagManager.instance.normalize_domain(domain)
|
domain = TagManager.instance.normalize_domain(domain)
|
||||||
@domains = Instance.searchable.where(Instance.arel_table[:domain].matches("#{Instance.sanitize_sql_like(domain)}%", false, true)).limit(10).pluck(:domain)
|
@domains = Instance.searchable.where(Instance.arel_table[:domain].matches("#{Instance.sanitize_sql_like(domain)}%", false, true)).limit(10).pluck(:domain)
|
||||||
end
|
end
|
||||||
|
rescue Addressable::URI::InvalidURIError
|
||||||
|
@domains = []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,15 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
|
||||||
before_action :set_translation
|
before_action :set_translation
|
||||||
|
|
||||||
rescue_from TranslationService::NotConfiguredError, with: :not_found
|
rescue_from TranslationService::NotConfiguredError, with: :not_found
|
||||||
rescue_from TranslationService::UnexpectedResponseError, TranslationService::QuotaExceededError, TranslationService::TooManyRequestsError, with: :service_unavailable
|
rescue_from TranslationService::UnexpectedResponseError, with: :service_unavailable
|
||||||
|
|
||||||
|
rescue_from TranslationService::QuotaExceededError do
|
||||||
|
render json: { error: I18n.t('translation.errors.quota_exceeded') }, status: 503
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from TranslationService::TooManyRequestsError do
|
||||||
|
render json: { error: I18n.t('translation.errors.too_many_requests') }, status: 503
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
render json: @translation, serializer: REST::TranslationSerializer
|
render json: @translation, serializer: REST::TranslationSerializer
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Timelines::TagController < Api::BaseController
|
class Api::V1::Timelines::TagController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
|
||||||
before_action :load_tag
|
before_action :load_tag
|
||||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
|
@ -12,6 +13,10 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def require_auth?
|
||||||
|
!Setting.timeline_preview
|
||||||
|
end
|
||||||
|
|
||||||
def load_tag
|
def load_tag
|
||||||
@tag = Tag.find_normalized(params[:id])
|
@tag = Tag.find_normalized(params[:id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base
|
||||||
include DomainControlHelper
|
include DomainControlHelper
|
||||||
include ThemingConcern
|
include ThemingConcern
|
||||||
include DatabaseHelper
|
include DatabaseHelper
|
||||||
|
include AuthorizedFetchHelper
|
||||||
|
|
||||||
helper_method :current_account
|
helper_method :current_account
|
||||||
helper_method :current_session
|
helper_method :current_session
|
||||||
|
@ -53,10 +54,6 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authorized_fetch_mode?
|
|
||||||
ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.limited_federation_mode
|
|
||||||
end
|
|
||||||
|
|
||||||
def public_fetch_mode?
|
def public_fetch_mode?
|
||||||
!authorized_fetch_mode?
|
!authorized_fetch_mode?
|
||||||
end
|
end
|
||||||
|
|
|
@ -119,6 +119,8 @@ module SignatureVerification
|
||||||
private
|
private
|
||||||
|
|
||||||
def fail_with!(message, **options)
|
def fail_with!(message, **options)
|
||||||
|
Rails.logger.debug { "Signature verification failed: #{message}" }
|
||||||
|
|
||||||
@signature_verification_failure_reason = { error: message }.merge(options)
|
@signature_verification_failure_reason = { error: message }.merge(options)
|
||||||
@signed_request_actor = nil
|
@signed_request_actor = nil
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,7 @@ module WebAppControllerConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
def skip_csrf_meta_tags?
|
def skip_csrf_meta_tags?
|
||||||
!(ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil?
|
!(ENV['ONE_CLICK_SSO_LOGIN'] == 'true' && ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_app_body_class
|
def set_app_body_class
|
||||||
|
|
|
@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.require(:account).permit(:discoverable, :unlocked, :show_collections, settings: UserSettings.keys)
|
params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
|
|
11
app/helpers/authorized_fetch_helper.rb
Normal file
11
app/helpers/authorized_fetch_helper.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module AuthorizedFetchHelper
|
||||||
|
def authorized_fetch_mode?
|
||||||
|
ENV.fetch('AUTHORIZED_FETCH') { Setting.authorized_fetch } == 'true' || Rails.configuration.x.limited_federation_mode
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorized_fetch_overridden?
|
||||||
|
ENV.key?('AUTHORIZED_FETCH') || Rails.configuration.x.limited_federation_mode
|
||||||
|
end
|
||||||
|
end
|
|
@ -193,7 +193,6 @@ module LanguagesHelper
|
||||||
cnr: ['Montenegrin', 'crnogorski'].freeze,
|
cnr: ['Montenegrin', 'crnogorski'].freeze,
|
||||||
jbo: ['Lojban', 'la .lojban.'].freeze,
|
jbo: ['Lojban', 'la .lojban.'].freeze,
|
||||||
kab: ['Kabyle', 'Taqbaylit'].freeze,
|
kab: ['Kabyle', 'Taqbaylit'].freeze,
|
||||||
kmr: ['Kurmanji (Kurdish)', 'Kurmancî'].freeze,
|
|
||||||
ldn: ['Láadan', 'Láadan'].freeze,
|
ldn: ['Láadan', 'Láadan'].freeze,
|
||||||
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
|
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
|
||||||
sco: ['Scots', 'Scots'].freeze,
|
sco: ['Scots', 'Scots'].freeze,
|
||||||
|
|
|
@ -14,6 +14,7 @@ module MediaComponentHelper
|
||||||
blurhash: video.blurhash,
|
blurhash: video.blurhash,
|
||||||
frameRate: meta.dig('original', 'frame_rate'),
|
frameRate: meta.dig('original', 'frame_rate'),
|
||||||
inline: true,
|
inline: true,
|
||||||
|
aspectRatio: "#{meta.dig('original', 'width')} / #{meta.dig('original', 'height')}",
|
||||||
media: [
|
media: [
|
||||||
serialize_media_attachment(video),
|
serialize_media_attachment(video),
|
||||||
].as_json,
|
].as_json,
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
// This file will be loaded on public pages, regardless of theme.
|
|
||||||
|
|
||||||
import 'packs/public-path';
|
|
||||||
|
|
||||||
import { delegate } from '@rails/ujs';
|
|
||||||
|
|
||||||
const getProfileAvatarAnimationHandler = (swapTo) => {
|
|
||||||
//animate avatar gifs on the profile page when moused over
|
|
||||||
return ({ target }) => {
|
|
||||||
const swapSrc = target.getAttribute(swapTo);
|
|
||||||
//only change the img source if autoplay is off and the image src is actually different
|
|
||||||
if(target.getAttribute('data-autoplay') !== 'true' && target.src !== swapSrc) {
|
|
||||||
target.src = swapSrc;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
delegate(document, 'img#profile_page_avatar', 'mouseover', getProfileAvatarAnimationHandler('data-original'));
|
|
||||||
|
|
||||||
delegate(document, 'img#profile_page_avatar', 'mouseout', getProfileAvatarAnimationHandler('data-static'));
|
|
||||||
|
|
||||||
delegate(document, '#account_header', 'change', ({ target }) => {
|
|
||||||
const header = document.querySelector('.card .card__img img');
|
|
||||||
const [file] = target.files || [];
|
|
||||||
const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
|
|
||||||
|
|
||||||
header.src = url;
|
|
||||||
});
|
|
|
@ -140,7 +140,9 @@ const fromAcct = (acct: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInteractionURL = (uri_or_domain: string) => {
|
const fetchInteractionURL = (uri_or_domain: string) => {
|
||||||
if (/^https?:\/\//.test(uri_or_domain)) {
|
if (uri_or_domain === '') {
|
||||||
|
fetchInteractionURLFailure();
|
||||||
|
} else if (/^https?:\/\//.test(uri_or_domain)) {
|
||||||
fromURL(uri_or_domain);
|
fromURL(uri_or_domain);
|
||||||
} else if (uri_or_domain.includes('@')) {
|
} else if (uri_or_domain.includes('@')) {
|
||||||
fromAcct(uri_or_domain);
|
fromAcct(uri_or_domain);
|
||||||
|
|
|
@ -2,21 +2,6 @@
|
||||||
|
|
||||||
import 'packs/public-path';
|
import 'packs/public-path';
|
||||||
import { delegate } from '@rails/ujs';
|
import { delegate } from '@rails/ujs';
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
|
||||||
|
|
||||||
|
|
||||||
import emojify from '../mastodon/features/emoji/emoji';
|
|
||||||
|
|
||||||
delegate(document, '#account_display_name', 'input', ({ target }) => {
|
|
||||||
const name = document.querySelector('.card .display-name strong');
|
|
||||||
if (name) {
|
|
||||||
if (target.value) {
|
|
||||||
name.innerHTML = emojify(escapeTextContentForBrowser(target.value));
|
|
||||||
} else {
|
|
||||||
name.textContent = name.textContent = target.dataset.default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
|
delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
|
||||||
const avatar = document.getElementById(target.id + '-preview');
|
const avatar = document.getElementById(target.id + '-preview');
|
||||||
|
@ -26,18 +11,6 @@ delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
|
||||||
avatar.src = url;
|
avatar.src = url;
|
||||||
});
|
});
|
||||||
|
|
||||||
delegate(document, '#account_locked', 'change', ({ target }) => {
|
|
||||||
const lock = document.querySelector('.card .display-name i');
|
|
||||||
|
|
||||||
if (lock) {
|
|
||||||
if (target.checked) {
|
|
||||||
delete lock.dataset.hidden;
|
|
||||||
} else {
|
|
||||||
lock.dataset.hidden = 'true';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
delegate(document, '.input-copy input', 'click', ({ target }) => {
|
delegate(document, '.input-copy input', 'click', ({ target }) => {
|
||||||
target.focus();
|
target.focus();
|
||||||
target.select();
|
target.select();
|
||||||
|
|
|
@ -13,8 +13,8 @@ pack:
|
||||||
mailer:
|
mailer:
|
||||||
filename: mailer.js
|
filename: mailer.js
|
||||||
stylesheet: true
|
stylesheet: true
|
||||||
modal: public.js
|
modal:
|
||||||
public: public.js
|
public:
|
||||||
settings: settings.js
|
settings: settings.js
|
||||||
sign_up:
|
sign_up:
|
||||||
share:
|
share:
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import api from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
import { fetchRelationships } from './accounts';
|
||||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||||
|
|
||||||
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||||
export const REBLOG_FAIL = 'REBLOG_FAIL';
|
export const REBLOG_FAIL = 'REBLOG_FAIL';
|
||||||
|
|
||||||
|
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||||
|
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||||
|
export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
|
||||||
|
|
||||||
export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
||||||
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
||||||
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
||||||
|
@ -26,6 +31,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
|
||||||
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
||||||
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST';
|
||||||
|
export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
|
||||||
|
export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
|
||||||
|
|
||||||
export const PIN_REQUEST = 'PIN_REQUEST';
|
export const PIN_REQUEST = 'PIN_REQUEST';
|
||||||
export const PIN_SUCCESS = 'PIN_SUCCESS';
|
export const PIN_SUCCESS = 'PIN_SUCCESS';
|
||||||
export const PIN_FAIL = 'PIN_FAIL';
|
export const PIN_FAIL = 'PIN_FAIL';
|
||||||
|
@ -259,8 +268,10 @@ export function fetchReblogs(id) {
|
||||||
dispatch(fetchReblogsRequest(id));
|
dispatch(fetchReblogsRequest(id));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(importFetchedAccounts(response.data));
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(fetchReblogsSuccess(id, response.data));
|
dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchReblogsFail(id, error));
|
dispatch(fetchReblogsFail(id, error));
|
||||||
});
|
});
|
||||||
|
@ -274,17 +285,62 @@ export function fetchReblogsRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchReblogsSuccess(id, accounts) {
|
export function fetchReblogsSuccess(id, accounts, next) {
|
||||||
return {
|
return {
|
||||||
type: REBLOGS_FETCH_SUCCESS,
|
type: REBLOGS_FETCH_SUCCESS,
|
||||||
id,
|
id,
|
||||||
accounts,
|
accounts,
|
||||||
|
next,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchReblogsFail(id, error) {
|
export function fetchReblogsFail(id, error) {
|
||||||
return {
|
return {
|
||||||
type: REBLOGS_FETCH_FAIL,
|
type: REBLOGS_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReblogs(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['user_lists', 'reblogged_by', id, 'next']);
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandReblogsRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
|
}).catch(error => dispatch(expandReblogsFail(id, error)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReblogsRequest(id) {
|
||||||
|
return {
|
||||||
|
type: REBLOGS_EXPAND_REQUEST,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReblogsSuccess(id, accounts, next) {
|
||||||
|
return {
|
||||||
|
type: REBLOGS_EXPAND_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReblogsFail(id, error) {
|
||||||
|
return {
|
||||||
|
type: REBLOGS_EXPAND_FAIL,
|
||||||
|
id,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -294,8 +350,10 @@ export function fetchFavourites(id) {
|
||||||
dispatch(fetchFavouritesRequest(id));
|
dispatch(fetchFavouritesRequest(id));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(importFetchedAccounts(response.data));
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(fetchFavouritesSuccess(id, response.data));
|
dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchFavouritesFail(id, error));
|
dispatch(fetchFavouritesFail(id, error));
|
||||||
});
|
});
|
||||||
|
@ -309,17 +367,62 @@ export function fetchFavouritesRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchFavouritesSuccess(id, accounts) {
|
export function fetchFavouritesSuccess(id, accounts, next) {
|
||||||
return {
|
return {
|
||||||
type: FAVOURITES_FETCH_SUCCESS,
|
type: FAVOURITES_FETCH_SUCCESS,
|
||||||
id,
|
id,
|
||||||
accounts,
|
accounts,
|
||||||
|
next,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchFavouritesFail(id, error) {
|
export function fetchFavouritesFail(id, error) {
|
||||||
return {
|
return {
|
||||||
type: FAVOURITES_FETCH_FAIL,
|
type: FAVOURITES_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandFavourites(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['user_lists', 'favourited_by', id, 'next']);
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandFavouritesRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
|
}).catch(error => dispatch(expandFavouritesFail(id, error)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandFavouritesRequest(id) {
|
||||||
|
return {
|
||||||
|
type: FAVOURITES_EXPAND_REQUEST,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandFavouritesSuccess(id, accounts, next) {
|
||||||
|
return {
|
||||||
|
type: FAVOURITES_EXPAND_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandFavouritesFail(id, error) {
|
||||||
|
return {
|
||||||
|
type: FAVOURITES_EXPAND_FAIL,
|
||||||
|
id,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
importFetchedStatuses,
|
importFetchedStatuses,
|
||||||
} from './importer';
|
} from './importer';
|
||||||
import { submitMarkers } from './markers';
|
import { submitMarkers } from './markers';
|
||||||
|
import { register as registerPushNotifications } from './push_notifications';
|
||||||
import { saveSettings } from './settings';
|
import { saveSettings } from './settings';
|
||||||
|
|
||||||
|
|
||||||
|
@ -384,6 +385,10 @@ export function requestBrowserPermission(callback = noOp) {
|
||||||
requestNotificationPermission((permission) => {
|
requestNotificationPermission((permission) => {
|
||||||
dispatch(setBrowserPermission(permission));
|
dispatch(setBrowserPermission(permission));
|
||||||
callback(permission);
|
callback(permission);
|
||||||
|
|
||||||
|
if (permission === 'granted') {
|
||||||
|
dispatch(registerPushNotifications());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import { fromJS } from 'immutable';
|
||||||
|
|
||||||
|
import { searchHistory } from 'flavours/glitch/settings';
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
|
@ -15,8 +19,7 @@ export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
|
||||||
export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
|
export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
|
||||||
export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
|
export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
|
||||||
|
|
||||||
export const SEARCH_RESULT_CLICK = 'SEARCH_RESULT_CLICK';
|
export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE';
|
||||||
export const SEARCH_RESULT_FORGET = 'SEARCH_RESULT_FORGET';
|
|
||||||
|
|
||||||
export function changeSearch(value) {
|
export function changeSearch(value) {
|
||||||
return {
|
return {
|
||||||
|
@ -37,17 +40,17 @@ export function submitSearch(type) {
|
||||||
const signedIn = !!getState().getIn(['meta', 'me']);
|
const signedIn = !!getState().getIn(['meta', 'me']);
|
||||||
|
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, ''));
|
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(fetchSearchRequest());
|
dispatch(fetchSearchRequest(type));
|
||||||
|
|
||||||
api(getState).get('/api/v2/search', {
|
api(getState).get('/api/v2/search', {
|
||||||
params: {
|
params: {
|
||||||
q: value,
|
q: value,
|
||||||
resolve: signedIn,
|
resolve: signedIn,
|
||||||
limit: 10,
|
limit: 11,
|
||||||
type,
|
type,
|
||||||
},
|
},
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
|
@ -59,7 +62,7 @@ export function submitSearch(type) {
|
||||||
dispatch(importFetchedStatuses(response.data.statuses));
|
dispatch(importFetchedStatuses(response.data.statuses));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(fetchSearchSuccess(response.data, value));
|
dispatch(fetchSearchSuccess(response.data, value, type));
|
||||||
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchSearchFail(error));
|
dispatch(fetchSearchFail(error));
|
||||||
|
@ -67,16 +70,18 @@ export function submitSearch(type) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchSearchRequest() {
|
export function fetchSearchRequest(searchType) {
|
||||||
return {
|
return {
|
||||||
type: SEARCH_FETCH_REQUEST,
|
type: SEARCH_FETCH_REQUEST,
|
||||||
|
searchType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchSearchSuccess(results, searchTerm) {
|
export function fetchSearchSuccess(results, searchTerm, searchType) {
|
||||||
return {
|
return {
|
||||||
type: SEARCH_FETCH_SUCCESS,
|
type: SEARCH_FETCH_SUCCESS,
|
||||||
results,
|
results,
|
||||||
|
searchType,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -90,15 +95,16 @@ export function fetchSearchFail(error) {
|
||||||
|
|
||||||
export const expandSearch = type => (dispatch, getState) => {
|
export const expandSearch = type => (dispatch, getState) => {
|
||||||
const value = getState().getIn(['search', 'value']);
|
const value = getState().getIn(['search', 'value']);
|
||||||
const offset = getState().getIn(['search', 'results', type]).size;
|
const offset = getState().getIn(['search', 'results', type]).size - 1;
|
||||||
|
|
||||||
dispatch(expandSearchRequest());
|
dispatch(expandSearchRequest(type));
|
||||||
|
|
||||||
api(getState).get('/api/v2/search', {
|
api(getState).get('/api/v2/search', {
|
||||||
params: {
|
params: {
|
||||||
q: value,
|
q: value,
|
||||||
type,
|
type,
|
||||||
offset,
|
offset,
|
||||||
|
limit: 11,
|
||||||
},
|
},
|
||||||
}).then(({ data }) => {
|
}).then(({ data }) => {
|
||||||
if (data.accounts) {
|
if (data.accounts) {
|
||||||
|
@ -116,8 +122,9 @@ export const expandSearch = type => (dispatch, getState) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expandSearchRequest = () => ({
|
export const expandSearchRequest = (searchType) => ({
|
||||||
type: SEARCH_EXPAND_REQUEST,
|
type: SEARCH_EXPAND_REQUEST,
|
||||||
|
searchType,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const expandSearchSuccess = (results, searchTerm, searchType) => ({
|
export const expandSearchSuccess = (results, searchTerm, searchType) => ({
|
||||||
|
@ -161,16 +168,34 @@ export const openURL = routerHistory => (dispatch, getState) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clickSearchResult = (q, type) => ({
|
export const clickSearchResult = (q, type) => (dispatch, getState) => {
|
||||||
type: SEARCH_RESULT_CLICK,
|
const previous = getState().getIn(['search', 'recent']);
|
||||||
|
const me = getState().getIn(['meta', 'me']);
|
||||||
|
const current = previous.add(fromJS({ type, q })).takeLast(4);
|
||||||
|
|
||||||
result: {
|
searchHistory.set(me, current.toJS());
|
||||||
type,
|
dispatch(updateSearchHistory(current));
|
||||||
q,
|
};
|
||||||
},
|
|
||||||
|
export const forgetSearchResult = q => (dispatch, getState) => {
|
||||||
|
const previous = getState().getIn(['search', 'recent']);
|
||||||
|
const me = getState().getIn(['meta', 'me']);
|
||||||
|
const current = previous.filterNot(result => result.get('q') === q);
|
||||||
|
|
||||||
|
searchHistory.set(me, current.toJS());
|
||||||
|
dispatch(updateSearchHistory(current));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSearchHistory = recent => ({
|
||||||
|
type: SEARCH_HISTORY_UPDATE,
|
||||||
|
recent,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const forgetSearchResult = q => ({
|
export const hydrateSearch = () => (dispatch, getState) => {
|
||||||
type: SEARCH_RESULT_FORGET,
|
const me = getState().getIn(['meta', 'me']);
|
||||||
q,
|
const history = searchHistory.get(me);
|
||||||
});
|
|
||||||
|
if (history !== null) {
|
||||||
|
dispatch(updateSearchHistory(history));
|
||||||
|
}
|
||||||
|
};
|
|
@ -2,6 +2,7 @@ import { Iterable, fromJS } from 'immutable';
|
||||||
|
|
||||||
import { hydrateCompose } from './compose';
|
import { hydrateCompose } from './compose';
|
||||||
import { importFetchedAccounts } from './importer';
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { hydrateSearch } from './search';
|
||||||
import { saveSettings } from './settings';
|
import { saveSettings } from './settings';
|
||||||
|
|
||||||
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
||||||
|
@ -34,6 +35,7 @@ export function hydrateStore(rawState) {
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(hydrateCompose());
|
dispatch(hydrateCompose());
|
||||||
|
dispatch(hydrateSearch());
|
||||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||||
dispatch(saveSettings());
|
dispatch(saveSettings());
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,8 +33,6 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='dismissable-banner'>
|
<div className='dismissable-banner'>
|
||||||
<div className='dismissable-banner__message'>{children}</div>
|
|
||||||
|
|
||||||
<div className='dismissable-banner__action'>
|
<div className='dismissable-banner__action'>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon='times'
|
icon='times'
|
||||||
|
@ -42,6 +40,8 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
||||||
onClick={handleDismiss}
|
onClick={handleDismiss}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='dismissable-banner__message'>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -793,6 +793,7 @@ class Status extends ImmutablePureComponent {
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
data-featured={featured ? 'true' : null}
|
data-featured={featured ? 'true' : null}
|
||||||
aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
|
aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
|
||||||
|
data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}
|
||||||
>
|
>
|
||||||
{!muted && prepend}
|
{!muted && prepend}
|
||||||
|
|
||||||
|
|
|
@ -212,11 +212,11 @@ class Audio extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleMute = () => {
|
toggleMute = () => {
|
||||||
const muted = !this.state.muted;
|
const muted = !(this.state.muted || this.state.volume === 0);
|
||||||
|
|
||||||
this.setState({ muted }, () => {
|
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
|
||||||
if (this.gainNode) {
|
if (this.gainNode) {
|
||||||
this.gainNode.gain.value = muted ? 0 : this.state.volume;
|
this.gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -294,7 +294,7 @@ class Audio extends PureComponent {
|
||||||
const { x } = getPointerPosition(this.volume, e);
|
const { x } = getPointerPosition(this.volume, e);
|
||||||
|
|
||||||
if(!isNaN(x)) {
|
if(!isNaN(x)) {
|
||||||
this.setState({ volume: x }, () => {
|
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
|
||||||
if (this.gainNode) {
|
if (this.gainNode) {
|
||||||
this.gainNode.gain.value = this.state.muted ? 0 : x;
|
this.gainNode.gain.value = this.state.muted ? 0 : x;
|
||||||
}
|
}
|
||||||
|
@ -473,8 +473,9 @@ class Audio extends PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props;
|
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props;
|
||||||
const { paused, muted, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
|
const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
|
||||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||||
|
const muted = this.state.muted || volume === 0;
|
||||||
|
|
||||||
let warning;
|
let warning;
|
||||||
|
|
||||||
|
@ -564,12 +565,12 @@ class Audio extends PureComponent {
|
||||||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||||
|
|
||||||
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
|
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
|
||||||
<div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} />
|
<div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }} />
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className='video-player__volume__handle'
|
className='video-player__volume__handle'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={{ left: `${volume * 100}%`, backgroundColor: this._getAccentColor() }}
|
style={{ left: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import {
|
import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
|
||||||
injectIntl,
|
|
||||||
FormattedMessage,
|
|
||||||
defineMessages,
|
|
||||||
} from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -13,7 +9,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { searchEnabled } from 'flavours/glitch/initial_state';
|
import { domain, searchEnabled } from 'flavours/glitch/initial_state';
|
||||||
import { focusRoot } from 'flavours/glitch/utils/dom_helpers';
|
import { focusRoot } from 'flavours/glitch/utils/dom_helpers';
|
||||||
import { HASHTAG_REGEX } from 'flavours/glitch/utils/hashtags';
|
import { HASHTAG_REGEX } from 'flavours/glitch/utils/hashtags';
|
||||||
|
|
||||||
|
@ -22,7 +18,17 @@ const messages = defineMessages({
|
||||||
placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' },
|
placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// The component.
|
const labelForRecentSearch = search => {
|
||||||
|
switch(search.get('type')) {
|
||||||
|
case 'account':
|
||||||
|
return `@${search.get('q')}`;
|
||||||
|
case 'hashtag':
|
||||||
|
return `#${search.get('q')}`;
|
||||||
|
default:
|
||||||
|
return search.get('q');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class Search extends PureComponent {
|
class Search extends PureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
@ -52,6 +58,17 @@ class Search extends PureComponent {
|
||||||
options: [],
|
options: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
defaultOptions = [
|
||||||
|
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:') } },
|
||||||
|
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:') } },
|
||||||
|
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:') } },
|
||||||
|
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:') } },
|
||||||
|
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:') } },
|
||||||
|
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:') } },
|
||||||
|
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:') } },
|
||||||
|
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:') } }
|
||||||
|
];
|
||||||
|
|
||||||
setRef = c => {
|
setRef = c => {
|
||||||
this.searchForm = c;
|
this.searchForm = c;
|
||||||
};
|
};
|
||||||
|
@ -100,7 +117,7 @@ class Search extends PureComponent {
|
||||||
|
|
||||||
handleKeyDown = (e) => {
|
handleKeyDown = (e) => {
|
||||||
const { selectedOption } = this.state;
|
const { selectedOption } = this.state;
|
||||||
const options = this._getOptions();
|
const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
|
||||||
|
|
||||||
switch(e.key) {
|
switch(e.key) {
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
|
@ -131,10 +148,9 @@ class Search extends PureComponent {
|
||||||
if (selectedOption === -1) {
|
if (selectedOption === -1) {
|
||||||
this._submit();
|
this._submit();
|
||||||
} else if (options.length > 0) {
|
} else if (options.length > 0) {
|
||||||
options[selectedOption].action();
|
options[selectedOption].action(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._unfocus();
|
|
||||||
break;
|
break;
|
||||||
case 'Delete':
|
case 'Delete':
|
||||||
if (selectedOption > -1 && options.length > 0) {
|
if (selectedOption > -1 && options.length > 0) {
|
||||||
|
@ -161,6 +177,7 @@ class Search extends PureComponent {
|
||||||
|
|
||||||
router.history.push(`/tags/${query}`);
|
router.history.push(`/tags/${query}`);
|
||||||
onClickSearchResult(query, 'hashtag');
|
onClickSearchResult(query, 'hashtag');
|
||||||
|
this._unfocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleAccountClick = () => {
|
handleAccountClick = () => {
|
||||||
|
@ -171,6 +188,7 @@ class Search extends PureComponent {
|
||||||
|
|
||||||
router.history.push(`/@${query}`);
|
router.history.push(`/@${query}`);
|
||||||
onClickSearchResult(query, 'account');
|
onClickSearchResult(query, 'account');
|
||||||
|
this._unfocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleURLClick = () => {
|
handleURLClick = () => {
|
||||||
|
@ -178,6 +196,7 @@ class Search extends PureComponent {
|
||||||
const { onOpenURL } = this.props;
|
const { onOpenURL } = this.props;
|
||||||
|
|
||||||
onOpenURL(router.history);
|
onOpenURL(router.history);
|
||||||
|
this._unfocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleStatusSearch = () => {
|
handleStatusSearch = () => {
|
||||||
|
@ -189,13 +208,19 @@ class Search extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleRecentSearchClick = search => {
|
handleRecentSearchClick = search => {
|
||||||
|
const { onChange } = this.props;
|
||||||
const { router } = this.context;
|
const { router } = this.context;
|
||||||
|
|
||||||
if (search.get('type') === 'account') {
|
if (search.get('type') === 'account') {
|
||||||
router.history.push(`/@${search.get('q')}`);
|
router.history.push(`/@${search.get('q')}`);
|
||||||
} else if (search.get('type') === 'hashtag') {
|
} else if (search.get('type') === 'hashtag') {
|
||||||
router.history.push(`/tags/${search.get('q')}`);
|
router.history.push(`/tags/${search.get('q')}`);
|
||||||
|
} else {
|
||||||
|
onChange(search.get('q'));
|
||||||
|
this._submit(search.get('type'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._unfocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleForgetRecentSearchClick = search => {
|
handleForgetRecentSearchClick = search => {
|
||||||
|
@ -208,15 +233,33 @@ class Search extends PureComponent {
|
||||||
document.querySelector('.ui').parentElement.focus();
|
document.querySelector('.ui').parentElement.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_insertText (text) {
|
||||||
|
const { value, onChange } = this.props;
|
||||||
|
|
||||||
|
if (value === '') {
|
||||||
|
onChange(text);
|
||||||
|
} else if (value[value.length - 1] === ' ') {
|
||||||
|
onChange(`${value}${text}`);
|
||||||
|
} else {
|
||||||
|
onChange(`${value} ${text}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_submit (type) {
|
_submit (type) {
|
||||||
const { onSubmit, openInRoute } = this.props;
|
const { onSubmit, openInRoute, value, onClickSearchResult } = this.props;
|
||||||
const { router } = this.context;
|
const { router } = this.context;
|
||||||
|
|
||||||
onSubmit(type);
|
onSubmit(type);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
onClickSearchResult(value, type);
|
||||||
|
}
|
||||||
|
|
||||||
if (openInRoute) {
|
if (openInRoute) {
|
||||||
router.history.push('/search');
|
router.history.push('/search');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._unfocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
_getOptions () {
|
_getOptions () {
|
||||||
|
@ -229,7 +272,7 @@ class Search extends PureComponent {
|
||||||
const { recent } = this.props;
|
const { recent } = this.props;
|
||||||
|
|
||||||
return recent.toArray().map(search => ({
|
return recent.toArray().map(search => ({
|
||||||
label: search.get('type') === 'account' ? `@${search.get('q')}` : `#${search.get('q')}`,
|
label: labelForRecentSearch(search),
|
||||||
|
|
||||||
action: () => this.handleRecentSearchClick(search),
|
action: () => this.handleRecentSearchClick(search),
|
||||||
|
|
||||||
|
@ -337,6 +380,22 @@ class Search extends PureComponent {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
|
||||||
|
|
||||||
|
{searchEnabled ? (
|
||||||
|
<div className='search__popout__menu'>
|
||||||
|
{this.defaultOptions.map(({ key, label, action }, i) => (
|
||||||
|
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === ((options.length || recent.size) + i) })}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='search__popout__menu__message'>
|
||||||
|
<FormattedMessage id='search_popout.full_text_search_disabled_message' defaultMessage='Not available on {domain}.' values={{ domain }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
@ -10,36 +10,26 @@ import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { LoadMore } from 'flavours/glitch/components/load_more';
|
import { LoadMore } from 'flavours/glitch/components/load_more';
|
||||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||||
import StatusContainer from 'flavours/glitch/containers/status_container';
|
import StatusContainer from 'flavours/glitch/containers/status_container';
|
||||||
import { searchEnabled } from 'flavours/glitch/initial_state';
|
import { SearchSection } from 'flavours/glitch/features/explore/components/search_section';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const INITIAL_PAGE_LIMIT = 10;
|
||||||
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
|
|
||||||
});
|
const withoutLastResult = list => {
|
||||||
|
if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) {
|
||||||
|
return list.skipLast(1);
|
||||||
|
} else {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class SearchResults extends ImmutablePureComponent {
|
class SearchResults extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
results: ImmutablePropTypes.map.isRequired,
|
results: ImmutablePropTypes.map.isRequired,
|
||||||
suggestions: ImmutablePropTypes.list.isRequired,
|
|
||||||
fetchSuggestions: PropTypes.func.isRequired,
|
|
||||||
expandSearch: PropTypes.func.isRequired,
|
expandSearch: PropTypes.func.isRequired,
|
||||||
dismissSuggestion: PropTypes.func.isRequired,
|
|
||||||
searchTerm: PropTypes.string,
|
searchTerm: PropTypes.string,
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
if (this.props.searchTerm === '') {
|
|
||||||
this.props.fetchSuggestions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
if (this.props.searchTerm === '') {
|
|
||||||
this.props.fetchSuggestions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLoadMoreAccounts = () => this.props.expandSearch('accounts');
|
handleLoadMoreAccounts = () => this.props.expandSearch('accounts');
|
||||||
|
|
||||||
handleLoadMoreStatuses = () => this.props.expandSearch('statuses');
|
handleLoadMoreStatuses = () => this.props.expandSearch('statuses');
|
||||||
|
@ -47,98 +37,51 @@ class SearchResults extends ImmutablePureComponent {
|
||||||
handleLoadMoreHashtags = () => this.props.expandSearch('hashtags');
|
handleLoadMoreHashtags = () => this.props.expandSearch('hashtags');
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props;
|
const { results } = this.props;
|
||||||
|
|
||||||
let accounts, statuses, hashtags;
|
let accounts, statuses, hashtags;
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
if (searchTerm === '' && !suggestions.isEmpty()) {
|
|
||||||
return (
|
|
||||||
<div className='drawer--results'>
|
|
||||||
<div className='trends'>
|
|
||||||
<div className='trends__header'>
|
|
||||||
<Icon fixedWidth id='user-plus' />
|
|
||||||
<FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{suggestions && suggestions.map(suggestion => (
|
|
||||||
<AccountContainer
|
|
||||||
key={suggestion.get('account')}
|
|
||||||
id={suggestion.get('account')}
|
|
||||||
actionIcon={suggestion.get('source') === 'past_interaction' ? 'times' : null}
|
|
||||||
actionTitle={suggestion.get('source') === 'past_interaction' ? intl.formatMessage(messages.dismissSuggestion) : null}
|
|
||||||
onActionClick={dismissSuggestion}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) {
|
|
||||||
statuses = (
|
|
||||||
<section className='search-results__section'>
|
|
||||||
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></h5>
|
|
||||||
|
|
||||||
<div className='search-results__info'>
|
|
||||||
<FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching posts by their content is not enabled on this Mastodon server.' />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||||
count += results.get('accounts').size;
|
|
||||||
accounts = (
|
accounts = (
|
||||||
<section className='search-results__section'>
|
<SearchSection title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
|
||||||
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></h5>
|
{withoutLastResult(results.get('accounts')).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||||
|
{(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreAccounts} />}
|
||||||
{results.get('accounts').map(accountId => <AccountContainer id={accountId} key={accountId} />)}
|
</SearchSection>
|
||||||
|
|
||||||
{results.get('accounts').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreAccounts} />}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.get('statuses') && results.get('statuses').size > 0) {
|
|
||||||
count += results.get('statuses').size;
|
|
||||||
statuses = (
|
|
||||||
<section className='search-results__section'>
|
|
||||||
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></h5>
|
|
||||||
|
|
||||||
{results.get('statuses').map(statusId => <StatusContainer id={statusId} key={statusId} />)}
|
|
||||||
|
|
||||||
{results.get('statuses').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreStatuses} />}
|
|
||||||
</section>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
||||||
count += results.get('hashtags').size;
|
|
||||||
hashtags = (
|
hashtags = (
|
||||||
<section className='search-results__section'>
|
<SearchSection title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>}>
|
||||||
<h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
|
{withoutLastResult(results.get('hashtags')).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||||
|
{(results.get('hashtags').size > INITIAL_PAGE_LIMIT && results.get('hashtags').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreHashtags} />}
|
||||||
{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
</SearchSection>
|
||||||
|
);
|
||||||
{results.get('hashtags').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreHashtags} />}
|
}
|
||||||
</section>
|
|
||||||
|
if (results.get('statuses') && results.get('statuses').size > 0) {
|
||||||
|
statuses = (
|
||||||
|
<SearchSection title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>}>
|
||||||
|
{withoutLastResult(results.get('statuses')).map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||||
|
{(results.get('statuses').size > INITIAL_PAGE_LIMIT && results.get('statuses').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreStatuses} />}
|
||||||
|
</SearchSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The result.
|
|
||||||
return (
|
return (
|
||||||
<div className='drawer--results'>
|
<div className='drawer--results'>
|
||||||
<header className='search-results__header'>
|
<header className='search-results__header'>
|
||||||
<Icon id='search' fixedWidth />
|
<Icon id='search' fixedWidth />
|
||||||
<FormattedMessage id='search_results.total' defaultMessage='{count, plural, one {# result} other {# results}}' values={{ count }} />
|
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{accounts}
|
{accounts}
|
||||||
{statuses}
|
|
||||||
{hashtags}
|
{hashtags}
|
||||||
|
{statuses}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(SearchResults);
|
export default SearchResults;
|
||||||
|
|
|
@ -15,7 +15,7 @@ import Search from '../components/search';
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
value: state.getIn(['search', 'value']),
|
value: state.getIn(['search', 'value']),
|
||||||
submitted: state.getIn(['search', 'submitted']),
|
submitted: state.getIn(['search', 'submitted']),
|
||||||
recent: state.getIn(['search', 'recent']),
|
recent: state.getIn(['search', 'recent']).reverse(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
export const SearchSection = ({ title, onClickMore, children }) => (
|
||||||
|
<div className='search-results__section'>
|
||||||
|
<div className='search-results__section__header'>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
{onClickMore && <button onClick={onClickMore}><FormattedMessage id='search_results.see_all' defaultMessage='See all' /></button>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
SearchSection.propTypes = {
|
||||||
|
title: PropTypes.node.isRequired,
|
||||||
|
onClickMore: PropTypes.func,
|
||||||
|
children: PropTypes.children,
|
||||||
|
};
|
|
@ -9,14 +9,14 @@ import { List as ImmutableList } from 'immutable';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { expandSearch } from 'flavours/glitch/actions/search';
|
import { submitSearch, expandSearch } from 'flavours/glitch/actions/search';
|
||||||
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
|
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
|
||||||
import { LoadMore } from 'flavours/glitch/components/load_more';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
import Account from 'flavours/glitch/containers/account_container';
|
import Account from 'flavours/glitch/containers/account_container';
|
||||||
import Status from 'flavours/glitch/containers/status_container';
|
import Status from 'flavours/glitch/containers/status_container';
|
||||||
|
|
||||||
|
import { SearchSection } from './components/search_section';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'search_results.title', defaultMessage: 'Search for {q}' },
|
title: { id: 'search_results.title', defaultMessage: 'Search for {q}' },
|
||||||
|
@ -26,85 +26,175 @@ const mapStateToProps = state => ({
|
||||||
isLoading: state.getIn(['search', 'isLoading']),
|
isLoading: state.getIn(['search', 'isLoading']),
|
||||||
results: state.getIn(['search', 'results']),
|
results: state.getIn(['search', 'results']),
|
||||||
q: state.getIn(['search', 'searchTerm']),
|
q: state.getIn(['search', 'searchTerm']),
|
||||||
|
submittedType: state.getIn(['search', 'type']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const appendLoadMore = (id, list, onLoadMore) => {
|
const INITIAL_PAGE_LIMIT = 10;
|
||||||
if (list.size >= 5) {
|
const INITIAL_DISPLAY = 4;
|
||||||
return list.push(<LoadMore key={`${id}-load-more`} visible onClick={onLoadMore} />);
|
|
||||||
|
const hidePeek = list => {
|
||||||
|
if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) {
|
||||||
|
return list.skipLast(1);
|
||||||
} else {
|
} else {
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAccounts = (results, onLoadMore) => appendLoadMore('accounts', results.get('accounts', ImmutableList()).map(item => (
|
const renderAccounts = accounts => hidePeek(accounts).map(id => (
|
||||||
<Account key={`account-${item}`} id={item} />
|
<Account key={id} id={id} />
|
||||||
)), onLoadMore);
|
));
|
||||||
|
|
||||||
const renderHashtags = (results, onLoadMore) => appendLoadMore('hashtags', results.get('hashtags', ImmutableList()).map(item => (
|
const renderHashtags = hashtags => hidePeek(hashtags).map(hashtag => (
|
||||||
<Hashtag key={`tag-${item.get('name')}`} hashtag={item} />
|
<Hashtag key={hashtag.get('name')} hashtag={hashtag} />
|
||||||
)), onLoadMore);
|
));
|
||||||
|
|
||||||
const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', results.get('statuses', ImmutableList()).map(item => (
|
const renderStatuses = statuses => hidePeek(statuses).map(id => (
|
||||||
<Status key={`status-${item}`} id={item} />
|
<Status key={id} id={id} />
|
||||||
)), onLoadMore);
|
));
|
||||||
|
|
||||||
class Results extends PureComponent {
|
class Results extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
results: ImmutablePropTypes.map,
|
results: ImmutablePropTypes.contains({
|
||||||
|
accounts: ImmutablePropTypes.orderedSet,
|
||||||
|
statuses: ImmutablePropTypes.orderedSet,
|
||||||
|
hashtags: ImmutablePropTypes.orderedSet,
|
||||||
|
}),
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
q: PropTypes.string,
|
q: PropTypes.string,
|
||||||
intl: PropTypes.object,
|
intl: PropTypes.object,
|
||||||
|
submittedType: PropTypes.oneOf(['accounts', 'statuses', 'hashtags']),
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
type: 'all',
|
type: this.props.submittedType || 'all',
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSelectAll = () => this.setState({ type: 'all' });
|
static getDerivedStateFromProps(props, state) {
|
||||||
handleSelectAccounts = () => this.setState({ type: 'accounts' });
|
if (props.submittedType !== state.type) {
|
||||||
handleSelectHashtags = () => this.setState({ type: 'hashtags' });
|
return {
|
||||||
handleSelectStatuses = () => this.setState({ type: 'statuses' });
|
type: props.submittedType || 'all',
|
||||||
handleLoadMoreAccounts = () => this.loadMore('accounts');
|
};
|
||||||
handleLoadMoreStatuses = () => this.loadMore('statuses');
|
}
|
||||||
handleLoadMoreHashtags = () => this.loadMore('hashtags');
|
|
||||||
|
|
||||||
loadMore (type) {
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelectAll = () => {
|
||||||
|
const { submittedType, dispatch } = this.props;
|
||||||
|
|
||||||
|
// If we originally searched for a specific type, we need to resubmit
|
||||||
|
// the query to get all types of results
|
||||||
|
if (submittedType) {
|
||||||
|
dispatch(submitSearch());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ type: 'all' });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelectAccounts = () => {
|
||||||
|
const { submittedType, dispatch } = this.props;
|
||||||
|
|
||||||
|
// If we originally searched for something else (but not everything),
|
||||||
|
// we need to resubmit the query for this specific type
|
||||||
|
if (submittedType !== 'accounts') {
|
||||||
|
dispatch(submitSearch('accounts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ type: 'accounts' });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelectHashtags = () => {
|
||||||
|
const { submittedType, dispatch } = this.props;
|
||||||
|
|
||||||
|
// If we originally searched for something else (but not everything),
|
||||||
|
// we need to resubmit the query for this specific type
|
||||||
|
if (submittedType !== 'hashtags') {
|
||||||
|
dispatch(submitSearch('hashtags'));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ type: 'hashtags' });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelectStatuses = () => {
|
||||||
|
const { submittedType, dispatch } = this.props;
|
||||||
|
|
||||||
|
// If we originally searched for something else (but not everything),
|
||||||
|
// we need to resubmit the query for this specific type
|
||||||
|
if (submittedType !== 'statuses') {
|
||||||
|
dispatch(submitSearch('statuses'));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ type: 'statuses' });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMoreAccounts = () => this._loadMore('accounts');
|
||||||
|
handleLoadMoreStatuses = () => this._loadMore('statuses');
|
||||||
|
handleLoadMoreHashtags = () => this._loadMore('hashtags');
|
||||||
|
|
||||||
|
_loadMore (type) {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
dispatch(expandSearch(type));
|
dispatch(expandSearch(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLoadMore = () => {
|
||||||
|
const { type } = this.state;
|
||||||
|
|
||||||
|
if (type !== 'all') {
|
||||||
|
this._loadMore(type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, isLoading, q, results } = this.props;
|
const { intl, isLoading, q, results } = this.props;
|
||||||
const { type } = this.state;
|
const { type } = this.state;
|
||||||
|
|
||||||
let filteredResults = ImmutableList();
|
// We request 1 more result than we display so we can tell if there'd be a next page
|
||||||
|
const hasMore = type !== 'all' ? results.get(type, ImmutableList()).size > INITIAL_PAGE_LIMIT && results.get(type).size % INITIAL_PAGE_LIMIT === 1 : false;
|
||||||
|
|
||||||
|
let filteredResults;
|
||||||
|
|
||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
|
const accounts = results.get('accounts', ImmutableList());
|
||||||
|
const hashtags = results.get('hashtags', ImmutableList());
|
||||||
|
const statuses = results.get('statuses', ImmutableList());
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'all':
|
case 'all':
|
||||||
filteredResults = filteredResults.concat(renderAccounts(results, this.handleLoadMoreAccounts), renderHashtags(results, this.handleLoadMoreHashtags), renderStatuses(results, this.handleLoadMoreStatuses));
|
filteredResults = (accounts.size + hashtags.size + statuses.size) > 0 ? (
|
||||||
|
<>
|
||||||
|
{accounts.size > 0 && (
|
||||||
|
<SearchSection key='accounts' title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>} onClickMore={this.handleLoadMoreAccounts}>
|
||||||
|
{accounts.take(INITIAL_DISPLAY).map(id => <Account key={id} id={id} />)}
|
||||||
|
</SearchSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hashtags.size > 0 && (
|
||||||
|
<SearchSection key='hashtags' title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>} onClickMore={this.handleLoadMoreHashtags}>
|
||||||
|
{hashtags.take(INITIAL_DISPLAY).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||||
|
</SearchSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{statuses.size > 0 && (
|
||||||
|
<SearchSection key='statuses' title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>} onClickMore={this.handleLoadMoreStatuses}>
|
||||||
|
{statuses.take(INITIAL_DISPLAY).map(id => <Status key={id} id={id} />)}
|
||||||
|
</SearchSection>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : [];
|
||||||
break;
|
break;
|
||||||
case 'accounts':
|
case 'accounts':
|
||||||
filteredResults = filteredResults.concat(renderAccounts(results, this.handleLoadMoreAccounts));
|
filteredResults = renderAccounts(accounts);
|
||||||
break;
|
break;
|
||||||
case 'hashtags':
|
case 'hashtags':
|
||||||
filteredResults = filteredResults.concat(renderHashtags(results, this.handleLoadMoreHashtags));
|
filteredResults = renderHashtags(hashtags);
|
||||||
break;
|
break;
|
||||||
case 'statuses':
|
case 'statuses':
|
||||||
filteredResults = filteredResults.concat(renderStatuses(results, this.handleLoadMoreStatuses));
|
filteredResults = renderStatuses(statuses);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filteredResults.size === 0) {
|
|
||||||
filteredResults = (
|
|
||||||
<div className='empty-column-indicator'>
|
|
||||||
<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -117,7 +207,16 @@ class Results extends PureComponent {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='explore__search-results'>
|
<div className='explore__search-results'>
|
||||||
{isLoading ? <LoadingIndicator /> : filteredResults}
|
<ScrollableList
|
||||||
|
scrollKey='search-results'
|
||||||
|
isLoading={isLoading}
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
|
emptyMessage={<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />}
|
||||||
|
bindToDocument
|
||||||
|
>
|
||||||
|
{filteredResults}
|
||||||
|
</ScrollableList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|
|
@ -8,7 +8,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { fetchFavourites } from 'flavours/glitch/actions/interactions';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import { fetchFavourites, expandFavourites } from 'flavours/glitch/actions/interactions';
|
||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||||
|
@ -23,7 +25,9 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
|
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'items']),
|
||||||
|
hasMore: !!state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'next']),
|
||||||
|
isLoading: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'isLoading'], true),
|
||||||
});
|
});
|
||||||
|
|
||||||
class Favourites extends ImmutablePureComponent {
|
class Favourites extends ImmutablePureComponent {
|
||||||
|
@ -32,6 +36,8 @@ class Favourites extends ImmutablePureComponent {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
accountIds: ImmutablePropTypes.list,
|
accountIds: ImmutablePropTypes.list,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -42,12 +48,6 @@ class Favourites extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
|
||||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
|
||||||
this.props.dispatch(fetchFavourites(nextProps.params.statusId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleHeaderClick = () => {
|
handleHeaderClick = () => {
|
||||||
this.column.scrollTop();
|
this.column.scrollTop();
|
||||||
};
|
};
|
||||||
|
@ -60,8 +60,12 @@ class Favourites extends ImmutablePureComponent {
|
||||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleLoadMore = debounce(() => {
|
||||||
|
this.props.dispatch(expandFavourites(this.props.params.statusId));
|
||||||
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, accountIds, multiColumn } = this.props;
|
const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
|
||||||
|
|
||||||
if (!accountIds) {
|
if (!accountIds) {
|
||||||
return (
|
return (
|
||||||
|
@ -87,6 +91,9 @@ class Favourites extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='favourites'
|
scrollKey='favourites'
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
|
isLoading={isLoading}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
export const CriticalUpdateBanner = () => (
|
||||||
|
<div className='warning-banner'>
|
||||||
|
<div className='warning-banner__message'>
|
||||||
|
<h1>
|
||||||
|
<FormattedMessage
|
||||||
|
id='home.pending_critical_update.title'
|
||||||
|
defaultMessage='Critical security update available!'
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='home.pending_critical_update.body'
|
||||||
|
defaultMessage='Please update your Mastodon server as soon as possible!'
|
||||||
|
/>{' '}
|
||||||
|
<a href='/admin/software_updates'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='home.pending_critical_update.link'
|
||||||
|
defaultMessage='See updates'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -14,7 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/act
|
||||||
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
|
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
|
||||||
import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator';
|
import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator';
|
||||||
import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container';
|
import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container';
|
||||||
import { me } from 'flavours/glitch/initial_state';
|
import { me, criticalUpdatesPending } from 'flavours/glitch/initial_state';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
|
@ -23,6 +23,7 @@ import ColumnHeader from '../../components/column_header';
|
||||||
import StatusListContainer from '../ui/containers/status_list_container';
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
|
||||||
import { ColumnSettings } from './components/column_settings';
|
import { ColumnSettings } from './components/column_settings';
|
||||||
|
import { CriticalUpdateBanner } from './components/critical_update_banner';
|
||||||
import { ExplorePrompt } from './components/explore_prompt';
|
import { ExplorePrompt } from './components/explore_prompt';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -158,8 +159,9 @@ class HomeTimeline extends PureComponent {
|
||||||
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
|
const banners = [];
|
||||||
|
|
||||||
let announcementsButton, banner;
|
let announcementsButton;
|
||||||
|
|
||||||
if (hasAnnouncements) {
|
if (hasAnnouncements) {
|
||||||
announcementsButton = (
|
announcementsButton = (
|
||||||
|
@ -174,8 +176,12 @@ class HomeTimeline extends PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (criticalUpdatesPending) {
|
||||||
|
banners.push(<CriticalUpdateBanner key='critical-update-banner' />);
|
||||||
|
}
|
||||||
|
|
||||||
if (tooSlow) {
|
if (tooSlow) {
|
||||||
banner = <ExplorePrompt />;
|
banners.push(<ExplorePrompt key='explore-prompt' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -197,7 +203,7 @@ class HomeTimeline extends PureComponent {
|
||||||
|
|
||||||
{signedIn ? (
|
{signedIn ? (
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
prepend={banner}
|
prepend={banners}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`home_timeline-${columnId}`}
|
scrollKey={`home_timeline-${columnId}`}
|
||||||
|
|
|
@ -100,8 +100,41 @@ class LoginForm extends React.PureComponent {
|
||||||
this.input = c;
|
this.input = c;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isValueValid = (value) => {
|
||||||
|
let likelyAcct = false;
|
||||||
|
let url = null;
|
||||||
|
|
||||||
|
if (value.startsWith('/')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.startsWith('@')) {
|
||||||
|
value = value.slice(1);
|
||||||
|
likelyAcct = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user is in the middle of typing something, do not error out
|
||||||
|
if (value === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^https?:\/\//.test(value) && !likelyAcct) {
|
||||||
|
url = value;
|
||||||
|
} else {
|
||||||
|
url = `https://${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch(_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleChange = ({ target }) => {
|
handleChange = ({ target }) => {
|
||||||
this.setState(state => ({ value: target.value, isLoading: true, error: false, options: addInputToOptions(target.value, state.networkOptions) }), () => this._loadOptions());
|
const error = !this.isValueValid(target.value);
|
||||||
|
this.setState(state => ({ error, value: target.value, isLoading: true, options: addInputToOptions(target.value, state.networkOptions) }), () => this._loadOptions());
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMessage = (event) => {
|
handleMessage = (event) => {
|
||||||
|
@ -115,11 +148,18 @@ class LoginForm extends React.PureComponent {
|
||||||
this.setState({ isSubmitting: false, error: true });
|
this.setState({ isSubmitting: false, error: true });
|
||||||
} else if (event.data?.type === 'fetchInteractionURL-success') {
|
} else if (event.data?.type === 'fetchInteractionURL-success') {
|
||||||
if (/^https?:\/\//.test(event.data.template)) {
|
if (/^https?:\/\//.test(event.data.template)) {
|
||||||
|
try {
|
||||||
|
const url = new URL(event.data.template.replace('{uri}', encodeURIComponent(resourceUrl)));
|
||||||
|
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
localStorage.setItem(PERSISTENCE_KEY, event.data.uri_or_domain);
|
localStorage.setItem(PERSISTENCE_KEY, event.data.uri_or_domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = event.data.template.replace('{uri}', encodeURIComponent(resourceUrl));
|
window.location.href = url;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.setState({ isSubmitting: false, error: true });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setState({ isSubmitting: false, error: true });
|
this.setState({ isSubmitting: false, error: true });
|
||||||
}
|
}
|
||||||
|
@ -259,7 +299,7 @@ class LoginForm extends React.PureComponent {
|
||||||
spellcheck='false'
|
spellcheck='false'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button onClick={this.handleSubmit} disabled={isSubmitting}><FormattedMessage id='interaction_modal.login.action' defaultMessage='Take me home' /></Button>
|
<Button onClick={this.handleSubmit} disabled={isSubmitting || error}><FormattedMessage id='interaction_modal.login.action' defaultMessage='Take me home' /></Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasPopOut && (
|
{hasPopOut && (
|
||||||
|
|
|
@ -8,7 +8,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { fetchReblogs } from 'flavours/glitch/actions/interactions';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import { fetchReblogs, expandReblogs } from 'flavours/glitch/actions/interactions';
|
||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||||
|
@ -16,17 +18,15 @@ import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||||
import Column from 'flavours/glitch/features/ui/components/column';
|
import Column from 'flavours/glitch/features/ui/components/column';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.reblogged_by', defaultMessage: 'Boosted by' },
|
heading: { id: 'column.reblogged_by', defaultMessage: 'Boosted by' },
|
||||||
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
|
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
|
accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'items']),
|
||||||
|
hasMore: !!state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'next']),
|
||||||
|
isLoading: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'isLoading'], true),
|
||||||
});
|
});
|
||||||
|
|
||||||
class Reblogs extends ImmutablePureComponent {
|
class Reblogs extends ImmutablePureComponent {
|
||||||
|
@ -35,6 +35,8 @@ class Reblogs extends ImmutablePureComponent {
|
||||||
params: PropTypes.object.isRequired,
|
params: PropTypes.object.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
accountIds: ImmutablePropTypes.list,
|
accountIds: ImmutablePropTypes.list,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -45,12 +47,6 @@ class Reblogs extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
|
||||||
this.props.dispatch(fetchReblogs(nextProps.params.statusId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleHeaderClick = () => {
|
handleHeaderClick = () => {
|
||||||
this.column.scrollTop();
|
this.column.scrollTop();
|
||||||
};
|
};
|
||||||
|
@ -63,8 +59,12 @@ class Reblogs extends ImmutablePureComponent {
|
||||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleLoadMore = debounce(() => {
|
||||||
|
this.props.dispatch(expandReblogs(this.props.params.statusId));
|
||||||
|
}, 300, { leading: true });
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, accountIds, multiColumn } = this.props;
|
const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
|
||||||
|
|
||||||
if (!accountIds) {
|
if (!accountIds) {
|
||||||
return (
|
return (
|
||||||
|
@ -91,6 +91,9 @@ class Reblogs extends ImmutablePureComponent {
|
||||||
|
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='reblogs'
|
scrollKey='reblogs'
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
hasMore={hasMore}
|
||||||
|
isLoading={isLoading}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
|
|
|
@ -29,6 +29,7 @@ const messages = defineMessages({
|
||||||
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
||||||
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||||
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
|
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
|
||||||
|
openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
|
||||||
app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -56,9 +57,13 @@ class NavigationPanel extends Component {
|
||||||
<div className='navigation-panel'>
|
<div className='navigation-panel'>
|
||||||
{transientSingleColumn && (
|
{transientSingleColumn && (
|
||||||
<div className='navigation-panel__logo'>
|
<div className='navigation-panel__logo'>
|
||||||
<a href={`/deck${location.pathname}`} className='button button--block'>
|
<div class='switch-to-advanced'>
|
||||||
|
{intl.formatMessage(messages.openedInClassicInterface)}
|
||||||
|
{" "}
|
||||||
|
<a href={`/deck${location.pathname}`} class='switch-to-advanced__toggle'>
|
||||||
{intl.formatMessage(messages.advancedInterface)}
|
{intl.formatMessage(messages.advancedInterface)}
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -78,6 +78,7 @@ const PageThree = ({ myAccount }) => (
|
||||||
onSubmit={noop}
|
onSubmit={noop}
|
||||||
onClear={noop}
|
onClear={noop}
|
||||||
onShow={noop}
|
onShow={noop}
|
||||||
|
recent={{}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='pseudo-drawer'>
|
<div className='pseudo-drawer'>
|
||||||
|
|
|
@ -220,8 +220,9 @@ class Video extends PureComponent {
|
||||||
const { x } = getPointerPosition(this.volume, e);
|
const { x } = getPointerPosition(this.volume, e);
|
||||||
|
|
||||||
if(!isNaN(x)) {
|
if(!isNaN(x)) {
|
||||||
this.setState({ volume: x }, () => {
|
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
|
||||||
this.video.volume = x;
|
this.video.volume = x;
|
||||||
|
this.video.muted = this.state.muted;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 15);
|
}, 15);
|
||||||
|
@ -428,10 +429,11 @@ class Video extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleMute = () => {
|
toggleMute = () => {
|
||||||
const muted = !this.video.muted;
|
const muted = !(this.video.muted || this.state.volume === 0);
|
||||||
|
|
||||||
this.setState({ muted }, () => {
|
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
|
||||||
this.video.muted = muted;
|
this.video.volume = this.state.volume;
|
||||||
|
this.video.muted = this.state.muted;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -508,8 +510,10 @@ class Video extends PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
|
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
|
||||||
const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state;
|
||||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||||
|
const muted = this.state.muted || volume === 0;
|
||||||
|
|
||||||
const playerStyle = {};
|
const playerStyle = {};
|
||||||
|
|
||||||
if (inline) {
|
if (inline) {
|
||||||
|
@ -603,12 +607,12 @@ class Video extends PureComponent {
|
||||||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||||
|
|
||||||
<div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
<div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||||
<div className='video-player__volume__current' style={{ width: `${volume * 100}%` }} />
|
<div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%` }} />
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={classNames('video-player__volume__handle')}
|
className={classNames('video-player__volume__handle')}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={{ left: `${volume * 100}%` }}
|
style={{ left: `${muted ? 0 : volume * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -100,6 +100,7 @@ export const hasMultiColumnPath = initialPath === '/'
|
||||||
* @typedef InitialState
|
* @typedef InitialState
|
||||||
* @property {Record<string, Account>} accounts
|
* @property {Record<string, Account>} accounts
|
||||||
* @property {InitialStateLanguage[]} languages
|
* @property {InitialStateLanguage[]} languages
|
||||||
|
* @property {boolean=} critical_updates_pending
|
||||||
* @property {InitialStateMeta} meta
|
* @property {InitialStateMeta} meta
|
||||||
* @property {object} local_settings
|
* @property {object} local_settings
|
||||||
* @property {number} max_toot_chars
|
* @property {number} max_toot_chars
|
||||||
|
@ -160,6 +161,7 @@ export const useBlurhash = getMeta('use_blurhash');
|
||||||
export const usePendingItems = getMeta('use_pending_items');
|
export const usePendingItems = getMeta('use_pending_items');
|
||||||
export const version = getMeta('version');
|
export const version = getMeta('version');
|
||||||
export const languages = initialState?.languages;
|
export const languages = initialState?.languages;
|
||||||
|
export const criticalUpdatesPending = initialState?.critical_updates_pending;
|
||||||
export const statusPageUrl = getMeta('status_page_url');
|
export const statusPageUrl = getMeta('status_page_url');
|
||||||
export const sso_redirect = getMeta('sso_redirect');
|
export const sso_redirect = getMeta('sso_redirect');
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,17 @@
|
||||||
"advanced_options.local-only.long": "不要傳遞給其他實例",
|
"advanced_options.local-only.long": "不要傳遞給其他實例",
|
||||||
"advanced_options.local-only.short": "僅限本地",
|
"advanced_options.local-only.short": "僅限本地",
|
||||||
"advanced_options.local-only.tooltip": "此嘟文僅限本地",
|
"advanced_options.local-only.tooltip": "此嘟文僅限本地",
|
||||||
|
"advanced_options.threaded_mode.long": "發佈時自動打開回覆",
|
||||||
"advanced_options.threaded_mode.short": "討論串模式",
|
"advanced_options.threaded_mode.short": "討論串模式",
|
||||||
"advanced_options.threaded_mode.tooltip": "已啟用討論串模式",
|
"advanced_options.threaded_mode.tooltip": "已啟用討論串模式",
|
||||||
"boost_modal.missing_description": "此嘟文包含未加說明的媒體檔案",
|
"boost_modal.missing_description": "此嘟文包含未加說明的媒體檔案",
|
||||||
|
"column.favourited_by": "誰按了最愛",
|
||||||
|
"column.heading": "雜項",
|
||||||
|
"column.reblogged_by": "被誰轉嘟",
|
||||||
|
"column.subheading": "其他選項",
|
||||||
"column_header.profile": "個人檔案",
|
"column_header.profile": "個人檔案",
|
||||||
"column_subheading.lists": "列表",
|
"column_subheading.lists": "列表",
|
||||||
|
"column_subheading.navigation": "導覽",
|
||||||
"community.column_settings.allow_local_only": "顯示僅限本地的嘟文",
|
"community.column_settings.allow_local_only": "顯示僅限本地的嘟文",
|
||||||
"compose.attach": "附加...",
|
"compose.attach": "附加...",
|
||||||
"compose.attach.doodle": "塗鴉",
|
"compose.attach.doodle": "塗鴉",
|
||||||
|
@ -30,27 +36,66 @@
|
||||||
"compose.content-type.plain": "純文字",
|
"compose.content-type.plain": "純文字",
|
||||||
"compose_form.poll.multiple_choices": "允許多重選擇",
|
"compose_form.poll.multiple_choices": "允許多重選擇",
|
||||||
"compose_form.poll.single_choice": "允許單一選擇",
|
"compose_form.poll.single_choice": "允許單一選擇",
|
||||||
|
"compose_form.spoiler": "將文字隱藏在內容警告後面",
|
||||||
"confirmation_modal.do_not_ask_again": "不要再顯示確認訊息",
|
"confirmation_modal.do_not_ask_again": "不要再顯示確認訊息",
|
||||||
"confirmations.deprecated_settings.confirm": "使用 Mastodon 偏好",
|
"confirmations.deprecated_settings.confirm": "使用 Mastodon 偏好",
|
||||||
|
"confirmations.deprecated_settings.message": "您正在使用的某些特定於 glitch-soc 設備的 {app_settings} 已被 Mastodon {preferences} 所取代,並將被覆蓋:",
|
||||||
|
"confirmations.missing_media_description.confirm": "仍要張貼",
|
||||||
"confirmations.missing_media_description.edit": "編輯媒體",
|
"confirmations.missing_media_description.edit": "編輯媒體",
|
||||||
"confirmations.missing_media_description.message": "至少有一個媒體附件缺少說明。 在發送嘟文之前,請考慮為視障人士在所有媒體附件加上說明。",
|
"confirmations.missing_media_description.message": "至少有一個媒體附件缺少說明。 在發送嘟文之前,請考慮為視障人士在所有媒體附件加上說明。",
|
||||||
"confirmations.unfilter.author": "作者",
|
"confirmations.unfilter.author": "作者",
|
||||||
"confirmations.unfilter.confirm": "顯示",
|
"confirmations.unfilter.confirm": "顯示",
|
||||||
|
"confirmations.unfilter.edit_filter": "編輯篩選器",
|
||||||
"content-type.change": "內容類型",
|
"content-type.change": "內容類型",
|
||||||
"direct.group_by_conversations": "以對話分組",
|
"direct.group_by_conversations": "以對話分組",
|
||||||
"empty_column.follow_recommendations": "似乎未能為您產生任何建議。您可以嘗試使用搜尋來尋找您可能認識的人,或是探索熱門主題標籤。",
|
"empty_column.follow_recommendations": "似乎未能為您產生任何建議。您可以嘗試使用搜尋來尋找您可能認識的人,或是探索熱門主題標籤。",
|
||||||
|
"endorsed_accounts_editor.endorsed_accounts": "受推薦帳號",
|
||||||
|
"favourite_modal.combo": "下次您可以按 {combo} 跳過",
|
||||||
|
"firehose.column_settings.allow_local_only": "在「全部」顯示僅限本地的貼文",
|
||||||
"follow_recommendations.done": "完成",
|
"follow_recommendations.done": "完成",
|
||||||
"follow_recommendations.heading": "跟隨您想檢視其嘟文的人!這裡有一些建議。",
|
"follow_recommendations.heading": "跟隨您想檢視其嘟文的人!這裡有一些建議。",
|
||||||
"follow_recommendations.lead": "來自您跟隨的人之嘟文將會按時間順序顯示在您的首頁時間軸上。不要害怕犯錯,您隨時都可以取消跟隨其他人!",
|
"follow_recommendations.lead": "來自您跟隨的人之嘟文將會按時間順序顯示在您的首頁時間軸上。不要害怕犯錯,您隨時都可以取消跟隨其他人!",
|
||||||
|
"getting_started.onboarding": "帶我四處看看",
|
||||||
"home.column_settings.advanced": "進階設定",
|
"home.column_settings.advanced": "進階設定",
|
||||||
"home.column_settings.filter_regex": "以正規表達式進行過濾",
|
"home.column_settings.filter_regex": "以正規表達式進行過濾",
|
||||||
|
"home.column_settings.show_direct": "顯示私人提及",
|
||||||
|
"home.settings": "欄位設定",
|
||||||
|
"keyboard_shortcuts.bookmark": "到書籤",
|
||||||
|
"keyboard_shortcuts.secondary_toot": "使用次要隱私設定來發布嘟文",
|
||||||
|
"keyboard_shortcuts.toggle_collapse": "去折疊/展開嘟文",
|
||||||
"media_gallery.sensitive": "敏感",
|
"media_gallery.sensitive": "敏感",
|
||||||
|
"moved_to_warning": "此帳戶已標記為移至 {moved_to_link},因此可能不接受新的追隨者。",
|
||||||
|
"navigation_bar.app_settings": "應用程式設定",
|
||||||
|
"navigation_bar.featured_users": "被推薦的使用者",
|
||||||
|
"navigation_bar.keyboard_shortcuts": "鍵盤快速鍵",
|
||||||
|
"navigation_bar.misc": "雜項",
|
||||||
|
"notification.markForDeletion": "標記刪除",
|
||||||
"notification_purge.btn_all": "選取全部",
|
"notification_purge.btn_all": "選取全部",
|
||||||
"notification_purge.btn_apply": "清除所選項目",
|
"notification_purge.btn_apply": "清除所選項目",
|
||||||
"notification_purge.btn_invert": "反向選擇",
|
"notification_purge.btn_invert": "反向選擇",
|
||||||
"notification_purge.btn_none": "取消選取",
|
"notification_purge.btn_none": "取消選取",
|
||||||
|
"notification_purge.start": "進入通知清理模式",
|
||||||
|
"notifications.marked_clear": "清除被選取的通知訊息",
|
||||||
|
"notifications.marked_clear_confirmation": "您確定要永久清除所有被選取的通知訊息嗎?",
|
||||||
|
"onboarding.done": "完成",
|
||||||
|
"onboarding.next": "下一個",
|
||||||
|
"onboarding.page_five.public_timelines": "本地時間軸顯示來自 {domain} 上所有人的公開貼文。聯合時間軸顯示 {domain} 上追隨的每個人發表的公開貼文。這些是公共時間軸,是發現新朋友的好方法。",
|
||||||
|
"onboarding.page_four.home": "首頁時間線會顯示你追隨的人發布的貼文。",
|
||||||
|
"onboarding.page_four.notifications": "當有人與您互動時會顯示在通知欄。",
|
||||||
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
|
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
|
||||||
|
"onboarding.page_one.handle": "你的帳號在 {domain} ,所以你的帳號全名是 {handle}",
|
||||||
|
"onboarding.page_one.welcome": "歡迎來到 {domain} !",
|
||||||
|
"onboarding.page_six.admin": "您的站台管理者是 {admin} 。",
|
||||||
|
"onboarding.page_six.almost_done": "就快完成了…",
|
||||||
|
"onboarding.page_six.apps_available": "有適用於 iOS、Android 和其他平台的 {apps}。",
|
||||||
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
|
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
|
||||||
|
"onboarding.page_six.guidelines": "社群規範",
|
||||||
|
"onboarding.page_six.read_guidelines": "請閱讀 {domain} 的 {guidelines}!",
|
||||||
|
"onboarding.page_six.various_app": "手機應用程式",
|
||||||
|
"onboarding.page_three.profile": "編輯您的個人資料以更改您的頭像、個人簡介和顯示名稱。在那裡,您還會發現其他偏好設置。",
|
||||||
|
"onboarding.page_three.search": "使用搜索欄查找他人與主題標籤,例如 {illustration} 和 {introductions} 。要尋找其他站台的人,請使用他們的完整帳號名稱。",
|
||||||
|
"onboarding.page_two.compose": "從撰寫欄撰寫帖子。您可以使用下面的圖示上傳圖片、更改隱私設置以及添加內容警告。",
|
||||||
|
"onboarding.skip": "略過",
|
||||||
"settings.always_show_spoilers_field": "永遠啟用內容警告欄位",
|
"settings.always_show_spoilers_field": "永遠啟用內容警告欄位",
|
||||||
"settings.auto_collapse": "自動折疊",
|
"settings.auto_collapse": "自動折疊",
|
||||||
"settings.auto_collapse_all": "全部",
|
"settings.auto_collapse_all": "全部",
|
||||||
|
@ -83,19 +128,23 @@
|
||||||
"settings.hicolor_privacy_icons.hint": "用明亮且易於區分的顏色顯示隱私圖示",
|
"settings.hicolor_privacy_icons.hint": "用明亮且易於區分的顏色顯示隱私圖示",
|
||||||
"settings.image_backgrounds": "圖片背景",
|
"settings.image_backgrounds": "圖片背景",
|
||||||
"settings.image_backgrounds_media": "預覽折疊嘟文的媒體檔案",
|
"settings.image_backgrounds_media": "預覽折疊嘟文的媒體檔案",
|
||||||
"settings.image_backgrounds_media_hint": "如果嘟文包含媒體檔案,使用的一個作為圖片背景",
|
"settings.image_backgrounds_media_hint": "如果嘟文包含媒體檔案,使用第一個作為圖片背景",
|
||||||
"settings.image_backgrounds_users": "為折疊的嘟文加上圖片背景",
|
"settings.image_backgrounds_users": "為折疊的嘟文加上圖片背景",
|
||||||
|
"settings.inline_preview_cards": "針對外部連接顯示內嵌的預覽卡",
|
||||||
"settings.layout_opts": "版面選項",
|
"settings.layout_opts": "版面選項",
|
||||||
"settings.media": "媒體",
|
"settings.media": "媒體",
|
||||||
"settings.media_fullwidth": "在媒體預覽中使用完整寬度",
|
"settings.media_fullwidth": "在媒體預覽中使用完整寬度",
|
||||||
"settings.media_letterbox": "在媒體預覽加上黑邊",
|
"settings.media_letterbox": "在媒體預覽加上黑邊",
|
||||||
"settings.media_letterbox_hint": "在媒體預覽中縮小並加上黑邊以取代延展與裁切",
|
"settings.media_letterbox_hint": "在媒體預覽中縮小並加上黑邊以取代延展與裁切",
|
||||||
"settings.media_reveal_behind_cw": "預設顯示隱藏在內容警告的敏感媒體檔案",
|
"settings.media_reveal_behind_cw": "預設顯示隱藏在內容警告的敏感媒體檔案",
|
||||||
|
"settings.notifications.favicon_badge": "未讀通知網站圖示徽章",
|
||||||
|
"settings.notifications.favicon_badge.hint": "在網站圖示上增加一個未讀通知徽章",
|
||||||
"settings.notifications.tab_badge": "未讀通知徽章",
|
"settings.notifications.tab_badge": "未讀通知徽章",
|
||||||
"settings.notifications.tab_badge.hint": "當通知列未打開時,在導引圖示中顯示未讀通知的徽章",
|
"settings.notifications.tab_badge.hint": "當通知列未打開時,在導引圖示中顯示未讀通知的徽章",
|
||||||
"settings.notifications_opts": "通知選項",
|
"settings.notifications_opts": "通知選項",
|
||||||
"settings.pop_in_left": "左邊",
|
"settings.pop_in_left": "左邊",
|
||||||
"settings.pop_in_player": "啟用彈出播放器",
|
"settings.pop_in_player": "啟用彈出播放器",
|
||||||
|
"settings.pop_in_position": "彈出播放器位置:",
|
||||||
"settings.pop_in_right": "右邊",
|
"settings.pop_in_right": "右邊",
|
||||||
"settings.preferences": "使用者偏好設定",
|
"settings.preferences": "使用者偏好設定",
|
||||||
"settings.prepend_cw_re": "回覆時在內容警告前添加 \"re:\"",
|
"settings.prepend_cw_re": "回覆時在內容警告前添加 \"re:\"",
|
||||||
|
@ -105,6 +154,7 @@
|
||||||
"settings.rewrite_mentions_acct": "改寫為使用者名稱與網域(當使用者來自外部)",
|
"settings.rewrite_mentions_acct": "改寫為使用者名稱與網域(當使用者來自外部)",
|
||||||
"settings.rewrite_mentions_no": "不要改寫提及",
|
"settings.rewrite_mentions_no": "不要改寫提及",
|
||||||
"settings.rewrite_mentions_username": "改寫為使用者名稱",
|
"settings.rewrite_mentions_username": "改寫為使用者名稱",
|
||||||
|
"settings.shared_settings_link": "使用者偏好設定",
|
||||||
"settings.show_action_bar": "在折疊的嘟文顯示操作按鈕",
|
"settings.show_action_bar": "在折疊的嘟文顯示操作按鈕",
|
||||||
"settings.show_content_type_choice": "在編寫嘟文時顯示內容類型選擇",
|
"settings.show_content_type_choice": "在編寫嘟文時顯示內容類型選擇",
|
||||||
"settings.show_reply_counter": "顯示回覆數量的估計值",
|
"settings.show_reply_counter": "顯示回覆數量的估計值",
|
||||||
|
@ -113,12 +163,14 @@
|
||||||
"settings.side_arm_reply_mode": "當回覆一篇嘟文時,次要發出嘟文按鈕應該設為:",
|
"settings.side_arm_reply_mode": "當回覆一篇嘟文時,次要發出嘟文按鈕應該設為:",
|
||||||
"settings.side_arm_reply_mode.copy": "複製回覆嘟文的隱私設置",
|
"settings.side_arm_reply_mode.copy": "複製回覆嘟文的隱私設置",
|
||||||
"settings.side_arm_reply_mode.keep": "保持原本的隱私設定",
|
"settings.side_arm_reply_mode.keep": "保持原本的隱私設定",
|
||||||
|
"settings.side_arm_reply_mode.restrict": "限制只能使用與回覆嘟文相同的隱私設置",
|
||||||
"settings.status_icons": "嘟文圖示",
|
"settings.status_icons": "嘟文圖示",
|
||||||
"settings.status_icons_language": "語言指示器",
|
"settings.status_icons_language": "語言指示器",
|
||||||
"settings.status_icons_local_only": "僅限本地指示器",
|
"settings.status_icons_local_only": "僅限本地指示器",
|
||||||
"settings.status_icons_media": "媒體與投票指示器",
|
"settings.status_icons_media": "媒體與投票指示器",
|
||||||
"settings.status_icons_reply": "回覆指示器",
|
"settings.status_icons_reply": "回覆指示器",
|
||||||
"settings.status_icons_visibility": "嘟文隱私指示器",
|
"settings.status_icons_visibility": "嘟文隱私指示器",
|
||||||
|
"settings.swipe_to_change_columns": "允許使用滑動手勢更改顯示欄位(僅限移動裝置)",
|
||||||
"settings.tag_misleading_links": "標記誤導性的連結",
|
"settings.tag_misleading_links": "標記誤導性的連結",
|
||||||
"settings.tag_misleading_links.hint": "在每個未明確提及的連結添加帶有連結目標主機的視覺指示",
|
"settings.tag_misleading_links.hint": "在每個未明確提及的連結添加帶有連結目標主機的視覺指示",
|
||||||
"settings.wide_view": "寬廣模式(僅限桌面模式)",
|
"settings.wide_view": "寬廣模式(僅限桌面模式)",
|
||||||
|
@ -130,5 +182,17 @@
|
||||||
"status.has_video": "包含視訊檔案",
|
"status.has_video": "包含視訊檔案",
|
||||||
"status.in_reply_to": "嘟文有回覆",
|
"status.in_reply_to": "嘟文有回覆",
|
||||||
"status.is_poll": "嘟文有投票",
|
"status.is_poll": "嘟文有投票",
|
||||||
"status.local_only": "只在此實例可見"
|
"status.local_only": "只在此實例可見",
|
||||||
|
"status.sensitive_toggle": "點擊查看",
|
||||||
|
"status.uncollapse": "展開",
|
||||||
|
"web_app_crash.change_your_settings": "修改你的 {settings}",
|
||||||
|
"web_app_crash.content": "您可以嘗試以下任一種方法:",
|
||||||
|
"web_app_crash.debug_info": "除錯資訊",
|
||||||
|
"web_app_crash.disable_addons": "禁用瀏覽器插件或內置翻譯工具",
|
||||||
|
"web_app_crash.issue_tracker": "問題追蹤系統",
|
||||||
|
"web_app_crash.reload": "重新載入",
|
||||||
|
"web_app_crash.reload_page": "{reload} 當前頁面",
|
||||||
|
"web_app_crash.report_issue": "到 {issuetracker} 回報問題",
|
||||||
|
"web_app_crash.settings": "設定",
|
||||||
|
"web_app_crash.title": "很抱歉,Mastodon 應用程序出現問題。"
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ function main() {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (registration) {
|
if (registration && 'Notification' in window && Notification.permission === 'granted') {
|
||||||
const registerPushNotifications = await import('flavours/glitch/actions/push_notifications');
|
const registerPushNotifications = await import('flavours/glitch/actions/push_notifications');
|
||||||
|
|
||||||
store.dispatch(registerPushNotifications.register());
|
store.dispatch(registerPushNotifications.register());
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
COMPOSE_MENTION,
|
COMPOSE_MENTION,
|
||||||
|
@ -12,9 +12,9 @@ import {
|
||||||
SEARCH_FETCH_FAIL,
|
SEARCH_FETCH_FAIL,
|
||||||
SEARCH_FETCH_SUCCESS,
|
SEARCH_FETCH_SUCCESS,
|
||||||
SEARCH_SHOW,
|
SEARCH_SHOW,
|
||||||
|
SEARCH_EXPAND_REQUEST,
|
||||||
SEARCH_EXPAND_SUCCESS,
|
SEARCH_EXPAND_SUCCESS,
|
||||||
SEARCH_RESULT_CLICK,
|
SEARCH_HISTORY_UPDATE,
|
||||||
SEARCH_RESULT_FORGET,
|
|
||||||
} from 'flavours/glitch/actions/search';
|
} from 'flavours/glitch/actions/search';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
|
@ -24,6 +24,7 @@ const initialState = ImmutableMap({
|
||||||
results: ImmutableMap(),
|
results: ImmutableMap(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
type: null,
|
||||||
recent: ImmutableOrderedSet(),
|
recent: ImmutableOrderedSet(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,6 +38,8 @@ export default function search(state = initialState, action) {
|
||||||
map.set('results', ImmutableMap());
|
map.set('results', ImmutableMap());
|
||||||
map.set('submitted', false);
|
map.set('submitted', false);
|
||||||
map.set('hidden', false);
|
map.set('hidden', false);
|
||||||
|
map.set('searchTerm', '');
|
||||||
|
map.set('type', null);
|
||||||
});
|
});
|
||||||
case SEARCH_SHOW:
|
case SEARCH_SHOW:
|
||||||
return state.set('hidden', false);
|
return state.set('hidden', false);
|
||||||
|
@ -48,27 +51,29 @@ export default function search(state = initialState, action) {
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('isLoading', true);
|
map.set('isLoading', true);
|
||||||
map.set('submitted', true);
|
map.set('submitted', true);
|
||||||
|
map.set('type', action.searchType);
|
||||||
});
|
});
|
||||||
case SEARCH_FETCH_FAIL:
|
case SEARCH_FETCH_FAIL:
|
||||||
return state.set('isLoading', false);
|
return state.set('isLoading', false);
|
||||||
case SEARCH_FETCH_SUCCESS:
|
case SEARCH_FETCH_SUCCESS:
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.set('results', ImmutableMap({
|
map.set('results', ImmutableMap({
|
||||||
accounts: ImmutableList(action.results.accounts.map(item => item.id)),
|
accounts: ImmutableOrderedSet(action.results.accounts.map(item => item.id)),
|
||||||
statuses: ImmutableList(action.results.statuses.map(item => item.id)),
|
statuses: ImmutableOrderedSet(action.results.statuses.map(item => item.id)),
|
||||||
hashtags: fromJS(action.results.hashtags),
|
hashtags: ImmutableOrderedSet(fromJS(action.results.hashtags)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
map.set('searchTerm', action.searchTerm);
|
map.set('searchTerm', action.searchTerm);
|
||||||
|
map.set('type', action.searchType);
|
||||||
map.set('isLoading', false);
|
map.set('isLoading', false);
|
||||||
});
|
});
|
||||||
|
case SEARCH_EXPAND_REQUEST:
|
||||||
|
return state.set('type', action.searchType);
|
||||||
case SEARCH_EXPAND_SUCCESS:
|
case SEARCH_EXPAND_SUCCESS:
|
||||||
const results = action.searchType === 'hashtags' ? fromJS(action.results.hashtags) : action.results[action.searchType].map(item => item.id);
|
const results = action.searchType === 'hashtags' ? ImmutableOrderedSet(fromJS(action.results.hashtags)) : action.results[action.searchType].map(item => item.id);
|
||||||
return state.updateIn(['results', action.searchType], list => list.concat(results));
|
return state.updateIn(['results', action.searchType], list => list.union(results));
|
||||||
case SEARCH_RESULT_CLICK:
|
case SEARCH_HISTORY_UPDATE:
|
||||||
return state.update('recent', set => set.add(fromJS(action.result)));
|
return state.set('recent', ImmutableOrderedSet(fromJS(action.recent)));
|
||||||
case SEARCH_RESULT_FORGET:
|
|
||||||
return state.update('recent', set => set.filterNot(result => result.get('q') === action.q));
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,8 +44,18 @@ import {
|
||||||
FEATURED_TAGS_FETCH_FAIL,
|
FEATURED_TAGS_FETCH_FAIL,
|
||||||
} from 'flavours/glitch/actions/featured_tags';
|
} from 'flavours/glitch/actions/featured_tags';
|
||||||
import {
|
import {
|
||||||
|
REBLOGS_FETCH_REQUEST,
|
||||||
REBLOGS_FETCH_SUCCESS,
|
REBLOGS_FETCH_SUCCESS,
|
||||||
|
REBLOGS_FETCH_FAIL,
|
||||||
|
REBLOGS_EXPAND_REQUEST,
|
||||||
|
REBLOGS_EXPAND_SUCCESS,
|
||||||
|
REBLOGS_EXPAND_FAIL,
|
||||||
|
FAVOURITES_FETCH_REQUEST,
|
||||||
FAVOURITES_FETCH_SUCCESS,
|
FAVOURITES_FETCH_SUCCESS,
|
||||||
|
FAVOURITES_FETCH_FAIL,
|
||||||
|
FAVOURITES_EXPAND_REQUEST,
|
||||||
|
FAVOURITES_EXPAND_SUCCESS,
|
||||||
|
FAVOURITES_EXPAND_FAIL,
|
||||||
} from 'flavours/glitch/actions/interactions';
|
} from 'flavours/glitch/actions/interactions';
|
||||||
import {
|
import {
|
||||||
MUTES_FETCH_REQUEST,
|
MUTES_FETCH_REQUEST,
|
||||||
|
@ -133,9 +143,25 @@ export default function userLists(state = initialState, action) {
|
||||||
case FOLLOWING_EXPAND_FAIL:
|
case FOLLOWING_EXPAND_FAIL:
|
||||||
return state.setIn(['following', action.id, 'isLoading'], false);
|
return state.setIn(['following', action.id, 'isLoading'], false);
|
||||||
case REBLOGS_FETCH_SUCCESS:
|
case REBLOGS_FETCH_SUCCESS:
|
||||||
return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
return normalizeList(state, ['reblogged_by', action.id], action.accounts, action.next);
|
||||||
|
case REBLOGS_EXPAND_SUCCESS:
|
||||||
|
return appendToList(state, ['reblogged_by', action.id], action.accounts, action.next);
|
||||||
|
case REBLOGS_FETCH_REQUEST:
|
||||||
|
case REBLOGS_EXPAND_REQUEST:
|
||||||
|
return state.setIn(['reblogged_by', action.id, 'isLoading'], true);
|
||||||
|
case REBLOGS_FETCH_FAIL:
|
||||||
|
case REBLOGS_EXPAND_FAIL:
|
||||||
|
return state.setIn(['reblogged_by', action.id, 'isLoading'], false);
|
||||||
case FAVOURITES_FETCH_SUCCESS:
|
case FAVOURITES_FETCH_SUCCESS:
|
||||||
return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
return normalizeList(state, ['favourited_by', action.id], action.accounts, action.next);
|
||||||
|
case FAVOURITES_EXPAND_SUCCESS:
|
||||||
|
return appendToList(state, ['favourited_by', action.id], action.accounts, action.next);
|
||||||
|
case FAVOURITES_FETCH_REQUEST:
|
||||||
|
case FAVOURITES_EXPAND_REQUEST:
|
||||||
|
return state.setIn(['favourited_by', action.id, 'isLoading'], true);
|
||||||
|
case FAVOURITES_FETCH_FAIL:
|
||||||
|
case FAVOURITES_EXPAND_FAIL:
|
||||||
|
return state.setIn(['favourited_by', action.id, 'isLoading'], false);
|
||||||
case NOTIFICATIONS_UPDATE:
|
case NOTIFICATIONS_UPDATE:
|
||||||
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
||||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||||
|
|
|
@ -46,3 +46,4 @@ export default class Settings {
|
||||||
export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
|
export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
|
||||||
export const tagHistory = new Settings('mastodon_tag_history');
|
export const tagHistory = new Settings('mastodon_tag_history');
|
||||||
export const bannerSettings = new Settings('mastodon_banner_settings');
|
export const bannerSettings = new Settings('mastodon_banner_settings');
|
||||||
|
export const searchHistory = new Settings('mastodon_search_history');
|
|
@ -192,6 +192,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-role,
|
.account-role,
|
||||||
|
.information-badge,
|
||||||
|
.simple_form .overridden,
|
||||||
.simple_form .recommended,
|
.simple_form .recommended,
|
||||||
.simple_form .not_recommended,
|
.simple_form .not_recommended,
|
||||||
.simple_form .glitch_only {
|
.simple_form .glitch_only {
|
||||||
|
|
|
@ -143,6 +143,11 @@ $content-width: 840px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning a {
|
||||||
|
color: $gold-star;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.simple-navigation-active-leaf a {
|
.simple-navigation-active-leaf a {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
background-color: $ui-highlight-color;
|
background-color: $ui-highlight-color;
|
||||||
|
|
|
@ -365,7 +365,7 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background: darken($ui-base-color, 4%);
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
@ -383,26 +383,18 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: $secondary-text-color;
|
color: $primary-text-color;
|
||||||
|
|
||||||
&::before,
|
&::before {
|
||||||
&::after {
|
|
||||||
display: block;
|
display: block;
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 0 10px 10px;
|
|
||||||
border-color: transparent transparent lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
bottom: -1px;
|
bottom: -1px;
|
||||||
border-color: transparent transparent $ui-base-color;
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: $highlight-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -228,6 +228,22 @@ $ui-header-height: 55px;
|
||||||
top: -48px;
|
top: -48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switch-to-advanced {
|
||||||
|
color: $light-text-color;
|
||||||
|
background-color: $ui-base-color;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
|
.switch-to-advanced__toggle {
|
||||||
|
color: $ui-button-tertiary-color;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.column-link {
|
.column-link {
|
||||||
background: lighten($ui-base-color, 8%);
|
background: lighten($ui-base-color, 8%);
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
|
@ -961,14 +977,14 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dismissable-banner {
|
.dismissable-banner,
|
||||||
|
.warning-banner {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid $highlight-text-color;
|
border: 1px solid $highlight-text-color;
|
||||||
background: rgba($highlight-text-color, 0.15);
|
background: rgba($highlight-text-color, 0.15);
|
||||||
padding-inline-end: 45px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&__background-image {
|
&__background-image {
|
||||||
|
@ -1028,10 +1044,8 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action {
|
&__action {
|
||||||
position: absolute;
|
float: right;
|
||||||
inset-inline-end: 0;
|
padding: 15px 10px;
|
||||||
top: 0;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
|
@ -1039,6 +1053,21 @@ $ui-header-height: 55px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning-banner {
|
||||||
|
border: 1px solid $warning-red;
|
||||||
|
background: rgba($warning-red, 0.15);
|
||||||
|
|
||||||
|
&__message {
|
||||||
|
h1 {
|
||||||
|
color: $warning-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hashtag-header {
|
.hashtag-header {
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
|
@ -132,22 +132,39 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-results__section {
|
.search-results__section {
|
||||||
margin-bottom: 5px;
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
h5 {
|
&:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
cursor: default;
|
|
||||||
display: flex;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
color: $dark-text-color;
|
color: $darker-text-color;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
.fa {
|
h3 .fa {
|
||||||
display: inline-block;
|
|
||||||
margin-inline-end: 5px;
|
margin-inline-end: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: $highlight-text-color;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: 0;
|
||||||
|
font: inherit;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.account:last-child,
|
.account:last-child,
|
||||||
|
|
|
@ -25,6 +25,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__menu {
|
&__menu {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&__message {
|
&__message {
|
||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
@ -72,6 +78,11 @@
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
overflow: inherit;
|
||||||
|
text-overflow: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,7 @@
|
||||||
|
|
||||||
.filter-form {
|
.filter-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autosuggest-textarea__textarea {
|
.autosuggest-textarea__textarea {
|
||||||
|
|
|
@ -461,8 +461,8 @@
|
||||||
|
|
||||||
&.status-direct > .status__content::after {
|
&.status-direct > .status__content::after {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
rgba(lighten($ui-base-color, 8%), 0),
|
rgba(mix($ui-base-color, $ui-highlight-color, 95%), 0),
|
||||||
rgba(lighten($ui-base-color, 8%), 1)
|
rgba(mix($ui-base-color, $ui-highlight-color, 95%), 1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -103,6 +103,7 @@ code {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overridden,
|
||||||
.recommended,
|
.recommended,
|
||||||
.not_recommended,
|
.not_recommended,
|
||||||
.glitch_only {
|
.glitch_only {
|
||||||
|
@ -1187,14 +1188,14 @@ code {
|
||||||
}
|
}
|
||||||
|
|
||||||
li:first-child .label {
|
li:first-child .label {
|
||||||
left: auto;
|
|
||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
|
inset-inline-end: auto;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li:last-child .label {
|
li:last-child .label {
|
||||||
left: auto;
|
inset-inline-start: auto;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
text-align: end;
|
text-align: end;
|
||||||
transform: none;
|
transform: none;
|
||||||
|
|
|
@ -113,4 +113,11 @@ body.rtl {
|
||||||
.fa-chevron-right::before {
|
.fa-chevron-right::before {
|
||||||
content: '\F053';
|
content: '\F053';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dismissable-banner,
|
||||||
|
.warning-banner {
|
||||||
|
&__action {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,11 @@
|
||||||
border-top: 1px solid $ui-base-color;
|
border-top: 1px solid $ui-base-color;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
background: darken($ui-base-color, 4%);
|
background: darken($ui-base-color, 4%);
|
||||||
|
|
||||||
|
&.critical {
|
||||||
|
font-weight: 700;
|
||||||
|
color: $gold-star;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > thead > tr > th {
|
& > thead > tr > th {
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
import api from '../api';
|
|
||||||
|
|
||||||
export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
|
|
||||||
export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
|
|
||||||
export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
|
|
||||||
|
|
||||||
export function submitAccountNote(id, value) {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
dispatch(submitAccountNoteRequest());
|
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/note`, {
|
|
||||||
comment: value,
|
|
||||||
}).then(response => {
|
|
||||||
dispatch(submitAccountNoteSuccess(response.data));
|
|
||||||
}).catch(error => dispatch(submitAccountNoteFail(error)));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function submitAccountNoteRequest() {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_NOTE_SUBMIT_REQUEST,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function submitAccountNoteSuccess(relationship) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
|
|
||||||
relationship,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function submitAccountNoteFail(error) {
|
|
||||||
return {
|
|
||||||
type: ACCOUNT_NOTE_SUBMIT_FAIL,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
}
|
|
18
app/javascript/mastodon/actions/account_notes.ts
Normal file
18
app/javascript/mastodon/actions/account_notes.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
|
||||||
|
|
||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export const submitAccountNote = createAppAsyncThunk(
|
||||||
|
'account_note/submit',
|
||||||
|
async (args: { id: string; value: string }, { getState }) => {
|
||||||
|
// TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged
|
||||||
|
const response = await api(getState).post<unknown>(
|
||||||
|
`/api/v1/accounts/${args.id}/note`,
|
||||||
|
{
|
||||||
|
comment: args.value,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return { relationship: response.data };
|
||||||
|
},
|
||||||
|
);
|
|
@ -84,6 +84,7 @@ const messages = defineMessages({
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
open: { id: 'compose.published.open', defaultMessage: 'Open' },
|
open: { id: 'compose.published.open', defaultMessage: 'Open' },
|
||||||
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
|
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
|
||||||
|
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||||
|
@ -246,7 +247,7 @@ export function submitCompose(routerHistory) {
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(showAlert({
|
dispatch(showAlert({
|
||||||
message: messages.published,
|
message: statusId === null ? messages.published : messages.saved,
|
||||||
action: messages.open,
|
action: messages.open,
|
||||||
dismissAfter: 10000,
|
dismissAfter: 10000,
|
||||||
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import api from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
import { fetchRelationships } from './accounts';
|
||||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||||
|
|
||||||
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||||
export const REBLOG_FAIL = 'REBLOG_FAIL';
|
export const REBLOG_FAIL = 'REBLOG_FAIL';
|
||||||
|
|
||||||
|
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||||
|
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||||
|
export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
|
||||||
|
|
||||||
export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
||||||
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
||||||
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
||||||
|
@ -26,6 +31,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
|
||||||
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
||||||
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST';
|
||||||
|
export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
|
||||||
|
export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
|
||||||
|
|
||||||
export const PIN_REQUEST = 'PIN_REQUEST';
|
export const PIN_REQUEST = 'PIN_REQUEST';
|
||||||
export const PIN_SUCCESS = 'PIN_SUCCESS';
|
export const PIN_SUCCESS = 'PIN_SUCCESS';
|
||||||
export const PIN_FAIL = 'PIN_FAIL';
|
export const PIN_FAIL = 'PIN_FAIL';
|
||||||
|
@ -273,8 +282,10 @@ export function fetchReblogs(id) {
|
||||||
dispatch(fetchReblogsRequest(id));
|
dispatch(fetchReblogsRequest(id));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(importFetchedAccounts(response.data));
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(fetchReblogsSuccess(id, response.data));
|
dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchReblogsFail(id, error));
|
dispatch(fetchReblogsFail(id, error));
|
||||||
});
|
});
|
||||||
|
@ -288,17 +299,62 @@ export function fetchReblogsRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchReblogsSuccess(id, accounts) {
|
export function fetchReblogsSuccess(id, accounts, next) {
|
||||||
return {
|
return {
|
||||||
type: REBLOGS_FETCH_SUCCESS,
|
type: REBLOGS_FETCH_SUCCESS,
|
||||||
id,
|
id,
|
||||||
accounts,
|
accounts,
|
||||||
|
next,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchReblogsFail(id, error) {
|
export function fetchReblogsFail(id, error) {
|
||||||
return {
|
return {
|
||||||
type: REBLOGS_FETCH_FAIL,
|
type: REBLOGS_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReblogs(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['user_lists', 'reblogged_by', id, 'next']);
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandReblogsRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
|
}).catch(error => dispatch(expandReblogsFail(id, error)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReblogsRequest(id) {
|
||||||
|
return {
|
||||||
|
type: REBLOGS_EXPAND_REQUEST,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReblogsSuccess(id, accounts, next) {
|
||||||
|
return {
|
||||||
|
type: REBLOGS_EXPAND_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandReblogsFail(id, error) {
|
||||||
|
return {
|
||||||
|
type: REBLOGS_EXPAND_FAIL,
|
||||||
|
id,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -308,8 +364,10 @@ export function fetchFavourites(id) {
|
||||||
dispatch(fetchFavouritesRequest(id));
|
dispatch(fetchFavouritesRequest(id));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
dispatch(importFetchedAccounts(response.data));
|
dispatch(importFetchedAccounts(response.data));
|
||||||
dispatch(fetchFavouritesSuccess(id, response.data));
|
dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchFavouritesFail(id, error));
|
dispatch(fetchFavouritesFail(id, error));
|
||||||
});
|
});
|
||||||
|
@ -323,17 +381,62 @@ export function fetchFavouritesRequest(id) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchFavouritesSuccess(id, accounts) {
|
export function fetchFavouritesSuccess(id, accounts, next) {
|
||||||
return {
|
return {
|
||||||
type: FAVOURITES_FETCH_SUCCESS,
|
type: FAVOURITES_FETCH_SUCCESS,
|
||||||
id,
|
id,
|
||||||
accounts,
|
accounts,
|
||||||
|
next,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchFavouritesFail(id, error) {
|
export function fetchFavouritesFail(id, error) {
|
||||||
return {
|
return {
|
||||||
type: FAVOURITES_FETCH_FAIL,
|
type: FAVOURITES_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandFavourites(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['user_lists', 'favourited_by', id, 'next']);
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandFavouritesRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
dispatch(importFetchedAccounts(response.data));
|
||||||
|
dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
|
}).catch(error => dispatch(expandFavouritesFail(id, error)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandFavouritesRequest(id) {
|
||||||
|
return {
|
||||||
|
type: FAVOURITES_EXPAND_REQUEST,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandFavouritesSuccess(id, accounts, next) {
|
||||||
|
return {
|
||||||
|
type: FAVOURITES_EXPAND_SUCCESS,
|
||||||
|
id,
|
||||||
|
accounts,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expandFavouritesFail(id, error) {
|
||||||
|
return {
|
||||||
|
type: FAVOURITES_EXPAND_FAIL,
|
||||||
|
id,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
importFetchedStatuses,
|
importFetchedStatuses,
|
||||||
} from './importer';
|
} from './importer';
|
||||||
import { submitMarkers } from './markers';
|
import { submitMarkers } from './markers';
|
||||||
|
import { register as registerPushNotifications } from './push_notifications';
|
||||||
import { saveSettings } from './settings';
|
import { saveSettings } from './settings';
|
||||||
|
|
||||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||||
|
@ -293,6 +294,10 @@ export function requestBrowserPermission(callback = noOp) {
|
||||||
requestNotificationPermission((permission) => {
|
requestNotificationPermission((permission) => {
|
||||||
dispatch(setBrowserPermission(permission));
|
dispatch(setBrowserPermission(permission));
|
||||||
callback(permission);
|
callback(permission);
|
||||||
|
|
||||||
|
if (permission === 'granted') {
|
||||||
|
dispatch(registerPushNotifications());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import { fromJS } from 'immutable';
|
||||||
|
|
||||||
|
import { searchHistory } from 'mastodon/settings';
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
|
@ -15,8 +19,7 @@ export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
|
||||||
export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
|
export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
|
||||||
export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
|
export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
|
||||||
|
|
||||||
export const SEARCH_RESULT_CLICK = 'SEARCH_RESULT_CLICK';
|
export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE';
|
||||||
export const SEARCH_RESULT_FORGET = 'SEARCH_RESULT_FORGET';
|
|
||||||
|
|
||||||
export function changeSearch(value) {
|
export function changeSearch(value) {
|
||||||
return {
|
return {
|
||||||
|
@ -37,17 +40,17 @@ export function submitSearch(type) {
|
||||||
const signedIn = !!getState().getIn(['meta', 'me']);
|
const signedIn = !!getState().getIn(['meta', 'me']);
|
||||||
|
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, ''));
|
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(fetchSearchRequest());
|
dispatch(fetchSearchRequest(type));
|
||||||
|
|
||||||
api(getState).get('/api/v2/search', {
|
api(getState).get('/api/v2/search', {
|
||||||
params: {
|
params: {
|
||||||
q: value,
|
q: value,
|
||||||
resolve: signedIn,
|
resolve: signedIn,
|
||||||
limit: 5,
|
limit: 11,
|
||||||
type,
|
type,
|
||||||
},
|
},
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
|
@ -59,7 +62,7 @@ export function submitSearch(type) {
|
||||||
dispatch(importFetchedStatuses(response.data.statuses));
|
dispatch(importFetchedStatuses(response.data.statuses));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(fetchSearchSuccess(response.data, value));
|
dispatch(fetchSearchSuccess(response.data, value, type));
|
||||||
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
|
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchSearchFail(error));
|
dispatch(fetchSearchFail(error));
|
||||||
|
@ -67,16 +70,18 @@ export function submitSearch(type) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchSearchRequest() {
|
export function fetchSearchRequest(searchType) {
|
||||||
return {
|
return {
|
||||||
type: SEARCH_FETCH_REQUEST,
|
type: SEARCH_FETCH_REQUEST,
|
||||||
|
searchType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchSearchSuccess(results, searchTerm) {
|
export function fetchSearchSuccess(results, searchTerm, searchType) {
|
||||||
return {
|
return {
|
||||||
type: SEARCH_FETCH_SUCCESS,
|
type: SEARCH_FETCH_SUCCESS,
|
||||||
results,
|
results,
|
||||||
|
searchType,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -90,15 +95,16 @@ export function fetchSearchFail(error) {
|
||||||
|
|
||||||
export const expandSearch = type => (dispatch, getState) => {
|
export const expandSearch = type => (dispatch, getState) => {
|
||||||
const value = getState().getIn(['search', 'value']);
|
const value = getState().getIn(['search', 'value']);
|
||||||
const offset = getState().getIn(['search', 'results', type]).size;
|
const offset = getState().getIn(['search', 'results', type]).size - 1;
|
||||||
|
|
||||||
dispatch(expandSearchRequest());
|
dispatch(expandSearchRequest(type));
|
||||||
|
|
||||||
api(getState).get('/api/v2/search', {
|
api(getState).get('/api/v2/search', {
|
||||||
params: {
|
params: {
|
||||||
q: value,
|
q: value,
|
||||||
type,
|
type,
|
||||||
offset,
|
offset,
|
||||||
|
limit: 11,
|
||||||
},
|
},
|
||||||
}).then(({ data }) => {
|
}).then(({ data }) => {
|
||||||
if (data.accounts) {
|
if (data.accounts) {
|
||||||
|
@ -116,8 +122,9 @@ export const expandSearch = type => (dispatch, getState) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expandSearchRequest = () => ({
|
export const expandSearchRequest = (searchType) => ({
|
||||||
type: SEARCH_EXPAND_REQUEST,
|
type: SEARCH_EXPAND_REQUEST,
|
||||||
|
searchType,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const expandSearchSuccess = (results, searchTerm, searchType) => ({
|
export const expandSearchSuccess = (results, searchTerm, searchType) => ({
|
||||||
|
@ -166,16 +173,34 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clickSearchResult = (q, type) => ({
|
export const clickSearchResult = (q, type) => (dispatch, getState) => {
|
||||||
type: SEARCH_RESULT_CLICK,
|
const previous = getState().getIn(['search', 'recent']);
|
||||||
|
const me = getState().getIn(['meta', 'me']);
|
||||||
|
const current = previous.add(fromJS({ type, q })).takeLast(4);
|
||||||
|
|
||||||
result: {
|
searchHistory.set(me, current.toJS());
|
||||||
type,
|
dispatch(updateSearchHistory(current));
|
||||||
q,
|
};
|
||||||
},
|
|
||||||
|
export const forgetSearchResult = q => (dispatch, getState) => {
|
||||||
|
const previous = getState().getIn(['search', 'recent']);
|
||||||
|
const me = getState().getIn(['meta', 'me']);
|
||||||
|
const current = previous.filterNot(result => result.get('q') === q);
|
||||||
|
|
||||||
|
searchHistory.set(me, current.toJS());
|
||||||
|
dispatch(updateSearchHistory(current));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSearchHistory = recent => ({
|
||||||
|
type: SEARCH_HISTORY_UPDATE,
|
||||||
|
recent,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const forgetSearchResult = q => ({
|
export const hydrateSearch = () => (dispatch, getState) => {
|
||||||
type: SEARCH_RESULT_FORGET,
|
const me = getState().getIn(['meta', 'me']);
|
||||||
q,
|
const history = searchHistory.get(me);
|
||||||
});
|
|
||||||
|
if (history !== null) {
|
||||||
|
dispatch(updateSearchHistory(history));
|
||||||
|
}
|
||||||
|
};
|
|
@ -2,6 +2,7 @@ import { Iterable, fromJS } from 'immutable';
|
||||||
|
|
||||||
import { hydrateCompose } from './compose';
|
import { hydrateCompose } from './compose';
|
||||||
import { importFetchedAccounts } from './importer';
|
import { importFetchedAccounts } from './importer';
|
||||||
|
import { hydrateSearch } from './search';
|
||||||
|
|
||||||
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
||||||
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
|
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
|
||||||
|
@ -20,6 +21,7 @@ export function hydrateStore(rawState) {
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(hydrateCompose());
|
dispatch(hydrateCompose());
|
||||||
|
dispatch(hydrateSearch());
|
||||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue