mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-05 12:54:35 +00:00
Merge branch 'release/26.08.0'
# Conflicts: # tools/manifest/gplay/release/aaptDump.txt
This commit is contained in:
@@ -28,6 +28,10 @@ changelog:
|
|||||||
labels:
|
labels:
|
||||||
- PR-Dependencies
|
- PR-Dependencies
|
||||||
|
|
||||||
|
- title: 💼 Element Pro
|
||||||
|
labels:
|
||||||
|
- PR-Element-Pro
|
||||||
|
|
||||||
- title: Others
|
- title: Others
|
||||||
labels:
|
labels:
|
||||||
- PR-misc
|
- PR-misc
|
||||||
|
|||||||
@@ -44,14 +44,14 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
# Skip in forks, it doesn't work even with the fallback token
|
# Skip in forks, it doesn't work even with the fallback token
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- name: ⏬ Checkout with LFS
|
- name: ⏬ Checkout with LFS
|
||||||
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
- name: Run World screenshots generation script
|
- name: Run World screenshots generation script
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ jobs:
|
|||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
name: Use JDK 21
|
name: Use JDK 21
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
name: Use JDK 21
|
name: Use JDK 21
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
concurrency:
|
concurrency:
|
||||||
group: maestro-test
|
group: maestro-test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
||||||
|
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -76,11 +76,11 @@ jobs:
|
|||||||
name: Dependency analysis
|
name: Dependency analysis
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
@@ -51,11 +51,11 @@ jobs:
|
|||||||
name: Search for invalid screenshot files
|
name: Search for invalid screenshot files
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
- name: Search for invalid screenshot files
|
- name: Search for invalid screenshot files
|
||||||
@@ -65,11 +65,11 @@ jobs:
|
|||||||
name: Search for invalid dependencies
|
name: Search for invalid dependencies
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
- name: Search for invalid dependencies
|
- name: Search for invalid dependencies
|
||||||
@@ -93,7 +93,7 @@ jobs:
|
|||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -168,7 +168,7 @@ jobs:
|
|||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -183,7 +183,7 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -213,7 +213,7 @@ jobs:
|
|||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -228,7 +228,7 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -254,7 +254,7 @@ jobs:
|
|||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -269,7 +269,7 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -295,7 +295,7 @@ jobs:
|
|||||||
group: ${{ github.ref == 'refs/heads/main' && format('check-docs-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-docs-develop-{0}', github.sha) || format('check-docs-{0}', github.ref) }}
|
group: ${{ github.ref == 'refs/heads/main' && format('check-docs-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-docs-develop-{0}', github.sha) || format('check-docs-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
@@ -319,7 +319,7 @@ jobs:
|
|||||||
name: Check shell scripts
|
name: Check shell scripts
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Run shellcheck
|
- name: Run shellcheck
|
||||||
@@ -333,10 +333,10 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
- uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0
|
||||||
|
|
||||||
upload_reports:
|
upload_reports:
|
||||||
name: Project Check Suite
|
name: Project Check Suite
|
||||||
@@ -344,7 +344,7 @@ jobs:
|
|||||||
needs: [konsist, lint, ktlint, detekt]
|
needs: [konsist, lint, ktlint, detekt]
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: ☕️ Use JDK 21
|
- name: ☕️ Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
group: ${{ format('build-release-main-enterprise-{0}', github.sha) }}
|
group: ${{ format('build-release-main-enterprise-{0}', github.sha) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Add SSH private keys for submodule repositories
|
- name: Add SSH private keys for submodule repositories
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Clone submodules
|
- name: Clone submodules
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -122,11 +122,11 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ jobs:
|
|||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
# Ensure we are building the branch and not the branch after being merged on develop
|
# Ensure we are building the branch and not the branch after being merged on develop
|
||||||
# https://github.com/actions/checkout/issues/881
|
# https://github.com/actions/checkout/issues/881
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ jobs:
|
|||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Use JDK 21
|
- name: Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
- name: Setup Localazy
|
- name: Setup Localazy
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
# No concurrency required, runs every time on a schedule.
|
# No concurrency required, runs every time on a schedule.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
- name: Install Prerequisite dependencies
|
- name: Install Prerequisite dependencies
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: ☕️ Use JDK 21
|
- name: ☕️ Use JDK 21
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
Generated
+34
@@ -1,5 +1,39 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
|
<JavaCodeStyleSettings>
|
||||||
|
<option name="IMPORT_LAYOUT_TABLE">
|
||||||
|
<value>
|
||||||
|
<package name="android" withSubpackages="true" static="true" />
|
||||||
|
<package name="androidx" withSubpackages="true" static="true" />
|
||||||
|
<package name="com" withSubpackages="true" static="true" />
|
||||||
|
<package name="junit" withSubpackages="true" static="true" />
|
||||||
|
<package name="net" withSubpackages="true" static="true" />
|
||||||
|
<package name="org" withSubpackages="true" static="true" />
|
||||||
|
<package name="java" withSubpackages="true" static="true" />
|
||||||
|
<package name="javax" withSubpackages="true" static="true" />
|
||||||
|
<package name="" withSubpackages="true" static="true" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="android" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="androidx" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="com" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="junit" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="net" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="org" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="java" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="javax" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="" withSubpackages="true" static="false" />
|
||||||
|
<emptyLine />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</JavaCodeStyleSettings>
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
|
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
|||||||
Generated
+2
-2
@@ -2,6 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="externalSystemId" value="Gradle" />
|
<option name="externalSystemId" value="Gradle" />
|
||||||
<option name="version" value="2.4.0" />
|
<option name="version" value="2.4.10" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -16,4 +16,4 @@ appId: ${MAESTRO_APP_ID}
|
|||||||
- extendedWaitUntil:
|
- extendedWaitUntil:
|
||||||
visible: "Device verified"
|
visible: "Device verified"
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
- tapOn: "Continue"
|
- tapOn: "Done"
|
||||||
|
|||||||
+70
@@ -1,3 +1,73 @@
|
|||||||
|
Changes in Element X v26.07.1
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v26.07.1 -->
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### ✨ Features
|
||||||
|
* Scroll to unread messages by @bmarty in https://github.com/element-hq/element-x-android/pull/7024
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Collapse runs of deleted messages in the timeline by @manfrommedan in https://github.com/element-hq/element-x-android/pull/7195
|
||||||
|
* Allow homeserver to disable encryption in Element Pro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7198
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Fix jump-to-unread requiring two taps for an out-of-window read marker by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/6943
|
||||||
|
* Add content description to video preview for screen readers by @bmarty in https://github.com/element-hq/element-x-android/pull/7137
|
||||||
|
* Fix accessibility for message composer input and read receipts button by @bmarty in https://github.com/element-hq/element-x-android/pull/7135
|
||||||
|
* Add accessibility label to search input by @bmarty in https://github.com/element-hq/element-x-android/pull/7133
|
||||||
|
* Fix timeline reading order distorted when TalkBack is active by @bmarty in https://github.com/element-hq/element-x-android/pull/7138
|
||||||
|
* Add programmatic label and focus indicator to PIN entry field by @bmarty in https://github.com/element-hq/element-x-android/pull/7132
|
||||||
|
* Fix timelines not loading after marking a room as read from notification when app was killed by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7202
|
||||||
|
### 🗣 Translations
|
||||||
|
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7192
|
||||||
|
### 🧱 Build
|
||||||
|
* Bump the default Gradle daemon heap to match what CI already uses by @PolynomialDivision in https://github.com/element-hq/element-x-android/pull/7152
|
||||||
|
* Update screenshots by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7155
|
||||||
|
* Update GitHub templates by @bmarty in https://github.com/element-hq/element-x-android/pull/7170
|
||||||
|
* Add check on merged manifest by @bmarty in https://github.com/element-hq/element-x-android/pull/7172
|
||||||
|
* Fix AAPT dump missing and reordering some permissions by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7193
|
||||||
|
* Try fixing DM creation in Maestro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7204
|
||||||
|
### 🚧 In development 🚧
|
||||||
|
* Support gallery messages by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6519
|
||||||
|
* Support gallery messages by @bmarty in https://github.com/element-hq/element-x-android/pull/7112
|
||||||
|
* Initial content scanner integration by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7139
|
||||||
|
* WIP : Set user status by @ganfra in https://github.com/element-hq/element-x-android/pull/7094
|
||||||
|
### Dependency upgrades
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.44.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7093
|
||||||
|
* Update dependency org.maplibre.gl:android-sdk to v13.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7129
|
||||||
|
* Update actions/setup-java action to v5.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7143
|
||||||
|
* Update danger/danger-js action to v13.0.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7142
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.46.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7140
|
||||||
|
* Update dependencyAnalysis to v3.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7144
|
||||||
|
* Update plugin gms_google_services to v4.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7086
|
||||||
|
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha21 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7007
|
||||||
|
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha22 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7158
|
||||||
|
* Update metro to v1.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7168
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.47.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7178
|
||||||
|
* Update dependency androidx.compose:compose-bom to v2026.06.01 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7174
|
||||||
|
* Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.5.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7180
|
||||||
|
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7201
|
||||||
|
* Update dependency io.github.zxing-cpp:android to v3.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7206
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7194
|
||||||
|
* Update dependency androidx.sqlite:sqlite-ktx to v2.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7177
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.15 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7216
|
||||||
|
### Others
|
||||||
|
* Timeline: jump-to-unread FAB and mark-as-read shortcut by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/6694
|
||||||
|
* Iterate on visual list by @bmarty in https://github.com/element-hq/element-x-android/pull/7141
|
||||||
|
* Fix wrong avatar shown in the thread list's top bar by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7044
|
||||||
|
* Update onboarding screen text style by @bmarty in https://github.com/element-hq/element-x-android/pull/7157
|
||||||
|
* Add full SVG support by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7020
|
||||||
|
* Add skin tone picker to the emoji picker by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6917
|
||||||
|
* Fix the message edit being lost when attaching media by @manfrommedan in https://github.com/element-hq/element-x-android/pull/7090
|
||||||
|
* Cleanup AndroidManifest.xml files. by @bmarty in https://github.com/element-hq/element-x-android/pull/7171
|
||||||
|
* Handle local network permission - Android 17 by @ganfra in https://github.com/element-hq/element-x-android/pull/7165
|
||||||
|
* Add item count text as the placeholder for in reply to box for galleries by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7176
|
||||||
|
* Default to dark theme for EC by @toger5 in https://github.com/element-hq/element-x-android/pull/7162
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @PolynomialDivision made their first contribution in https://github.com/element-hq/element-x-android/pull/7152
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.07.0...v26.07.1
|
||||||
|
|
||||||
Changes in Element X v26.07.0
|
Changes in Element X v26.07.0
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import io.element.android.libraries.di.BaseDirectory
|
|||||||
import io.element.android.libraries.di.CacheDirectory
|
import io.element.android.libraries.di.CacheDirectory
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||||
import io.element.android.libraries.recentemojis.api.EmojibaseProvider
|
|
||||||
import io.element.android.libraries.recentemojis.impl.DefaultEmojibaseProvider
|
|
||||||
import io.element.android.x.BuildConfig
|
import io.element.android.x.BuildConfig
|
||||||
import io.element.android.x.R
|
import io.element.android.x.R
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.CoroutineName
|
||||||
@@ -115,10 +113,4 @@ object AppModule {
|
|||||||
fun provideSnackbarDispatcher(): SnackbarDispatcher {
|
fun provideSnackbarDispatcher(): SnackbarDispatcher {
|
||||||
return SnackbarDispatcher()
|
return SnackbarDispatcher()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@SingleIn(AppScope::class)
|
|
||||||
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
|
|
||||||
return DefaultEmojibaseProvider(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
Submodule enterprise updated: cca96c4fc2...7bbf7db013
@@ -0,0 +1,2 @@
|
|||||||
|
Main changes in this version: bug fixes and improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.contentscanner.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to perform security scans in the contents for a given media source.
|
||||||
|
*
|
||||||
|
* The default implementation (FOSS) always returns a valid state.
|
||||||
|
* This will only process media when a [ContentScanner] is provided when building the client in Pro.
|
||||||
|
*/
|
||||||
|
fun interface ContentScannerService {
|
||||||
|
fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState)
|
||||||
|
}
|
||||||
@@ -32,4 +32,5 @@ dependencies {
|
|||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
}
|
}
|
||||||
|
|||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.contentscanner.impl
|
||||||
|
|
||||||
|
import io.element.android.features.contentscanner.api.ContentScannerService
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [ContentScannerService] that uses a [ContentScanner] to scan media sources.
|
||||||
|
*/
|
||||||
|
class DefaultContentScannerService(
|
||||||
|
private val contentScanner: ContentScanner,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
) : ContentScannerService {
|
||||||
|
private val dispatcher = coroutineDispatchers.io.limitedParallelism(4)
|
||||||
|
|
||||||
|
override fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState) {
|
||||||
|
for (mediaSource in mediaSources) {
|
||||||
|
val url = mediaSource.safeUrl
|
||||||
|
val currentState = contentValidationState.getCurrentMediaState(url)
|
||||||
|
if (currentState != ContentValidationValue.Unknown) continue
|
||||||
|
contentValidationState.update(url, ContentValidationValue.Loading)
|
||||||
|
|
||||||
|
coroutineScope.launch(dispatcher) {
|
||||||
|
contentScanner.scan(mediaSource)
|
||||||
|
.onSuccess { isValid ->
|
||||||
|
val contentValidationValue = if (isValid) ContentValidationValue.Valid else ContentValidationValue.Invalid
|
||||||
|
contentValidationState.update(url, contentValidationValue)
|
||||||
|
}
|
||||||
|
.onFailure { exception ->
|
||||||
|
when (exception) {
|
||||||
|
is IOException -> {
|
||||||
|
// If it's an IO-related exception, we can retry later, so we don't store the error
|
||||||
|
contentValidationState.update(url, ContentValidationValue.Unknown)
|
||||||
|
}
|
||||||
|
is ClientException.ContentScanner if exception.reason.isDangerous() -> {
|
||||||
|
// It's a dangerous content (banned mime-type?), we mark it as invalid
|
||||||
|
contentValidationState.update(url, ContentValidationValue.Invalid)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// For other exceptions, we cache the failure
|
||||||
|
contentValidationState.update(url, ContentValidationValue.UnrecoverableError(exception))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.contentscanner.impl
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.EventContentValidationCache
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
@SingleIn(RoomScope::class)
|
||||||
|
@ContributesBinding(RoomScope::class)
|
||||||
|
class DefaultEventContentValidationCache(
|
||||||
|
private val contentScanner: ContentScanner?,
|
||||||
|
) : EventContentValidationCache {
|
||||||
|
private val cache = ConcurrentHashMap<EventId, ContentValidationState>()
|
||||||
|
|
||||||
|
override operator fun get(eventId: EventId): ContentValidationState {
|
||||||
|
return cache.getOrPut(eventId) {
|
||||||
|
if (contentScanner != null) {
|
||||||
|
DefaultContentValidationState()
|
||||||
|
} else {
|
||||||
|
Timber.v("Content scanner is not available, returning NoopContentValidationState for eventId: $eventId")
|
||||||
|
NoopContentValidationState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.contentscanner.impl.di
|
||||||
|
|
||||||
|
import io.element.android.features.contentscanner.api.ContentScannerService
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Noop implementation of [ContentScannerService] that always returns success.
|
||||||
|
*
|
||||||
|
* This is used when the content scanner feature is not enabled or available.
|
||||||
|
*/
|
||||||
|
class AlwaysValidContentScannerService : ContentScannerService {
|
||||||
|
override fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState) {
|
||||||
|
// Always return success for the noop implementation
|
||||||
|
for (mediaSource in mediaSources) {
|
||||||
|
val url = mediaSource.safeUrl
|
||||||
|
contentValidationState.update(url, ContentValidationValue.Valid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.contentscanner.impl.di
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.BindingContainer
|
||||||
|
import dev.zacsweers.metro.ContributesTo
|
||||||
|
import dev.zacsweers.metro.Provides
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
|
import io.element.android.features.contentscanner.api.ContentScannerService
|
||||||
|
import io.element.android.features.contentscanner.impl.DefaultContentScannerService
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.di.annotations.RoomCoroutineScope
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
@BindingContainer
|
||||||
|
@ContributesTo(RoomScope::class)
|
||||||
|
object ContentScannerModule {
|
||||||
|
@Provides
|
||||||
|
@SingleIn(RoomScope::class)
|
||||||
|
fun providesContentScannerService(
|
||||||
|
contentScanner: ContentScanner?,
|
||||||
|
@RoomCoroutineScope coroutineScope: CoroutineScope,
|
||||||
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
): ContentScannerService {
|
||||||
|
return if (contentScanner != null) {
|
||||||
|
DefaultContentScannerService(
|
||||||
|
contentScanner = contentScanner,
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AlwaysValidContentScannerService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+208
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.contentscanner.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.test.media.aMediaSource
|
||||||
|
import io.element.android.libraries.matrix.test.scanner.FakeContentScanner
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class DefaultContentScannerServiceTest {
|
||||||
|
@Test
|
||||||
|
fun `scan with no media sources returns unknown`() = runTest {
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.success(true) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
).scan(
|
||||||
|
mediaSources = emptyList(),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
scanLambda.assertions().isNeverCalled()
|
||||||
|
assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.Unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `scan with single media source`() = runTest {
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.success(false) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
|
||||||
|
val mediaSource = aMediaSource("https://example.com/media.jpg")
|
||||||
|
createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
).scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
scanLambda.assertions().isCalledOnce().with(value(mediaSource))
|
||||||
|
assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.Invalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `scan with several media sources`() = runTest {
|
||||||
|
val mediaSourceA = aMediaSource("https://example.com/mediaA.jpg")
|
||||||
|
val mediaSourceB = aMediaSource("https://example.com/mediaB.jpg")
|
||||||
|
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { source ->
|
||||||
|
when (source) {
|
||||||
|
mediaSourceA -> Result.success(true)
|
||||||
|
mediaSourceB -> Result.success(false)
|
||||||
|
else -> error("Unexpected media source: $source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
|
||||||
|
createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
).scan(
|
||||||
|
mediaSources = listOf(mediaSourceA, mediaSourceB),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
scanLambda.assertions().isCalledExactly(2)
|
||||||
|
assertThat(contentValidationState.getMediaStateFlow(mediaSourceA.safeUrl).first()).isEqualTo(ContentValidationValue.Valid)
|
||||||
|
assertThat(contentValidationState.getMediaStateFlow(mediaSourceB.safeUrl).first()).isEqualTo(ContentValidationValue.Invalid)
|
||||||
|
assertThat(contentValidationState.overallStateFlow.first()).isEqualTo(ContentValidationValue.Invalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `scan won't be repeated for sources which are already being scanned`() = runTest {
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.success(false) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
|
||||||
|
val mediaSource = aMediaSource("https://example.com/media.jpg")
|
||||||
|
val service = createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
)
|
||||||
|
|
||||||
|
// First scan attempt
|
||||||
|
service.scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Immediately after, a new scan attempt is made for the same media source
|
||||||
|
service.scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
// The scan action was done just once, and the overall state is invalid as expected
|
||||||
|
scanLambda.assertions().isCalledOnce().with(value(mediaSource))
|
||||||
|
assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.Invalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `scan won't be repeated for sources which already have a cached value`() = runTest {
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.success(false) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = NoopContentValidationState(ContentValidationValue.Valid)
|
||||||
|
|
||||||
|
val mediaSource = aMediaSource("https://example.com/media.jpg")
|
||||||
|
val service = createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
)
|
||||||
|
|
||||||
|
service.scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
scanLambda.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `if a scan fails with a recoverable error, the content validation value will be back to Unknown`() = runTest {
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.failure(FileNotFoundException("Some IO exception")) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
|
||||||
|
val mediaSource = aMediaSource("https://example.com/media.jpg")
|
||||||
|
val service = createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
)
|
||||||
|
|
||||||
|
service.scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
scanLambda.assertions().isCalledOnce().with(value(mediaSource))
|
||||||
|
assertThat(contentValidationState.getCurrentMediaState(mediaSource.safeUrl)).isEqualTo(ContentValidationValue.Unknown)
|
||||||
|
assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.Unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `if a scan fails with an unrecoverable error, the content validation value will be back to UnrecoverableError`() = runTest {
|
||||||
|
val error = OutOfMemoryError("BOOM")
|
||||||
|
val scanLambda = lambdaRecorder<MediaSource, Result<Boolean>> { Result.failure(error) }
|
||||||
|
val scanner = FakeContentScanner(scan = scanLambda)
|
||||||
|
val contentValidationState = DefaultContentValidationState()
|
||||||
|
|
||||||
|
val mediaSource = aMediaSource("https://example.com/media.jpg")
|
||||||
|
val service = createDefaultContentScannerService(
|
||||||
|
contentScanner = scanner,
|
||||||
|
)
|
||||||
|
|
||||||
|
service.scan(
|
||||||
|
mediaSources = listOf(mediaSource),
|
||||||
|
contentValidationState = contentValidationState,
|
||||||
|
)
|
||||||
|
|
||||||
|
runCurrent()
|
||||||
|
|
||||||
|
scanLambda.assertions().isCalledOnce().with(value(mediaSource))
|
||||||
|
assertThat(contentValidationState.getCurrentMediaState(mediaSource.safeUrl)).isEqualTo(ContentValidationValue.UnrecoverableError(error))
|
||||||
|
assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.UnrecoverableError(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createDefaultContentScannerService(
|
||||||
|
contentScanner: ContentScanner = FakeContentScanner(),
|
||||||
|
sessionCoroutineScope: CoroutineScope = backgroundScope,
|
||||||
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
): DefaultContentScannerService {
|
||||||
|
return DefaultContentScannerService(
|
||||||
|
contentScanner = contentScanner,
|
||||||
|
coroutineScope = sessionCoroutineScope,
|
||||||
|
coroutineDispatchers = coroutineDispatchers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.contentscanner.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.scanner.FakeContentScanner
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DefaultEventContentValidationCacheTest {
|
||||||
|
@Test
|
||||||
|
fun `get returns NoopContentValidationState when ContentScanner is null`() {
|
||||||
|
// Given
|
||||||
|
val contentScanner: ContentScanner? = null
|
||||||
|
val cache = DefaultEventContentValidationCache(contentScanner)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val state = cache[AN_EVENT_ID]
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(state).isInstanceOf(NoopContentValidationState::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get returns an actual DefaultContentValidationState when ContentScanner is not null`() {
|
||||||
|
// Given
|
||||||
|
val contentScanner: ContentScanner? = FakeContentScanner()
|
||||||
|
val cache = DefaultEventContentValidationCache(contentScanner)
|
||||||
|
|
||||||
|
// When
|
||||||
|
val state = cache[AN_EVENT_ID]
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(state).isInstanceOf(DefaultContentValidationState::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.contentscanner.impl.di
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.contentscanner.impl.DefaultContentScannerService
|
||||||
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
|
import io.element.android.libraries.matrix.test.scanner.FakeContentScanner
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ContentScannerModuleTest {
|
||||||
|
@Test
|
||||||
|
fun `ContentScannerModule provides NoopContentScannerService when ContentScanner is null`() = runTest {
|
||||||
|
// Given
|
||||||
|
val contentScanner: ContentScanner? = null
|
||||||
|
val sessionCoroutineScope = backgroundScope
|
||||||
|
val coroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val contentScannerService = ContentScannerModule.providesContentScannerService(
|
||||||
|
contentScanner = contentScanner,
|
||||||
|
coroutineScope = sessionCoroutineScope,
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(contentScannerService).isInstanceOf(AlwaysValidContentScannerService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ContentScannerModule provides DefaultContentScannerService when ContentScanner is not null`() = runTest {
|
||||||
|
// Given
|
||||||
|
val contentScanner: ContentScanner? = FakeContentScanner()
|
||||||
|
val sessionCoroutineScope = backgroundScope
|
||||||
|
val coroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
|
||||||
|
// When
|
||||||
|
val contentScannerService = ContentScannerModule.providesContentScannerService(
|
||||||
|
contentScanner = contentScanner,
|
||||||
|
coroutineScope = sessionCoroutineScope,
|
||||||
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(contentScannerService).isInstanceOf(DefaultContentScannerService::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,4 +17,5 @@ dependencies {
|
|||||||
implementation(projects.libraries.compound)
|
implementation(projects.libraries.compound)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -11,6 +11,7 @@ package io.element.android.features.enterprise.api
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface EnterpriseService {
|
interface EnterpriseService {
|
||||||
@@ -41,6 +42,11 @@ interface EnterpriseService {
|
|||||||
*/
|
*/
|
||||||
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
|
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the overridden Element Well-Known data if it has been set, or null if not set.
|
||||||
|
*/
|
||||||
|
fun overriddenElementWellKnown(): ElementWellKnown?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ANY_ACCOUNT_PROVIDER = "*"
|
const val ANY_ACCOUNT_PROVIDER = "*"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ dependencies {
|
|||||||
api(projects.features.enterprise.api)
|
api(projects.features.enterprise.api)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
|
|
||||||
testCommonDependencies(libs)
|
testCommonDependencies(libs)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
|||||||
+3
@@ -15,6 +15,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark
|
|||||||
import io.element.android.features.enterprise.api.BugReportUrl
|
import io.element.android.features.enterprise.api.BugReportUrl
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
||||||
@@ -45,4 +46,6 @@ class DefaultEnterpriseService : EnterpriseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
|
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
|
||||||
|
|
||||||
|
override fun overriddenElementWellKnown(): ElementWellKnown? = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,5 +18,6 @@ dependencies {
|
|||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.compound)
|
implementation(projects.libraries.compound)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
implementation(projects.tests.testutils)
|
implementation(projects.tests.testutils)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -13,6 +13,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark
|
|||||||
import io.element.android.features.enterprise.api.BugReportUrl
|
import io.element.android.features.enterprise.api.BugReportUrl
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -31,6 +32,7 @@ class FakeEnterpriseService(
|
|||||||
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
||||||
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
|
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
|
||||||
private val tweakMasUrlResult: (String, String) -> String = { _, _ -> lambdaError() },
|
private val tweakMasUrlResult: (String, String) -> String = { _, _ -> lambdaError() },
|
||||||
|
private val overrideWellKnownResult: () -> ElementWellKnown? = { lambdaError() },
|
||||||
) : EnterpriseService {
|
) : EnterpriseService {
|
||||||
private val brandColorState = MutableStateFlow(initialBrandColor)
|
private val brandColorState = MutableStateFlow(initialBrandColor)
|
||||||
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
|
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
|
||||||
@@ -79,4 +81,8 @@ class FakeEnterpriseService(
|
|||||||
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? {
|
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? {
|
||||||
return getNoisyNotificationChannelIdResult(sessionId)
|
return getNoisyNotificationChannelIdResult(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun overriddenElementWellKnown(): ElementWellKnown? {
|
||||||
|
return overrideWellKnownResult()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-14
@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.calculateStartPadding
|
|||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.plus
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
@@ -65,6 +66,8 @@ import io.element.android.libraries.designsystem.theme.components.HorizontalFloa
|
|||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalFloatingToolbarSeparator
|
import io.element.android.libraries.designsystem.theme.components.HorizontalFloatingToolbarSeparator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
@@ -234,7 +237,15 @@ private fun HomeScaffold(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButtonPosition = FabPosition.Center,
|
floatingActionButtonPosition = FabPosition.Center,
|
||||||
|
contentWindowInsets = scaffoldScrollableContentInsets,
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
|
val outerPadding = PaddingValues(
|
||||||
|
start = padding.calculateStartPadding(LocalLayoutDirection.current),
|
||||||
|
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
||||||
|
// Remove these two lines once https://issuetracker.google.com/issues/436432313 has been fixed
|
||||||
|
bottom = padding.calculateBottomPadding(),
|
||||||
|
top = padding.calculateTopPadding()
|
||||||
|
)
|
||||||
val contentPadding = PaddingValues(
|
val contentPadding = PaddingValues(
|
||||||
bottom = 96.dp,
|
bottom = 96.dp,
|
||||||
)
|
)
|
||||||
@@ -251,18 +262,10 @@ private fun HomeScaffold(
|
|||||||
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
|
||||||
onRoomClick = ::onRoomClick,
|
onRoomClick = ::onRoomClick,
|
||||||
onCreateRoomClick = onStartChatClick,
|
onCreateRoomClick = onStartChatClick,
|
||||||
contentPadding = contentPadding,
|
contentPadding = lazyColumnContentPadding + contentPadding,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(
|
.padding(outerPadding)
|
||||||
PaddingValues(
|
.consumeWindowInsets(outerPadding)
|
||||||
start = padding.calculateStartPadding(LocalLayoutDirection.current),
|
|
||||||
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
|
||||||
// Remove these two lines once https://issuetracker.google.com/issues/436432313 has been fixed
|
|
||||||
bottom = padding.calculateBottomPadding(),
|
|
||||||
top = padding.calculateTopPadding()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.consumeWindowInsets(padding)
|
|
||||||
.hazeSource(state = hazeState)
|
.hazeSource(state = hazeState)
|
||||||
)
|
)
|
||||||
SpaceFiltersView(roomListState.spaceFiltersState)
|
SpaceFiltersView(roomListState.spaceFiltersState)
|
||||||
@@ -271,10 +274,10 @@ private fun HomeScaffold(
|
|||||||
HomeSpacesView(
|
HomeSpacesView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(outerPadding)
|
||||||
.consumeWindowInsets(padding)
|
.consumeWindowInsets(outerPadding)
|
||||||
.hazeSource(state = hazeState),
|
.hazeSource(state = hazeState),
|
||||||
contentPadding = contentPadding,
|
contentPadding = lazyColumnContentPadding + contentPadding,
|
||||||
state = state.homeSpacesState,
|
state = state.homeSpacesState,
|
||||||
lazyListState = spacesLazyListState,
|
lazyListState = spacesLazyListState,
|
||||||
onSpaceClick = { spaceId ->
|
onSpaceClick = { spaceId ->
|
||||||
|
|||||||
+8
-2
@@ -12,8 +12,12 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.pager.VerticalPager
|
import androidx.compose.foundation.pager.VerticalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
@@ -96,13 +100,15 @@ fun HomeTopBar(
|
|||||||
spaceFiltersState: SpaceFiltersState,
|
spaceFiltersState: SpaceFiltersState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val contentPadding = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.backgroundVerticalGradient(
|
.backgroundVerticalGradient(
|
||||||
isVisible = !areSearchResultsDisplayed,
|
isVisible = !areSearchResultsDisplayed,
|
||||||
)
|
)
|
||||||
.statusBarsPadding(),
|
.statusBarsPadding()
|
||||||
|
.padding(contentPadding),
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
scrolledContainerColor = Color.Transparent,
|
scrolledContainerColor = Color.Transparent,
|
||||||
@@ -153,7 +159,7 @@ fun HomeTopBar(
|
|||||||
TopAppBarScrollBehaviorLayout(scrollBehavior = scrollBehavior) {
|
TopAppBarScrollBehaviorLayout(scrollBehavior = scrollBehavior) {
|
||||||
RoomListFiltersView(
|
RoomListFiltersView(
|
||||||
state = filtersState,
|
state = filtersState,
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
modifier = Modifier.padding(bottom = 16.dp).padding(contentPadding)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-11
@@ -63,6 +63,8 @@ import io.element.android.libraries.designsystem.theme.roomListRoomName
|
|||||||
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
||||||
import io.element.android.libraries.matrix.api.notification.CallIntent
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
|
||||||
import io.element.android.libraries.matrix.ui.components.InviteSenderView
|
import io.element.android.libraries.matrix.ui.components.InviteSenderView
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -126,7 +128,8 @@ internal fun RoomSummaryRow(
|
|||||||
NameAndTimestampRow(
|
NameAndTimestampRow(
|
||||||
name = room.name,
|
name = room.name,
|
||||||
timestamp = room.timestamp,
|
timestamp = room.timestamp,
|
||||||
isHighlighted = room.isHighlighted
|
isHighlighted = room.isHighlighted,
|
||||||
|
dmUserStatus = room.dmUserStatus,
|
||||||
)
|
)
|
||||||
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
|
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
|
||||||
}
|
}
|
||||||
@@ -142,7 +145,8 @@ internal fun RoomSummaryRow(
|
|||||||
NameAndTimestampRow(
|
NameAndTimestampRow(
|
||||||
name = room.name,
|
name = room.name,
|
||||||
timestamp = null,
|
timestamp = null,
|
||||||
isHighlighted = room.isHighlighted
|
isHighlighted = room.isHighlighted,
|
||||||
|
dmUserStatus = null,
|
||||||
)
|
)
|
||||||
if (room.canonicalAlias != null) {
|
if (room.canonicalAlias != null) {
|
||||||
Text(
|
Text(
|
||||||
@@ -218,22 +222,21 @@ private fun NameAndTimestampRow(
|
|||||||
name: String?,
|
name: String?,
|
||||||
timestamp: String?,
|
timestamp: String?,
|
||||||
isHighlighted: Boolean,
|
isHighlighted: Boolean,
|
||||||
|
dmUserStatus: DisplayedStatus?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = spacedBy(16.dp)
|
horizontalArrangement = spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
val displayName = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name)
|
||||||
modifier = Modifier
|
DisplayNameWithStatus(
|
||||||
.weight(1f)
|
name = displayName,
|
||||||
.clipToBounds(),
|
status = dmUserStatus,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name),
|
nameColor = ElementTheme.colors.roomListRoomName,
|
||||||
fontStyle = FontStyle.Italic.takeIf { name == null },
|
nameFontStyle = FontStyle.Italic.takeIf { name == null },
|
||||||
color = ElementTheme.colors.roomListRoomName,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
// Timestamp
|
// Timestamp
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
+2
@@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.room.CallIntentConsensus
|
|||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
|
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import io.element.android.libraries.matrix.ui.model.dmUserStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -76,6 +77,7 @@ class RoomListRoomSummaryFactory(
|
|||||||
}.toImmutableList(),
|
}.toImmutableList(),
|
||||||
isTombstoned = roomInfo.successorRoom != null,
|
isTombstoned = roomInfo.successorRoom != null,
|
||||||
isSpace = roomInfo.isSpace,
|
isSpace = roomInfo.isSpace,
|
||||||
|
dmUserStatus = roomInfo.dmUserStatus(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
|||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.notification.CallIntent
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ data class RoomListRoomSummary(
|
|||||||
val isTombstoned: Boolean,
|
val isTombstoned: Boolean,
|
||||||
val heroes: ImmutableList<AvatarData>,
|
val heroes: ImmutableList<AvatarData>,
|
||||||
val isSpace: Boolean,
|
val isSpace: Boolean,
|
||||||
|
val dmUserStatus: DisplayedStatus?,
|
||||||
) {
|
) {
|
||||||
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
||||||
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
||||||
|
|||||||
+4
-1
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.notification.CallIntent
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@@ -182,6 +183,7 @@ internal fun aRoomListRoomSummary(
|
|||||||
heroes: List<AvatarData> = emptyList(),
|
heroes: List<AvatarData> = emptyList(),
|
||||||
isTombstoned: Boolean = false,
|
isTombstoned: Boolean = false,
|
||||||
isSpace: Boolean = false,
|
isSpace: Boolean = false,
|
||||||
|
dmUserStatus: DisplayedStatus? = null,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = id,
|
id = id,
|
||||||
roomId = RoomId(id),
|
roomId = RoomId(id),
|
||||||
@@ -204,5 +206,6 @@ internal fun aRoomListRoomSummary(
|
|||||||
canonicalAlias = canonicalAlias,
|
canonicalAlias = canonicalAlias,
|
||||||
heroes = heroes.toImmutableList(),
|
heroes = heroes.toImmutableList(),
|
||||||
isTombstoned = isTombstoned,
|
isTombstoned = isTombstoned,
|
||||||
isSpace = isSpace
|
isSpace = isSpace,
|
||||||
|
dmUserStatus = dmUserStatus,
|
||||||
)
|
)
|
||||||
|
|||||||
+9
-1
@@ -13,9 +13,15 @@ import androidx.compose.animation.AnimatedVisibility
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
@@ -153,7 +159,8 @@ private fun RoomListSearchContent(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom),
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -167,6 +174,7 @@ private fun RoomListSearchContent(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = state.results,
|
items = state.results,
|
||||||
|
|||||||
+8
-7
@@ -15,9 +15,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -45,6 +43,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchField
|
import io.element.android.libraries.designsystem.theme.components.SearchField
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
|
import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -75,9 +75,7 @@ fun SpaceFiltersView(
|
|||||||
}
|
}
|
||||||
if (sheetState.isVisible || isSelecting) {
|
if (sheetState.isVisible || isSelecting) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.systemBarsPadding()
|
|
||||||
.navigationBarsPadding(),
|
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
if (state is SpaceFiltersState.Selecting) {
|
if (state is SpaceFiltersState.Selecting) {
|
||||||
@@ -85,6 +83,7 @@ fun SpaceFiltersView(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollable = false,
|
scrollable = false,
|
||||||
|
contentWindowInsets = { scaffoldScrollableContentInsets },
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -113,7 +112,7 @@ private fun SpaceFiltersBottomSheetContent(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.padding(vertical = 16.dp)
|
modifier = modifier.padding(top = 16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.screen_roomlist_your_spaces),
|
text = stringResource(R.string.screen_roomlist_your_spaces),
|
||||||
@@ -131,7 +130,9 @@ private fun SpaceFiltersBottomSheetContent(
|
|||||||
placeholder = stringResource(CommonStrings.action_search),
|
placeholder = stringResource(CommonStrings.action_search),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
LazyColumn {
|
LazyColumn(
|
||||||
|
contentPadding = lazyColumnContentPadding,
|
||||||
|
) {
|
||||||
items(filters) { filter ->
|
items(filters) { filter ->
|
||||||
SpaceFilterItem(
|
SpaceFilterItem(
|
||||||
filter = filter,
|
filter = filter,
|
||||||
|
|||||||
+4
-1
@@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -87,6 +88,7 @@ internal fun createRoomListRoomSummary(
|
|||||||
timestamp: String? = null,
|
timestamp: String? = null,
|
||||||
isTombstoned: Boolean = false,
|
isTombstoned: Boolean = false,
|
||||||
isSpace: Boolean = false,
|
isSpace: Boolean = false,
|
||||||
|
dmUserStatus: DisplayedStatus? = null,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = A_ROOM_ID.value,
|
id = A_ROOM_ID.value,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
@@ -109,5 +111,6 @@ internal fun createRoomListRoomSummary(
|
|||||||
isDm = false,
|
isDm = false,
|
||||||
heroes = heroes.toImmutableList(),
|
heroes = heroes.toImmutableList(),
|
||||||
isTombstoned = isTombstoned,
|
isTombstoned = isTombstoned,
|
||||||
isSpace = isSpace
|
isSpace = isSpace,
|
||||||
|
dmUserStatus = dmUserStatus,
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-4
@@ -88,7 +88,7 @@ class DefaultInvitePeoplePresenter(
|
|||||||
override fun present(): InvitePeopleState {
|
override fun present(): InvitePeopleState {
|
||||||
val roomMembers = remember { mutableStateOf<AsyncData<ImmutableList<RoomMember>>>(AsyncData.Loading()) }
|
val roomMembers = remember { mutableStateOf<AsyncData<ImmutableList<RoomMember>>>(AsyncData.Loading()) }
|
||||||
val selectedUsers = remember { mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf()) }
|
val selectedUsers = remember { mutableStateOf<ImmutableList<MatrixUser>>(persistentListOf()) }
|
||||||
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(SearchBarResultState.Initial()) }
|
val searchResults = remember { mutableStateOf<SearchBarResultState<ImmutableList<InvitableUser>>>(SearchBarResultState.Initial) }
|
||||||
val queryState = rememberTextFieldState()
|
val queryState = rememberTextFieldState()
|
||||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||||
@@ -332,15 +332,15 @@ class DefaultInvitePeoplePresenter(
|
|||||||
showSearchLoader: MutableState<Boolean>,
|
showSearchLoader: MutableState<Boolean>,
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
) = withContext(coroutineDispatchers.io) {
|
) = withContext(coroutineDispatchers.io) {
|
||||||
searchResults.value = SearchBarResultState.Initial()
|
searchResults.value = SearchBarResultState.Initial
|
||||||
showSearchLoader.value = false
|
showSearchLoader.value = false
|
||||||
val joinedMembers = roomMembers.value.dataOrNull().orEmpty()
|
val joinedMembers = roomMembers.value.dataOrNull().orEmpty()
|
||||||
|
|
||||||
userRepository.search(searchQuery).onEach { state ->
|
userRepository.search(searchQuery).onEach { state ->
|
||||||
showSearchLoader.value = state.isSearching
|
showSearchLoader.value = state.isSearching
|
||||||
searchResults.value = when {
|
searchResults.value = when {
|
||||||
state.results.isEmpty() && state.isSearching -> SearchBarResultState.Initial()
|
state.results.isEmpty() && state.isSearching -> SearchBarResultState.Initial
|
||||||
state.results.isEmpty() && !state.isSearching -> SearchBarResultState.NoResultsFound()
|
state.results.isEmpty() && !state.isSearching -> SearchBarResultState.NoResultsFound
|
||||||
else -> SearchBarResultState.Results(state.results.map { result ->
|
else -> SearchBarResultState.Results(state.results.map { result ->
|
||||||
val existingMembership = joinedMembers.firstOrNull { j -> j.userId == result.matrixUser.userId }?.membership
|
val existingMembership = joinedMembers.firstOrNull { j -> j.userId == result.matrixUser.userId }?.membership
|
||||||
val isJoined = existingMembership == RoomMembershipState.JOIN
|
val isJoined = existingMembership == RoomMembershipState.JOIN
|
||||||
|
|||||||
+2
-2
@@ -33,7 +33,7 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider<Defau
|
|||||||
aDefaultInvitePeopleState(canInvite = true, selectedUsers = aMatrixUserList().toImmutableList()),
|
aDefaultInvitePeopleState(canInvite = true, selectedUsers = aMatrixUserList().toImmutableList()),
|
||||||
aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query"),
|
aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query"),
|
||||||
aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query", selectedUsers = aMatrixUserList().toImmutableList()),
|
aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query", selectedUsers = aMatrixUserList().toImmutableList()),
|
||||||
aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResultsFound()),
|
aDefaultInvitePeopleState(isSearchActive = true, searchQuery = "some query", searchResults = SearchBarResultState.NoResultsFound),
|
||||||
aDefaultInvitePeopleState(
|
aDefaultInvitePeopleState(
|
||||||
isSearchActive = true,
|
isSearchActive = true,
|
||||||
canInvite = true,
|
canInvite = true,
|
||||||
@@ -115,7 +115,7 @@ private fun aDefaultInvitePeopleState(
|
|||||||
room: AsyncData<Unit> = AsyncData.Success(Unit),
|
room: AsyncData<Unit> = AsyncData.Success(Unit),
|
||||||
canInvite: Boolean = false,
|
canInvite: Boolean = false,
|
||||||
searchQuery: String = "",
|
searchQuery: String = "",
|
||||||
searchResults: SearchBarResultState<ImmutableList<InvitableUser>> = SearchBarResultState.Initial(),
|
searchResults: SearchBarResultState<ImmutableList<InvitableUser>> = SearchBarResultState.Initial,
|
||||||
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
showSearchLoader: Boolean = false,
|
showSearchLoader: Boolean = false,
|
||||||
|
|||||||
@@ -2,4 +2,8 @@
|
|||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_invite_users_already_a_member">"Allerede medlem"</string>
|
<string name="screen_invite_users_already_a_member">"Allerede medlem"</string>
|
||||||
<string name="screen_invite_users_already_invited">"Allerede invitert"</string>
|
<string name="screen_invite_users_already_invited">"Allerede invitert"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_subtitle_multiple_users">"Du har for øyeblikket ingen samtaler med disse kontaktene. Bekreft at du vil invitere dem til dette rommet før du fortsetter."</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_subtitle_one_user">"Du har for øyeblikket ingen samtaler med denne kontakten. Bekreft at du vil invitere vedkommende til dette rommet før du fortsetter."</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_title_mutiple_users">"Vil du invitere nye kontakter til dette rommet?"</string>
|
||||||
|
<string name="screen_invite_users_confirm_dialog_title_one_user">"Vil du invitere en ny kontakt til dette rommet?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+4
-8
@@ -95,7 +95,7 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||||||
resultState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
resultState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||||
assertThat(awaitItemAsDefault().searchQuery.text.toString()).isEqualTo("some query")
|
assertThat(awaitItemAsDefault().searchQuery.text.toString()).isEqualTo("some query")
|
||||||
resultState.eventSink(InvitePeopleEvents.CloseSearch)
|
resultState.eventSink(InvitePeopleEvents.CloseSearch)
|
||||||
skipItems(2)
|
skipItems(1)
|
||||||
awaitItemAsDefault().also {
|
awaitItemAsDefault().also {
|
||||||
assertThat(it.isSearchActive).isFalse()
|
assertThat(it.isSearchActive).isFalse()
|
||||||
assertThat(it.searchQuery.text.toString()).isEmpty()
|
assertThat(it.searchQuery.text.toString()).isEmpty()
|
||||||
@@ -116,7 +116,7 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||||||
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
|
||||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||||
repository.emitState(UserSearchResultState(results = emptyList(), isSearching = true))
|
repository.emitState(UserSearchResultState(results = emptyList(), isSearching = true))
|
||||||
skipItems(3)
|
skipItems(2)
|
||||||
awaitItemAsDefault().also { state ->
|
awaitItemAsDefault().also { state ->
|
||||||
assertThat(state.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
assertThat(state.searchResults).isInstanceOf(SearchBarResultState.Initial::class.java)
|
||||||
assertThat(state.showSearchLoader).isTrue()
|
assertThat(state.showSearchLoader).isTrue()
|
||||||
@@ -145,7 +145,6 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||||||
|
|
||||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||||
repository.emitStateWithUsers(users = aMatrixUserList())
|
repository.emitStateWithUsers(users = aMatrixUserList())
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
val resultState = awaitItemAsDefault()
|
val resultState = awaitItemAsDefault()
|
||||||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
@@ -198,7 +197,6 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||||||
|
|
||||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||||
repository.emitStateWithUsers(users = aMatrixUserList())
|
repository.emitStateWithUsers(users = aMatrixUserList())
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
val resultState = awaitItemAsDefault()
|
val resultState = awaitItemAsDefault()
|
||||||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
@@ -265,7 +263,6 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||||||
it
|
it
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
skipItems(1)
|
|
||||||
|
|
||||||
val resultState = awaitItemAsDefault()
|
val resultState = awaitItemAsDefault()
|
||||||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
@@ -329,7 +326,7 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||||||
|
|
||||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||||
repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
|
repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
|
||||||
skipItems(2)
|
skipItems(1)
|
||||||
|
|
||||||
val resultState = awaitItemAsDefault()
|
val resultState = awaitItemAsDefault()
|
||||||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
@@ -366,7 +363,6 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||||||
|
|
||||||
assertThat(repository.providedQuery).isEqualTo("some query")
|
assertThat(repository.providedQuery).isEqualTo("some query")
|
||||||
repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
|
repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
|
||||||
skipItems(1)
|
|
||||||
awaitItemAsDefault().also { state ->
|
awaitItemAsDefault().also { state ->
|
||||||
// selectedUser is not selected
|
// selectedUser is not selected
|
||||||
assertThat(state.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
assertThat(state.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
@@ -732,7 +728,7 @@ internal class DefaultInvitePeoplePresenterTest {
|
|||||||
isSearching = true
|
isSearching = true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
skipItems(3)
|
skipItems(2)
|
||||||
|
|
||||||
awaitItemAsDefault().run {
|
awaitItemAsDefault().run {
|
||||||
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
|
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
|
||||||
|
|||||||
+6
-2
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.plus
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -73,6 +74,8 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@@ -94,7 +97,8 @@ fun KnockRequestsListView(
|
|||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding),
|
.consumeWindowInsets(padding),
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
contentWindowInsets = scaffoldScrollableContentInsets,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +136,7 @@ private fun KnockRequestsListContent(
|
|||||||
onAcceptClick = ::onAcceptClick,
|
onAcceptClick = ::onAcceptClick,
|
||||||
onDeclineClick = ::onDeclineClick,
|
onDeclineClick = ::onDeclineClick,
|
||||||
onBanClick = ::onBanClick,
|
onBanClick = ::onBanClick,
|
||||||
contentPadding = PaddingValues(bottom = bottomPaddingInPixels.toDp()),
|
contentPadding = lazyColumnContentPadding + PaddingValues(bottom = bottomPaddingInPixels.toDp()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -9,7 +9,7 @@
|
|||||||
package io.element.android.features.licenses.impl.details
|
package io.element.android.features.licenses.impl.details
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.plus
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -25,6 +25,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -41,9 +43,10 @@ fun DependenciesDetailsView(
|
|||||||
navigationIcon = { BackButton(onClick = onBack) },
|
navigationIcon = { BackButton(onClick = onBack) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
contentWindowInsets = scaffoldScrollableContentInsets,
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.padding(contentPadding),
|
contentPadding = contentPadding + lazyColumnContentPadding,
|
||||||
) {
|
) {
|
||||||
val licenses = licenseItem.licenses.orEmpty() +
|
val licenses = licenseItem.licenses.orEmpty() +
|
||||||
licenseItem.unknownLicenses.orEmpty()
|
licenseItem.unknownLicenses.orEmpty()
|
||||||
|
|||||||
+8
-2
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.plus
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -34,6 +35,8 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextField
|
import io.element.android.libraries.designsystem.theme.components.TextField
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -52,10 +55,11 @@ fun DependencyLicensesListView(
|
|||||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
contentWindowInsets = scaffoldScrollableContentInsets,
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
.padding(contentPadding + lazyColumnContentPadding)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
if (state.licenses.isSuccess()) {
|
if (state.licenses.isSuccess()) {
|
||||||
@@ -72,7 +76,9 @@ fun DependencyLicensesListView(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LazyColumn {
|
LazyColumn(
|
||||||
|
contentPadding = lazyColumnContentPadding,
|
||||||
|
) {
|
||||||
when (state.licenses) {
|
when (state.licenses) {
|
||||||
is AsyncData.Failure -> item {
|
is AsyncData.Failure -> item {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
+19
-3
@@ -9,21 +9,25 @@
|
|||||||
|
|
||||||
package io.element.android.features.linknewdevice.impl.screens.desktop
|
package io.element.android.features.linknewdevice.impl.screens.desktop
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.linknewdevice.impl.R
|
import io.element.android.features.linknewdevice.impl.R
|
||||||
import io.element.android.libraries.designsystem.atomic.organisms.NumberedListOrganism
|
import io.element.android.libraries.designsystem.atomic.organisms.NumberedListOrganism
|
||||||
@@ -34,6 +38,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||||||
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
import io.element.android.libraries.designsystem.theme.LocalBuildMeta
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
||||||
import io.element.android.libraries.permissions.api.PermissionsView
|
import io.element.android.libraries.permissions.api.PermissionsView
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
@@ -62,6 +67,7 @@ fun DesktopNoticeView(
|
|||||||
title = stringResource(R.string.screen_link_new_device_desktop_title, appName),
|
title = stringResource(R.string.screen_link_new_device_desktop_title, appName),
|
||||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
|
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
isScrollable = true,
|
||||||
buttons = {
|
buttons = {
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(R.string.screen_link_new_device_desktop_submit),
|
text = stringResource(R.string.screen_link_new_device_desktop_submit),
|
||||||
@@ -71,11 +77,10 @@ fun DesktopNoticeView(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
NumberedListOrganism(
|
NumberedListOrganism(
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
items = persistentListOf(
|
items = persistentListOf(
|
||||||
AnnotatedString(stringResource(R.string.screen_link_new_device_desktop_step1, appName)),
|
AnnotatedString(stringResource(R.string.screen_link_new_device_desktop_step1, appName)),
|
||||||
annotatedTextWithBold(
|
annotatedTextWithBold(
|
||||||
@@ -88,6 +93,17 @@ fun DesktopNoticeView(
|
|||||||
AnnotatedString(stringResource(R.string.screen_link_new_device_desktop_step3)),
|
AnnotatedString(stringResource(R.string.screen_link_new_device_desktop_step3)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(ElementTheme.colors.bgCriticalSubtle)
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
text = stringResource(R.string.screen_link_new_device_desktop_warning),
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = ElementTheme.colors.textCriticalPrimary,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<string name="screen_link_new_device_desktop_step3">"Zeskanuj kod QR za pomocą tego urządzenia"</string>
|
<string name="screen_link_new_device_desktop_step3">"Zeskanuj kod QR za pomocą tego urządzenia"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"Gotowy do skanowania"</string>
|
<string name="screen_link_new_device_desktop_submit">"Gotowy do skanowania"</string>
|
||||||
<string name="screen_link_new_device_desktop_title">"Otwórz %1$s na komputerze stacjonarnym, aby uzyskać kod QR"</string>
|
<string name="screen_link_new_device_desktop_title">"Otwórz %1$s na komputerze stacjonarnym, aby uzyskać kod QR"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_warning">"Zeskanuj kod QR dopiero po wykonaniu powyższych kroków na swoim urządzeniu. Nigdy nie skanuj kodu przesłanego przez inną osobę."</string>
|
||||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Liczby nie pasują do siebie"</string>
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Liczby nie pasują do siebie"</string>
|
||||||
<string name="screen_link_new_device_enter_number_notice">"Wprowadź 2-cyfrowy kod"</string>
|
<string name="screen_link_new_device_enter_number_notice">"Wprowadź 2-cyfrowy kod"</string>
|
||||||
<string name="screen_link_new_device_enter_number_subtitle">"Pozwoli to sprawdzić, czy połączenie z drugim urządzeniem jest bezpieczne."</string>
|
<string name="screen_link_new_device_enter_number_subtitle">"Pozwoli to sprawdzić, czy połączenie z drugim urządzeniem jest bezpieczne."</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<string name="screen_link_new_device_desktop_step3">"Зіскануйте QR-код цим пристроєм"</string>
|
<string name="screen_link_new_device_desktop_step3">"Зіскануйте QR-код цим пристроєм"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"Готовий до сканування"</string>
|
<string name="screen_link_new_device_desktop_submit">"Готовий до сканування"</string>
|
||||||
<string name="screen_link_new_device_desktop_title">"Відкрийте %1$s на комп\'ютері, щоб отримати QR-код"</string>
|
<string name="screen_link_new_device_desktop_title">"Відкрийте %1$s на комп\'ютері, щоб отримати QR-код"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_warning">"Скануйте QR-код лише після виконання наведених вище кроків на своєму власному пристрої. Ніколи не скануйте код, надісланий іншою особою."</string>
|
||||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Цифри не збігаються"</string>
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Цифри не збігаються"</string>
|
||||||
<string name="screen_link_new_device_enter_number_notice">"Введіть 2-значний код"</string>
|
<string name="screen_link_new_device_enter_number_notice">"Введіть 2-значний код"</string>
|
||||||
<string name="screen_link_new_device_enter_number_subtitle">"Це підтвердить, що з\'єднання з іншим пристроєм захищене."</string>
|
<string name="screen_link_new_device_enter_number_subtitle">"Це підтвердить, що з\'єднання з іншим пристроєм захищене."</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<string name="screen_link_new_device_desktop_step3">"使用此设备扫描二维码"</string>
|
<string name="screen_link_new_device_desktop_step3">"使用此设备扫描二维码"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"准备进行扫描"</string>
|
<string name="screen_link_new_device_desktop_submit">"准备进行扫描"</string>
|
||||||
<string name="screen_link_new_device_desktop_title">"在台式电脑上打开 %1$s 以获取二维码"</string>
|
<string name="screen_link_new_device_desktop_title">"在台式电脑上打开 %1$s 以获取二维码"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_warning">"确保仅在属于你自己的设备上按上述步骤操作后再扫描二维码。绝对不要扫描来自他人的二维码。"</string>
|
||||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"数字不匹配"</string>
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"数字不匹配"</string>
|
||||||
<string name="screen_link_new_device_enter_number_notice">"输入两位数字的代码"</string>
|
<string name="screen_link_new_device_enter_number_notice">"输入两位数字的代码"</string>
|
||||||
<string name="screen_link_new_device_enter_number_subtitle">"这将验证你与其它设备的连接是否安全。"</string>
|
<string name="screen_link_new_device_enter_number_subtitle">"这将验证你与其它设备的连接是否安全。"</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<string name="screen_link_new_device_desktop_step3">"Scan the QR code with this device"</string>
|
<string name="screen_link_new_device_desktop_step3">"Scan the QR code with this device"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"Ready to scan"</string>
|
<string name="screen_link_new_device_desktop_submit">"Ready to scan"</string>
|
||||||
<string name="screen_link_new_device_desktop_title">"Open %1$s on a desktop computer to get the QR code"</string>
|
<string name="screen_link_new_device_desktop_title">"Open %1$s on a desktop computer to get the QR code"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_warning">"Only scan a QR code after following the steps above on your own device. Never scan a code sent by another person."</string>
|
||||||
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"The numbers don’t match"</string>
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"The numbers don’t match"</string>
|
||||||
<string name="screen_link_new_device_enter_number_notice">"Enter 2-digit code"</string>
|
<string name="screen_link_new_device_enter_number_notice">"Enter 2-digit code"</string>
|
||||||
<string name="screen_link_new_device_enter_number_subtitle">"This will verify that the connection to your other device is secure."</string>
|
<string name="screen_link_new_device_enter_number_subtitle">"This will verify that the connection to your other device is secure."</string>
|
||||||
|
|||||||
+25
-9
@@ -52,9 +52,11 @@ class DefaultActiveLiveLocationShareManager(
|
|||||||
private val clock: SystemClock,
|
private val clock: SystemClock,
|
||||||
private val sessionObserver: SessionObserver,
|
private val sessionObserver: SessionObserver,
|
||||||
) : ActiveLiveLocationShareManager, LiveLocationReceiver {
|
) : ActiveLiveLocationShareManager, LiveLocationReceiver {
|
||||||
|
private data class Timeout(val expiresAt: Instant, val job: Job)
|
||||||
|
|
||||||
private val isSetup = AtomicBoolean(false)
|
private val isSetup = AtomicBoolean(false)
|
||||||
private val cachedRooms = ConcurrentHashMap<RoomId, JoinedRoom>()
|
private val cachedRooms = ConcurrentHashMap<RoomId, JoinedRoom>()
|
||||||
private val timeoutJobs = ConcurrentHashMap<RoomId, Job>()
|
private val timeouts = ConcurrentHashMap<RoomId, Timeout>()
|
||||||
private val syncedActiveShareIds = MutableStateFlow<Set<BeaconId>>(emptySet())
|
private val syncedActiveShareIds = MutableStateFlow<Set<BeaconId>>(emptySet())
|
||||||
private val localSharingRoomIds = MutableStateFlow<Set<RoomId>>(emptySet())
|
private val localSharingRoomIds = MutableStateFlow<Set<RoomId>>(emptySet())
|
||||||
override val sharingRoomIds: StateFlow<Set<RoomId>> = localSharingRoomIds
|
override val sharingRoomIds: StateFlow<Set<RoomId>> = localSharingRoomIds
|
||||||
@@ -139,9 +141,9 @@ class DefaultActiveLiveLocationShareManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onLocationUpdate(location: Location) {
|
override suspend fun onLocationUpdate(location: Location) {
|
||||||
val activeSharesCount = localSharingRoomIds.value.size
|
val active = stopExpiredShares()
|
||||||
Timber.d("ActiveLiveLocationShareManager received location update for $activeSharesCount active share(s)")
|
Timber.d("ActiveLiveLocationShareManager received location update for ${active.size} active share(s)")
|
||||||
localSharingRoomIds.value.forEach { roomId ->
|
active.forEach { roomId ->
|
||||||
Timber.d("ActiveLiveLocationShareManager sending location to room $roomId")
|
Timber.d("ActiveLiveLocationShareManager sending location to room $roomId")
|
||||||
sendLiveLocation(roomId, location)
|
sendLiveLocation(roomId, location)
|
||||||
.onFailure {
|
.onFailure {
|
||||||
@@ -150,6 +152,19 @@ class DefaultActiveLiveLocationShareManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun stopExpiredShares(): List<RoomId> {
|
||||||
|
val nowMillis = clock.epochMillis()
|
||||||
|
val (expired, active) = localSharingRoomIds.value.partition { roomId ->
|
||||||
|
val timeout = timeouts[roomId] ?: return@partition false
|
||||||
|
timeout.expiresAt.toEpochMilliseconds() <= nowMillis
|
||||||
|
}
|
||||||
|
expired.forEach { roomId ->
|
||||||
|
Timber.d("ActiveLiveLocationShareManager location tick detected expired share for room $roomId, stopping")
|
||||||
|
stopShare(roomId)
|
||||||
|
}
|
||||||
|
return active
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun sendLiveLocation(roomId: RoomId, location: Location): Result<Unit> {
|
private suspend fun sendLiveLocation(roomId: RoomId, location: Location): Result<Unit> {
|
||||||
val room = cachedRooms.getOrPut(roomId) {
|
val room = cachedRooms.getOrPut(roomId) {
|
||||||
matrixClient.getJoinedRoom(roomId) ?: return Result.failure(IllegalStateException("No room found for $roomId"))
|
matrixClient.getJoinedRoom(roomId) ?: return Result.failure(IllegalStateException("No room found for $roomId"))
|
||||||
@@ -192,20 +207,21 @@ class DefaultActiveLiveLocationShareManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun scheduleTimeout(roomId: RoomId, expiresAt: Instant) {
|
private fun scheduleTimeout(roomId: RoomId, expiresAt: Instant) {
|
||||||
timeoutJobs.remove(roomId)?.cancel()
|
timeouts.remove(roomId)?.job?.cancel()
|
||||||
val delayMillis = expiresAt.toEpochMilliseconds() - clock.epochMillis()
|
val delayMillis = expiresAt.toEpochMilliseconds() - clock.epochMillis()
|
||||||
timeoutJobs[roomId] = matrixClient.sessionCoroutineScope.launch {
|
val job = matrixClient.sessionCoroutineScope.launch {
|
||||||
delay(delayMillis)
|
delay(delayMillis)
|
||||||
stopShare(roomId)
|
stopShare(roomId)
|
||||||
.onFailure { error ->
|
.onFailure { error ->
|
||||||
Timber.e(error, "ActiveLiveLocationShareManager failed to stop timed out share for room $roomId")
|
Timber.e(error, "ActiveLiveLocationShareManager failed to stop timed out share for room $roomId")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
timeouts[roomId] = Timeout(expiresAt = expiresAt, job = job)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stopLocalShare(roomId: RoomId) {
|
private suspend fun stopLocalShare(roomId: RoomId) {
|
||||||
Timber.d("ActiveLiveLocationShareManager stop local share in $roomId")
|
Timber.d("ActiveLiveLocationShareManager stop local share in $roomId")
|
||||||
timeoutJobs.remove(roomId)?.cancel()
|
timeouts.remove(roomId)?.job?.cancel()
|
||||||
val wasSharing = localSharingRoomIds.getAndUpdate { it - roomId }.isNotEmpty()
|
val wasSharing = localSharingRoomIds.getAndUpdate { it - roomId }.isNotEmpty()
|
||||||
cachedRooms.remove(roomId)?.close()
|
cachedRooms.remove(roomId)?.close()
|
||||||
liveLocationStore.removeLiveLocationExpiry(roomId)
|
liveLocationStore.removeLiveLocationExpiry(roomId)
|
||||||
@@ -220,11 +236,11 @@ class DefaultActiveLiveLocationShareManager(
|
|||||||
sessionObserver.removeListener(sessionListener)
|
sessionObserver.removeListener(sessionListener)
|
||||||
coordinator.unregister(matrixClient.sessionId)
|
coordinator.unregister(matrixClient.sessionId)
|
||||||
liveLocationStore.clear()
|
liveLocationStore.clear()
|
||||||
|
timeouts.values.forEach { it.job.cancel() }
|
||||||
|
timeouts.clear()
|
||||||
for (room in cachedRooms.values) {
|
for (room in cachedRooms.values) {
|
||||||
room.close()
|
room.close()
|
||||||
timeoutJobs[room.roomId]?.cancel()
|
|
||||||
}
|
}
|
||||||
timeoutJobs.clear()
|
|
||||||
cachedRooms.clear()
|
cachedRooms.clear()
|
||||||
localSharingRoomIds.value = emptySet()
|
localSharingRoomIds.value = emptySet()
|
||||||
syncedActiveShareIds.value = emptySet()
|
syncedActiveShareIds.value = emptySet()
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_share_location_live_location_disclaimer_title">"Din sanntids lokasjonshistorikk lagres i rommet og vil være synlig for medlemmene etter at økten er avsluttet."</string>
|
||||||
<string name="screen_share_location_live_location_duration_picker_title">"Velg hvor lenge du vil dele din sanntidslokasjon."</string>
|
<string name="screen_share_location_live_location_duration_picker_title">"Velg hvor lenge du vil dele din sanntidslokasjon."</string>
|
||||||
|
<string name="screen_share_location_live_location_missing_permissions">"Du har ikke tillatelse til å dele din sanntidslokasjon i dette rommet"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+47
@@ -373,6 +373,53 @@ class DefaultActiveLiveLocationShareManagerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `location update after expiry stops the share and does not send location`() = runTest {
|
||||||
|
val liveLocationStore = createInMemoryLiveLocationStore()
|
||||||
|
val beaconInfoUpdates = MutableSharedFlow<BeaconInfoUpdate>(replay = 1)
|
||||||
|
val stopLiveLocationShareResult = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val sendLiveLocationResult = lambdaRecorder<String, Result<Unit>> { _ -> Result.success(Unit) }
|
||||||
|
val clock = FakeSystemClock(epochMillisResult = 1_000L)
|
||||||
|
val manager = createManager(
|
||||||
|
client = FakeMatrixClient(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
sessionCoroutineScope = backgroundScope,
|
||||||
|
ownBeaconInfoUpdates = beaconInfoUpdates,
|
||||||
|
).apply {
|
||||||
|
givenGetRoomResult(
|
||||||
|
A_ROOM_ID,
|
||||||
|
FakeJoinedRoom(
|
||||||
|
startLiveLocationShareResult = { Result.success(AN_EVENT_ID) },
|
||||||
|
stopLiveLocationShareResult = stopLiveLocationShareResult,
|
||||||
|
sendLiveLocationResult = sendLiveLocationResult,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
coordinator = createCoordinator(),
|
||||||
|
liveLocationStore = liveLocationStore,
|
||||||
|
clock = clock,
|
||||||
|
)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val startResult = async { manager.startShare(A_ROOM_ID, 1.minutes) }
|
||||||
|
beaconInfoUpdates.emit(BeaconInfoUpdate(roomId = A_ROOM_ID, beaconId = AN_EVENT_ID, isLive = true))
|
||||||
|
assertThat(startResult.await().isSuccess).isTrue()
|
||||||
|
|
||||||
|
// Advance the clock past the expiry so the next location tick is beyond the deadline.
|
||||||
|
clock.epochMillisResult = 1_000L + 1.minutes.inWholeMilliseconds + 1_000L
|
||||||
|
|
||||||
|
manager.sharingRoomIds.test {
|
||||||
|
assertThat(awaitItem()).containsExactly(A_ROOM_ID)
|
||||||
|
manager.onLocationUpdate(io.element.android.features.location.api.Location(lat = 0.0, lon = 0.0, accuracy = null))
|
||||||
|
assertThat(awaitItem()).isEmpty()
|
||||||
|
advanceUntilIdle()
|
||||||
|
}
|
||||||
|
assertThat(liveLocationStore.getLiveLocationExpiries()).doesNotContainKey(A_ROOM_ID)
|
||||||
|
assert(sendLiveLocationResult).isNeverCalled()
|
||||||
|
// stopLiveLocationShare is called twice: once defensively in startShare, once from the tick-triggered stopShare.
|
||||||
|
assert(stopLiveLocationShareResult).isCalledExactly(2)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `session deleted clears local state`() = runTest {
|
fun `session deleted clears local state`() = runTest {
|
||||||
val startServiceRecorder = lambdaRecorder<Unit> { }
|
val startServiceRecorder = lambdaRecorder<Unit> { }
|
||||||
|
|||||||
+2
-1
@@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
@@ -40,7 +41,7 @@ fun AccountProviderOtherView(
|
|||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onClick() }
|
.clickable(role = Role.Button) { onClick() }
|
||||||
) {
|
) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
+2
-1
@@ -19,6 +19,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
@@ -45,7 +46,7 @@ fun AccountProviderView(
|
|||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onClick() }
|
.clickable(role = Role.Button) { onClick() }
|
||||||
) {
|
) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
+2
@@ -141,6 +141,8 @@ class QrCodeLoginFlowNode(
|
|||||||
}
|
}
|
||||||
QrLoginException.CheckCodeAlreadySent,
|
QrLoginException.CheckCodeAlreadySent,
|
||||||
QrLoginException.CheckCodeCannotBeSent,
|
QrLoginException.CheckCodeCannotBeSent,
|
||||||
|
QrLoginException.ContinuationAlreadySent,
|
||||||
|
QrLoginException.ContinuationCannotBeSent,
|
||||||
QrLoginException.UnsupportedQrCodeType,
|
QrLoginException.UnsupportedQrCodeType,
|
||||||
QrLoginException.Unknown -> {
|
QrLoginException.Unknown -> {
|
||||||
Timber.e(error, "Unknown error found")
|
Timber.e(error, "Unknown error found")
|
||||||
|
|||||||
+5
-1
@@ -39,6 +39,8 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
|||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817
|
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817
|
||||||
@@ -59,7 +61,8 @@ fun ChangeAccountProviderView(
|
|||||||
title = {},
|
title = {},
|
||||||
navigationIcon = { BackButton(onClick = onBackClick) }
|
navigationIcon = { BackButton(onClick = onBackClick) }
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
contentWindowInsets = scaffoldScrollableContentInsets,
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -71,6 +74,7 @@ fun ChangeAccountProviderView(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(state = rememberScrollState())
|
.verticalScroll(state = rememberScrollState())
|
||||||
|
.padding(lazyColumnContentPadding)
|
||||||
) {
|
) {
|
||||||
IconTitleSubtitleMolecule(
|
IconTitleSubtitleMolecule(
|
||||||
modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp),
|
modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp),
|
||||||
|
|||||||
+5
-1
@@ -44,6 +44,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
import io.element.android.libraries.matrix.api.auth.OAuthDetails
|
||||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -71,7 +73,8 @@ fun ChooseAccountProviderView(
|
|||||||
title = {},
|
title = {},
|
||||||
navigationIcon = { BackButton(onClick = onBackClick) }
|
navigationIcon = { BackButton(onClick = onBackClick) }
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
contentWindowInsets = scaffoldScrollableContentInsets,
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -84,6 +87,7 @@ fun ChooseAccountProviderView(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(state = rememberScrollState())
|
.verticalScroll(state = rememberScrollState())
|
||||||
|
.padding(lazyColumnContentPadding)
|
||||||
) {
|
) {
|
||||||
IconTitleSubtitleMolecule(
|
IconTitleSubtitleMolecule(
|
||||||
modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp),
|
modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp),
|
||||||
|
|||||||
+10
-4
@@ -36,6 +36,7 @@ import androidx.compose.ui.focus.FocusDirection
|
|||||||
import androidx.compose.ui.platform.LocalAutofillManager
|
import androidx.compose.ui.platform.LocalAutofillManager
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.semantics.contentType
|
import androidx.compose.ui.semantics.contentType
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -211,10 +212,15 @@ private fun LoginForm(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
trailingIcon = if (loginFieldState.isNotEmpty()) {
|
trailingIcon = if (loginFieldState.isNotEmpty()) {
|
||||||
{
|
{
|
||||||
Box(Modifier.clickable {
|
Box(
|
||||||
loginFieldState = ""
|
Modifier.clickable(
|
||||||
eventSink(LoginPasswordEvents.SetLogin(""))
|
onClickLabel = stringResource(CommonStrings.action_clear),
|
||||||
}) {
|
role = Role.Button,
|
||||||
|
) {
|
||||||
|
loginFieldState = ""
|
||||||
|
eventSink(LoginPasswordEvents.SetLogin(""))
|
||||||
|
}
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Close(),
|
imageVector = CompoundIcons.Close(),
|
||||||
contentDescription = stringResource(CommonStrings.action_clear),
|
contentDescription = stringResource(CommonStrings.action_clear),
|
||||||
|
|||||||
+2
-1
@@ -27,6 +27,7 @@ import androidx.compose.ui.BiasAlignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -336,7 +337,7 @@ private fun OnBoardingButtons(
|
|||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable(role = Role.Button) {
|
||||||
state.eventSink(OnBoardingEvents.OnVersionClick)
|
state.eventSink(OnBoardingEvents.OnVersionClick)
|
||||||
}
|
}
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
|
|||||||
-9
@@ -17,7 +17,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
|
||||||
import io.element.android.features.login.impl.qrcode.QrCodeLoginManager
|
import io.element.android.features.login.impl.qrcode.QrCodeLoginManager
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
@@ -39,7 +38,6 @@ class QrCodeScanPresenter(
|
|||||||
private val qrCodeLoginDataFactory: MatrixQrCodeLoginDataFactory,
|
private val qrCodeLoginDataFactory: MatrixQrCodeLoginDataFactory,
|
||||||
private val qrCodeLoginManager: QrCodeLoginManager,
|
private val qrCodeLoginManager: QrCodeLoginManager,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl,
|
|
||||||
) : Presenter<QrCodeScanState> {
|
) : Presenter<QrCodeScanState> {
|
||||||
private var isScanning by mutableStateOf(true)
|
private var isScanning by mutableStateOf(true)
|
||||||
|
|
||||||
@@ -97,13 +95,6 @@ class QrCodeScanPresenter(
|
|||||||
val data = qrCodeLoginDataFactory.parseQrCodeData(code).onFailure {
|
val data = qrCodeLoginDataFactory.parseQrCodeData(code).onFailure {
|
||||||
Timber.e(it, "Error parsing QR code data")
|
Timber.e(it, "Error parsing QR code data")
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
val serverName = data.serverName()
|
|
||||||
if (serverName != null) {
|
|
||||||
defaultAccountProviderAccessControl.assertIsAllowedToConnectToAccountProvider(
|
|
||||||
title = serverName,
|
|
||||||
accountProviderUrl = serverName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
data
|
data
|
||||||
}.runCatchingUpdatingState(codeScannedAction)
|
}.runCatchingUpdatingState(codeScannedAction)
|
||||||
}.invokeOnCompletion {
|
}.invokeOnCompletion {
|
||||||
|
|||||||
+10
-4
@@ -34,6 +34,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.focus.FocusDirection
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
@@ -125,10 +126,15 @@ fun SearchAccountProviderView(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
trailingIcon = if (userInputState.isNotEmpty()) {
|
trailingIcon = if (userInputState.isNotEmpty()) {
|
||||||
{
|
{
|
||||||
Box(Modifier.clickable {
|
Box(
|
||||||
userInputState = ""
|
Modifier.clickable(
|
||||||
eventSink(SearchAccountProviderEvents.UserInput(""))
|
onClickLabel = stringResource(CommonStrings.action_clear),
|
||||||
}) {
|
role = Role.Button,
|
||||||
|
) {
|
||||||
|
userInputState = ""
|
||||||
|
eventSink(SearchAccountProviderEvents.UserInput(""))
|
||||||
|
}
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Close(),
|
imageVector = CompoundIcons.Close(),
|
||||||
contentDescription = stringResource(CommonStrings.action_clear)
|
contentDescription = stringResource(CommonStrings.action_clear)
|
||||||
|
|||||||
@@ -37,7 +37,14 @@
|
|||||||
<string name="screen_login_subtitle">"Matrix er et åpent nettverk for sikker, desentralisert kommunikasjon."</string>
|
<string name="screen_login_subtitle">"Matrix er et åpent nettverk for sikker, desentralisert kommunikasjon."</string>
|
||||||
<string name="screen_login_title">"Velkommen tilbake!"</string>
|
<string name="screen_login_title">"Velkommen tilbake!"</string>
|
||||||
<string name="screen_login_title_with_homeserver">"Logg inn på %1$s"</string>
|
<string name="screen_login_title_with_homeserver">"Logg inn på %1$s"</string>
|
||||||
|
<string name="screen_missing_key_backup_open_element_classic">"Åpne Element Classic"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_1">"Åpne Element Classic på enheten din"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_2_android">"Gå til Innstillinger → Sikkerhet og personvern"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_4">"Følg instruksjonene for å aktivere nøkkellagringen din"</string>
|
||||||
|
<string name="screen_missing_key_backup_step_5">"Kom tilbake til %1$s"</string>
|
||||||
|
<string name="screen_missing_key_backup_title">"Aktiver nøkkellagringen din før du går videre til %1$s"</string>
|
||||||
<string name="screen_onboarding_app_version">"Versjon %1$s"</string>
|
<string name="screen_onboarding_app_version">"Versjon %1$s"</string>
|
||||||
|
<string name="screen_onboarding_checking_account">"Sjekker kontoen"</string>
|
||||||
<string name="screen_onboarding_sign_in_manually">"Logg på manuelt"</string>
|
<string name="screen_onboarding_sign_in_manually">"Logg på manuelt"</string>
|
||||||
<string name="screen_onboarding_sign_in_to">"Logg inn på %1$s"</string>
|
<string name="screen_onboarding_sign_in_to">"Logg inn på %1$s"</string>
|
||||||
<string name="screen_onboarding_sign_in_with_qr_code">"Logg inn med QR-kode"</string>
|
<string name="screen_onboarding_sign_in_with_qr_code">"Logg inn med QR-kode"</string>
|
||||||
|
|||||||
-53
@@ -9,18 +9,12 @@
|
|||||||
package io.element.android.features.login.impl.screens.qrcode.scan
|
package io.element.android.features.login.impl.screens.qrcode.scan
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
|
||||||
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
|
||||||
import io.element.android.features.login.impl.qrcode.FakeQrCodeLoginManager
|
import io.element.android.features.login.impl.qrcode.FakeQrCodeLoginManager
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
||||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginDataFactory
|
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginDataFactory
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
@@ -53,9 +47,6 @@ class QrCodeScanPresenterTest {
|
|||||||
)
|
)
|
||||||
val presenter = createQrCodeScanPresenter(
|
val presenter = createQrCodeScanPresenter(
|
||||||
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
||||||
enterpriseService = FakeEnterpriseService(
|
|
||||||
isAllowedToConnectToHomeserverResult = { true },
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
@@ -66,44 +57,6 @@ class QrCodeScanPresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - scanned QR code successfully, but homeserver not allowed`() = runTest {
|
|
||||||
val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(
|
|
||||||
parseQrCodeLoginDataResult = {
|
|
||||||
Result.success(
|
|
||||||
FakeMatrixQrCodeLoginData(
|
|
||||||
serverNameResult = { "example.com" }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val presenter = createQrCodeScanPresenter(
|
|
||||||
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
|
||||||
enterpriseService = FakeEnterpriseService(
|
|
||||||
isAllowedToConnectToHomeserverResult = { false },
|
|
||||||
defaultHomeserverListResult = { listOf("element.io") },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf()))
|
|
||||||
assertThat(awaitItem().isScanning).isFalse()
|
|
||||||
assertThat(awaitItem().authenticationAction.isLoading()).isTrue()
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(
|
|
||||||
(state.authenticationAction
|
|
||||||
.errorOrNull() as AccountProviderAccessException.UnauthorizedAccountProviderException).unauthorisedAccountProviderTitle
|
|
||||||
)
|
|
||||||
.isEqualTo("example.com")
|
|
||||||
assertThat(
|
|
||||||
(state.authenticationAction
|
|
||||||
.errorOrNull() as AccountProviderAccessException.UnauthorizedAccountProviderException).authorisedAccountProviderTitles
|
|
||||||
)
|
|
||||||
.containsExactly("element.io")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - scanned QR code failed and can be retried`() = runTest {
|
fun `present - scanned QR code failed and can be retried`() = runTest {
|
||||||
val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(
|
val qrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(
|
||||||
@@ -153,15 +106,9 @@ class QrCodeScanPresenterTest {
|
|||||||
qrCodeLoginDataFactory: FakeMatrixQrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(),
|
qrCodeLoginDataFactory: FakeMatrixQrCodeLoginDataFactory = FakeMatrixQrCodeLoginDataFactory(),
|
||||||
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
qrCodeLoginManager: FakeQrCodeLoginManager = FakeQrCodeLoginManager(),
|
qrCodeLoginManager: FakeQrCodeLoginManager = FakeQrCodeLoginManager(),
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
|
||||||
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
|
||||||
) = QrCodeScanPresenter(
|
) = QrCodeScanPresenter(
|
||||||
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
||||||
qrCodeLoginManager = qrCodeLoginManager,
|
qrCodeLoginManager = qrCodeLoginManager,
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
|
||||||
enterpriseService = enterpriseService,
|
|
||||||
wellknownRetriever = wellknownRetriever,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ dependencies {
|
|||||||
implementation(projects.libraries.mediaupload.api)
|
implementation(projects.libraries.mediaupload.api)
|
||||||
implementation(projects.libraries.permissions.api)
|
implementation(projects.libraries.permissions.api)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.recentemojis.api)
|
implementation(projects.libraries.emoji.api)
|
||||||
implementation(projects.libraries.roomselect.api)
|
implementation(projects.libraries.roomselect.api)
|
||||||
implementation(projects.libraries.slashcommands.api)
|
implementation(projects.libraries.slashcommands.api)
|
||||||
implementation(projects.libraries.audio.api)
|
implementation(projects.libraries.audio.api)
|
||||||
@@ -62,6 +62,7 @@ dependencies {
|
|||||||
implementation(projects.libraries.uiUtils)
|
implementation(projects.libraries.uiUtils)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.features.networkmonitor.api)
|
implementation(projects.features.networkmonitor.api)
|
||||||
|
implementation(projects.features.contentscanner.api)
|
||||||
implementation(projects.services.analytics.compose)
|
implementation(projects.services.analytics.compose)
|
||||||
implementation(projects.services.appnavstate.api)
|
implementation(projects.services.appnavstate.api)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
@@ -105,6 +106,7 @@ dependencies {
|
|||||||
testImplementation(projects.libraries.testtags)
|
testImplementation(projects.libraries.testtags)
|
||||||
testImplementation(projects.features.poll.test)
|
testImplementation(projects.features.poll.test)
|
||||||
testImplementation(projects.libraries.eventformatter.test)
|
testImplementation(projects.libraries.eventformatter.test)
|
||||||
testImplementation(projects.libraries.recentemojis.test)
|
testImplementation(projects.libraries.emoji.impl)
|
||||||
|
testImplementation(projects.libraries.emoji.test)
|
||||||
testImplementation(projects.libraries.slashcommands.test)
|
testImplementation(projects.libraries.slashcommands.test)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -57,6 +57,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.blurHash
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.duration
|
import io.element.android.features.messages.impl.timeline.model.event.duration
|
||||||
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||||
import io.element.android.features.poll.api.create.CreatePollMode
|
import io.element.android.features.poll.api.create.CreatePollMode
|
||||||
@@ -155,6 +156,7 @@ class MessagesFlowNode(
|
|||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val canUseOverlay: Boolean,
|
val canUseOverlay: Boolean,
|
||||||
|
val blurHash: String?,
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -377,6 +379,7 @@ class MessagesFlowNode(
|
|||||||
mediaInfo = navTarget.mediaInfo,
|
mediaInfo = navTarget.mediaInfo,
|
||||||
mediaSource = navTarget.mediaSource,
|
mediaSource = navTarget.mediaSource,
|
||||||
thumbnailSource = navTarget.thumbnailSource,
|
thumbnailSource = navTarget.thumbnailSource,
|
||||||
|
blurHash = navTarget.blurHash,
|
||||||
)
|
)
|
||||||
val callback = object : MediaViewerEntryPoint.Callback {
|
val callback = object : MediaViewerEntryPoint.Callback {
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
@@ -828,6 +831,7 @@ class MessagesFlowNode(
|
|||||||
mediaSource = galleryItem.mediaSource,
|
mediaSource = galleryItem.mediaSource,
|
||||||
thumbnailSource = galleryItem.thumbnailSource,
|
thumbnailSource = galleryItem.thumbnailSource,
|
||||||
type = galleryItem.type.toMediaViewerType(),
|
type = galleryItem.type.toMediaViewerType(),
|
||||||
|
blurHash = galleryItem.blurhash,
|
||||||
)
|
)
|
||||||
}.reversed()
|
}.reversed()
|
||||||
NavTarget.GalleryViewer(
|
NavTarget.GalleryViewer(
|
||||||
@@ -861,6 +865,7 @@ class MessagesFlowNode(
|
|||||||
mediaSource = attachment.mediaSource,
|
mediaSource = attachment.mediaSource,
|
||||||
thumbnailSource = attachment.thumbnailSource,
|
thumbnailSource = attachment.thumbnailSource,
|
||||||
type = GalleryItemData.Type.File,
|
type = GalleryItemData.Type.File,
|
||||||
|
blurHash = null,
|
||||||
)
|
)
|
||||||
}.reversed()
|
}.reversed()
|
||||||
NavTarget.GalleryViewer(
|
NavTarget.GalleryViewer(
|
||||||
@@ -920,6 +925,7 @@ class MessagesFlowNode(
|
|||||||
mediaSource = mediaSource,
|
mediaSource = mediaSource,
|
||||||
thumbnailSource = thumbnailSource,
|
thumbnailSource = thumbnailSource,
|
||||||
canUseOverlay = canUseOverlay,
|
canUseOverlay = canUseOverlay,
|
||||||
|
blurHash = content.blurHash(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
@@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
|
|||||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineEvent
|
import io.element.android.features.messages.impl.timeline.TimelineEvent
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
||||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
@@ -54,6 +55,7 @@ import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
|||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
|
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||||
import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
|
import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
@@ -66,6 +68,8 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
|
|||||||
import io.element.android.libraries.matrix.api.room.alias.matches
|
import io.element.android.libraries.matrix.api.room.alias.matches
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.EventContentValidationCache
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.LocalEventContentValidationState
|
||||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -97,6 +101,8 @@ class MessagesNode(
|
|||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer,
|
private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer,
|
||||||
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
|
private val roomMemberModerationRenderer: RoomMemberModerationRenderer,
|
||||||
|
private val eventContentValidationCache: EventContentValidationCache,
|
||||||
|
private val emojiPickerRenderer: EmojiPickerRenderer,
|
||||||
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val focusedEventId: EventId?,
|
val focusedEventId: EventId?,
|
||||||
@@ -261,6 +267,7 @@ class MessagesNode(
|
|||||||
val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard()
|
val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard()
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
||||||
|
LocalEventContentValidationState provides eventContentValidationCache,
|
||||||
) {
|
) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
||||||
@@ -325,6 +332,15 @@ class MessagesNode(
|
|||||||
onViewRequestsClick = callback::navigateToKnockRequestsList,
|
onViewRequestsClick = callback::navigateToKnockRequestsList,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
customReactionBottomSheet = {
|
||||||
|
CustomReactionBottomSheet(
|
||||||
|
state = state.customReactionState,
|
||||||
|
onSelectEmoji = { uniqueId, emoji ->
|
||||||
|
state.eventSink(MessagesEvent.ToggleReaction(emoji.unicode, uniqueId))
|
||||||
|
},
|
||||||
|
emojiPickerRenderer = emojiPickerRenderer,
|
||||||
|
)
|
||||||
|
},
|
||||||
onThreadsListClick = callback::navigateToThreadsList,
|
onThreadsListClick = callback::navigateToThreadsList,
|
||||||
)
|
)
|
||||||
roomMemberModerationRenderer.Render(
|
roomMemberModerationRenderer.Render(
|
||||||
|
|||||||
+3
-1
@@ -69,6 +69,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
|
|||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||||
|
import io.element.android.libraries.emoji.api.recentemojis.AddRecentEmoji
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.toThreadId
|
import io.element.android.libraries.matrix.api.core.toThreadId
|
||||||
@@ -83,9 +84,9 @@ import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsSta
|
|||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||||
|
import io.element.android.libraries.matrix.ui.model.dmUserStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||||
import io.element.android.libraries.recentemojis.api.AddRecentEmoji
|
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
@@ -319,6 +320,7 @@ class MessagesPresenter(
|
|||||||
appName = buildMeta.applicationName,
|
appName = buildMeta.applicationName,
|
||||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
dmUserVerificationState = dmUserVerificationState,
|
dmUserVerificationState = dmUserVerificationState,
|
||||||
|
dmUserStatus = roomInfo.dmUserStatus(),
|
||||||
roomMemberModerationState = roomMemberModerationState,
|
roomMemberModerationState = roomMemberModerationState,
|
||||||
topBarSharedHistoryIcon = topBarSharedHistoryIcon,
|
topBarSharedHistoryIcon = topBarSharedHistoryIcon,
|
||||||
successorRoom = roomInfo.successorRoom,
|
successorRoom = roomInfo.successorRoom,
|
||||||
|
|||||||
+2
@@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
|||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||||
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class MessagesState(
|
data class MessagesState(
|
||||||
@@ -53,6 +54,7 @@ data class MessagesState(
|
|||||||
val appName: String,
|
val appName: String,
|
||||||
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
||||||
val dmUserVerificationState: IdentityState?,
|
val dmUserVerificationState: IdentityState?,
|
||||||
|
val dmUserStatus: DisplayedStatus?,
|
||||||
val roomMemberModerationState: RoomMemberModerationState,
|
val roomMemberModerationState: RoomMemberModerationState,
|
||||||
/** Type of "shared history" icon to show in the top bar. */
|
/** Type of "shared history" icon to show in the top bar. */
|
||||||
val topBarSharedHistoryIcon: SharedHistoryIcon,
|
val topBarSharedHistoryIcon: SharedHistoryIcon,
|
||||||
|
|||||||
+3
-3
@@ -50,10 +50,10 @@ import io.element.android.libraries.matrix.api.core.ThreadId
|
|||||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||||
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
|
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
|
||||||
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
|
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
|
|
||||||
@@ -129,6 +129,7 @@ fun aMessagesState(
|
|||||||
hasUnreadThreads = false,
|
hasUnreadThreads = false,
|
||||||
),
|
),
|
||||||
isCurrentlySharingLiveLocationInRoom: Boolean = false,
|
isCurrentlySharingLiveLocationInRoom: Boolean = false,
|
||||||
|
dmUserStatus: DisplayedStatus? = null,
|
||||||
eventSink: (MessagesEvent) -> Unit = {},
|
eventSink: (MessagesEvent) -> Unit = {},
|
||||||
) = MessagesState(
|
) = MessagesState(
|
||||||
roomId = RoomId("!id:domain"),
|
roomId = RoomId("!id:domain"),
|
||||||
@@ -159,6 +160,7 @@ fun aMessagesState(
|
|||||||
successorRoom = successorRoom,
|
successorRoom = successorRoom,
|
||||||
threads = threads,
|
threads = threads,
|
||||||
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom,
|
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom,
|
||||||
|
dmUserStatus = dmUserStatus,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -193,11 +195,9 @@ fun aReactionSummaryState(
|
|||||||
|
|
||||||
fun aCustomReactionState(
|
fun aCustomReactionState(
|
||||||
target: CustomReactionState.Target = CustomReactionState.Target.None,
|
target: CustomReactionState.Target = CustomReactionState.Target.None,
|
||||||
recentEmojis: ImmutableList<String> = persistentListOf(),
|
|
||||||
eventSink: (CustomReactionEvent) -> Unit = {},
|
eventSink: (CustomReactionEvent) -> Unit = {},
|
||||||
) = CustomReactionState(
|
) = CustomReactionState(
|
||||||
target = target,
|
target = target,
|
||||||
recentEmojis = recentEmojis,
|
|
||||||
selectedEmoji = persistentSetOf(),
|
selectedEmoji = persistentSetOf(),
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|||||||
+32
-15
@@ -20,14 +20,17 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -81,7 +84,6 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemDaySepara
|
|||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.components.CallMenuItem
|
import io.element.android.features.messages.impl.timeline.components.CallMenuItem
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
|
||||||
@@ -113,6 +115,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||||||
import io.element.android.libraries.designsystem.utils.HideKeyboardWhenDisposed
|
import io.element.android.libraries.designsystem.utils.HideKeyboardWhenDisposed
|
||||||
import io.element.android.libraries.designsystem.utils.KeepScreenOn
|
import io.element.android.libraries.designsystem.utils.KeepScreenOn
|
||||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
@@ -123,6 +126,8 @@ import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
|||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.LocalEventContentValidationState
|
||||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.wysiwyg.link.Link
|
import io.element.android.wysiwyg.link.Link
|
||||||
@@ -144,10 +149,13 @@ fun MessagesView(
|
|||||||
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||||
onViewAllPinnedMessagesClick: () -> Unit,
|
onViewAllPinnedMessagesClick: () -> Unit,
|
||||||
onThreadsListClick: () -> Unit,
|
onThreadsListClick: () -> Unit,
|
||||||
|
knockRequestsBannerView: @Composable () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
forceJumpToBottomVisibility: Boolean = false,
|
forceJumpToBottomVisibility: Boolean = false,
|
||||||
knockRequestsBannerView: @Composable () -> Unit,
|
customReactionBottomSheet: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val eventContentValidationState = LocalEventContentValidationState.current
|
||||||
|
|
||||||
OnLifecycleEvent { _, event ->
|
OnLifecycleEvent { _, event ->
|
||||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.LifecycleEvent(event))
|
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvent.LifecycleEvent(event))
|
||||||
}
|
}
|
||||||
@@ -170,6 +178,9 @@ fun MessagesView(
|
|||||||
|
|
||||||
fun onContentClick(event: TimelineItem.Event) {
|
fun onContentClick(event: TimelineItem.Event) {
|
||||||
Timber.v("onMessageClick= ${event.id}")
|
Timber.v("onMessageClick= ${event.id}")
|
||||||
|
val eventId = event.eventId ?: return
|
||||||
|
if (eventContentValidationState[eventId].getCurrentOverallState() != ContentValidationValue.Valid) return
|
||||||
|
|
||||||
val hideKeyboard = onEventContentClick(state.timelineState.isLive, event)
|
val hideKeyboard = onEventContentClick(state.timelineState.isLive, event)
|
||||||
if (hideKeyboard) {
|
if (hideKeyboard) {
|
||||||
localView.hideKeyboard()
|
localView.hideKeyboard()
|
||||||
@@ -219,7 +230,7 @@ fun MessagesView(
|
|||||||
},
|
},
|
||||||
content = {
|
content = {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
contentWindowInsets = WindowInsets.statusBars,
|
contentWindowInsets = scaffoldScrollableContentInsets,
|
||||||
topBar = {
|
topBar = {
|
||||||
if (state.timelineState.timelineMode is Timeline.Mode.Thread) {
|
if (state.timelineState.timelineMode is Timeline.Mode.Thread) {
|
||||||
ThreadTopBar(
|
ThreadTopBar(
|
||||||
@@ -237,6 +248,7 @@ fun MessagesView(
|
|||||||
heroes = state.heroes,
|
heroes = state.heroes,
|
||||||
dmUserIdentityState = state.dmUserVerificationState,
|
dmUserIdentityState = state.dmUserVerificationState,
|
||||||
sharedHistoryIcon = state.topBarSharedHistoryIcon,
|
sharedHistoryIcon = state.topBarSharedHistoryIcon,
|
||||||
|
dmUserStatus = state.dmUserStatus,
|
||||||
onBackClick = { hidingKeyboard { onBackClick() } },
|
onBackClick = { hidingKeyboard { onBackClick() } },
|
||||||
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
|
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
|
||||||
menuActions = {
|
menuActions = {
|
||||||
@@ -294,6 +306,7 @@ fun MessagesView(
|
|||||||
onSwipeToReply = { targetEvent ->
|
onSwipeToReply = { targetEvent ->
|
||||||
state.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Reply, targetEvent))
|
state.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Reply, targetEvent))
|
||||||
},
|
},
|
||||||
|
onJoinCallClick = onJoinCallClick,
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
|
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
|
||||||
knockRequestsBannerView = knockRequestsBannerView,
|
knockRequestsBannerView = knockRequestsBannerView,
|
||||||
@@ -402,12 +415,7 @@ fun MessagesView(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
CustomReactionBottomSheet(
|
customReactionBottomSheet()
|
||||||
state = state.customReactionState,
|
|
||||||
onSelectEmoji = { uniqueId, emoji ->
|
|
||||||
state.eventSink(MessagesEvent.ToggleReaction(emoji.unicode, uniqueId))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ReactionSummaryView(state = state.reactionSummaryState)
|
ReactionSummaryView(state = state.reactionSummaryState)
|
||||||
ReadReceiptBottomSheet(
|
ReadReceiptBottomSheet(
|
||||||
@@ -474,6 +482,7 @@ private fun MessagesViewContent(
|
|||||||
onSendLocationClick: () -> Unit,
|
onSendLocationClick: () -> Unit,
|
||||||
onCreatePollClick: () -> Unit,
|
onCreatePollClick: () -> Unit,
|
||||||
onViewAllPinnedMessagesClick: () -> Unit,
|
onViewAllPinnedMessagesClick: () -> Unit,
|
||||||
|
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||||
forceJumpToBottomVisibility: Boolean,
|
forceJumpToBottomVisibility: Boolean,
|
||||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -531,6 +540,7 @@ private fun MessagesViewContent(
|
|||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
|
onJoinCallClick = onJoinCallClick,
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
nestedScrollConnection = scrollBehavior.nestedScrollConnection,
|
nestedScrollConnection = scrollBehavior.nestedScrollConnection,
|
||||||
floatingDateTopOffset = topBannersHeightDp,
|
floatingDateTopOffset = topBannersHeightDp,
|
||||||
@@ -576,12 +586,17 @@ private fun MessagesViewComposerBottomSheetContents(
|
|||||||
onRoomSuccessorClick: (RoomId) -> Unit,
|
onRoomSuccessorClick: (RoomId) -> Unit,
|
||||||
onLinkClick: (String, Boolean) -> Unit,
|
onLinkClick: (String, Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val contentPadding = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal).asPaddingValues()
|
||||||
when {
|
when {
|
||||||
state.successorRoom != null -> {
|
state.successorRoom != null -> {
|
||||||
SuccessorRoomBanner(roomSuccessor = state.successorRoom, onRoomSuccessorClick = onRoomSuccessorClick)
|
SuccessorRoomBanner(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(contentPadding),
|
||||||
|
roomSuccessor = state.successorRoom,
|
||||||
|
onRoomSuccessorClick = onRoomSuccessorClick
|
||||||
|
)
|
||||||
}
|
}
|
||||||
state.userEventPermissions.canSendMessage -> {
|
state.userEventPermissions.canSendMessage -> {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth().padding(contentPadding)) {
|
||||||
// Do not show the identity change if user is composing a Rich message or is seeing suggestion(s).
|
// Do not show the identity change if user is composing a Rich message or is seeing suggestion(s).
|
||||||
if (state.composerState.suggestions.isEmpty() &&
|
if (state.composerState.suggestions.isEmpty() &&
|
||||||
state.composerState.textEditorState is TextEditorState.Markdown) {
|
state.composerState.textEditorState is TextEditorState.Markdown) {
|
||||||
@@ -605,15 +620,15 @@ private fun MessagesViewComposerBottomSheetContents(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
CantSendMessageBanner()
|
CantSendMessageBanner(Modifier.padding(contentPadding))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CantSendMessageBanner() {
|
private fun CantSendMessageBanner(modifier: Modifier = Modifier) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(ElementTheme.colors.bgSubtleSecondary)
|
.background(ElementTheme.colors.bgSubtleSecondary)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
@@ -662,6 +677,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
|
|||||||
onViewAllPinnedMessagesClick = { },
|
onViewAllPinnedMessagesClick = { },
|
||||||
forceJumpToBottomVisibility = true,
|
forceJumpToBottomVisibility = true,
|
||||||
knockRequestsBannerView = {},
|
knockRequestsBannerView = {},
|
||||||
|
customReactionBottomSheet = {},
|
||||||
onThreadsListClick = {},
|
onThreadsListClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -719,5 +735,6 @@ internal fun MessagesViewA11yPreview() = ElementPreview {
|
|||||||
onThreadsListClick = {},
|
onThreadsListClick = {},
|
||||||
forceJumpToBottomVisibility = true,
|
forceJumpToBottomVisibility = true,
|
||||||
knockRequestsBannerView = {},
|
knockRequestsBannerView = {},
|
||||||
|
customReactionBottomSheet = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -44,13 +44,13 @@ import io.element.android.libraries.architecture.Presenter
|
|||||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.recentemojis.api.GetRecentEmojis
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|||||||
+1
@@ -43,6 +43,7 @@ internal fun MessagesViewWithIdentityChangePreview(
|
|||||||
onJoinCallClick = {},
|
onJoinCallClick = {},
|
||||||
onViewAllPinnedMessagesClick = {},
|
onViewAllPinnedMessagesClick = {},
|
||||||
knockRequestsBannerView = {},
|
knockRequestsBannerView = {},
|
||||||
|
customReactionBottomSheet = {},
|
||||||
onThreadsListClick = {},
|
onThreadsListClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -31,6 +31,7 @@ import dev.zacsweers.metro.AssistedFactory
|
|||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import im.vector.app.features.analytics.plan.Interaction
|
import im.vector.app.features.analytics.plan.Interaction
|
||||||
|
import io.element.android.features.contentscanner.api.ContentScannerService
|
||||||
import io.element.android.features.location.api.LocationService
|
import io.element.android.features.location.api.LocationService
|
||||||
import io.element.android.features.messages.impl.MessagesNavigator
|
import io.element.android.features.messages.impl.MessagesNavigator
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
@@ -61,8 +62,12 @@ import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
|
|||||||
import io.element.android.libraries.matrix.api.room.getDirectRoomMember
|
import io.element.android.libraries.matrix.api.room.getDirectRoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.use
|
import io.element.android.libraries.matrix.api.room.powerlevels.use
|
||||||
import io.element.android.libraries.matrix.api.timeline.TimelineException
|
import io.element.android.libraries.matrix.api.timeline.TimelineException
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.mediaSources
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.EventContentValidationCache
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.reply.content
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.reply.eventId
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
|
||||||
@@ -135,6 +140,8 @@ class MessageComposerPresenter(
|
|||||||
private val notificationConversationService: NotificationConversationService,
|
private val notificationConversationService: NotificationConversationService,
|
||||||
private val slashCommandService: SlashCommandService,
|
private val slashCommandService: SlashCommandService,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val contentScannerService: ContentScannerService,
|
||||||
|
private val contentValidationCache: EventContentValidationCache,
|
||||||
) : Presenter<MessageComposerState> {
|
) : Presenter<MessageComposerState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
@@ -848,6 +855,11 @@ class MessageComposerPresenter(
|
|||||||
setText(newComposerMode.content, markdownTextEditorState, richTextEditorState, requestFocus = true)
|
setText(newComposerMode.content, markdownTextEditorState, richTextEditorState, requestFocus = true)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
if (currentComposerMode is MessageComposerMode.Reply) {
|
||||||
|
val mediaSources = currentComposerMode.replyToDetails.content()?.mediaSources() ?: return@launch
|
||||||
|
val contentValidationState = contentValidationCache[currentComposerMode.replyToDetails.eventId()]
|
||||||
|
contentScannerService.scan(mediaSources, contentValidationState)
|
||||||
|
}
|
||||||
// When coming from edit, just clear the composer as it'd be weird to reset a volatile draft in this scenario.
|
// When coming from edit, just clear the composer as it'd be weird to reset a volatile draft in this scenario.
|
||||||
if (currentComposerMode.isEditing) {
|
if (currentComposerMode.isEditing) {
|
||||||
setText("", markdownTextEditorState, richTextEditorState)
|
setText("", markdownTextEditorState, richTextEditorState)
|
||||||
|
|||||||
+13
-6
@@ -41,6 +41,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
|
import io.element.android.libraries.matrix.api.room.getBestName
|
||||||
|
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion
|
import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
@@ -112,12 +114,16 @@ private fun SuggestionItemView(
|
|||||||
is ResolvedSuggestion.Member -> AvatarType.User
|
is ResolvedSuggestion.Member -> AvatarType.User
|
||||||
is ResolvedSuggestion.Command -> null
|
is ResolvedSuggestion.Command -> null
|
||||||
}
|
}
|
||||||
val title = when (suggestion) {
|
val title: String? = when (suggestion) {
|
||||||
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
|
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
|
||||||
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName
|
is ResolvedSuggestion.Member -> suggestion.roomMember.getBestName()
|
||||||
is ResolvedSuggestion.Alias -> suggestion.roomName
|
is ResolvedSuggestion.Alias -> suggestion.roomName
|
||||||
is ResolvedSuggestion.Command -> suggestion.command.command
|
is ResolvedSuggestion.Command -> suggestion.command.command
|
||||||
}
|
}
|
||||||
|
val status = when (suggestion) {
|
||||||
|
is ResolvedSuggestion.Member -> suggestion.roomMember.displayedStatus
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
val details = when (suggestion) {
|
val details = when (suggestion) {
|
||||||
is ResolvedSuggestion.AtRoom,
|
is ResolvedSuggestion.AtRoom,
|
||||||
is ResolvedSuggestion.Member,
|
is ResolvedSuggestion.Member,
|
||||||
@@ -148,11 +154,11 @@ private fun SuggestionItemView(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
title?.let {
|
title?.let {
|
||||||
Text(
|
DisplayNameWithStatus(
|
||||||
text = it,
|
name = title,
|
||||||
|
status = status,
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
maxLines = 1,
|
nameColor = ElementTheme.colors.textPrimary,
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
details?.let {
|
details?.let {
|
||||||
@@ -191,6 +197,7 @@ internal fun SuggestionsPickerViewPreview() {
|
|||||||
role = RoomMember.Role.User,
|
role = RoomMember.Role.User,
|
||||||
membershipChangeReason = null,
|
membershipChangeReason = null,
|
||||||
isServiceMember = false,
|
isServiceMember = false,
|
||||||
|
displayedStatus = null,
|
||||||
)
|
)
|
||||||
val anAlias = remember { RoomAlias("#room:domain.org") }
|
val anAlias = remember { RoomAlias("#room:domain.org") }
|
||||||
SuggestionsPickerView(
|
SuggestionsPickerView(
|
||||||
|
|||||||
+7
-1
@@ -16,10 +16,15 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -98,7 +103,8 @@ private fun PinnedMessagesBannerRow(
|
|||||||
onClick(state.currentPinnedMessage.eventId)
|
onClick(state.currentPinnedMessage.eventId)
|
||||||
state.eventSink(PinnedMessagesBannerEvent.MoveToNextPinned)
|
state.eventSink(PinnedMessagesBannerEvent.MoveToNextPinned)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
.padding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal).asPaddingValues()),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.width(26.dp))
|
Spacer(modifier = Modifier.width(26.dp))
|
||||||
|
|||||||
+10
-5
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.plus
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
@@ -36,7 +37,6 @@ import io.element.android.features.messages.impl.timeline.components.event.Timel
|
|||||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
|
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||||
import io.element.android.features.poll.api.pollcontent.PollTitleView
|
import io.element.android.features.poll.api.pollcontent.PollTitleView
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
@@ -48,6 +48,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -91,7 +93,8 @@ fun PinnedMessagesListView(
|
|||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding),
|
.consumeWindowInsets(padding),
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
contentWindowInsets = scaffoldScrollableContentInsets,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,11 +210,12 @@ private fun PinnedMessagesListLoaded(
|
|||||||
onEmojiReactionClick = { _, _ -> },
|
onEmojiReactionClick = { _, _ -> },
|
||||||
onVerifiedUserSendFailureClick = {}
|
onVerifiedUserSendFailureClick = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
state = rememberLazyListState(),
|
state = rememberLazyListState(),
|
||||||
reverseLayout = true,
|
reverseLayout = true,
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
contentPadding = lazyColumnContentPadding + PaddingValues(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = state.timelineItems,
|
items = state.timelineItems,
|
||||||
@@ -240,6 +244,7 @@ private fun PinnedMessagesListLoaded(
|
|||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
onReadReceiptClick = {},
|
onReadReceiptClick = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
|
onJoinCallClick = {},
|
||||||
eventSink = { timelineItemEvent ->
|
eventSink = { timelineItemEvent ->
|
||||||
when (timelineItemEvent) {
|
when (timelineItemEvent) {
|
||||||
is TimelineEvent.OpenThread -> state.eventSink(PinnedMessagesListEvent.OpenThread(timelineItemEvent.threadRootEventId))
|
is TimelineEvent.OpenThread -> state.eventSink(PinnedMessagesListEvent.OpenThread(timelineItemEvent.threadRootEventId))
|
||||||
@@ -290,9 +295,9 @@ private fun TimelineItemEventContentViewWrapper(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
|
eventId = event.eventId,
|
||||||
content = event.content,
|
content = event.content,
|
||||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine),
|
timelineProtectionState = timelineProtectionState,
|
||||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
|
||||||
onGalleryItemClick = onGalleryItemClick,
|
onGalleryItemClick = onGalleryItemClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onLinkLongClick = onLinkLongClick,
|
onLinkLongClick = onLinkLongClick,
|
||||||
|
|||||||
+1
@@ -322,6 +322,7 @@ class ThreadedMessagesNode(
|
|||||||
onViewAllPinnedMessagesClick = {},
|
onViewAllPinnedMessagesClick = {},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
knockRequestsBannerView = {},
|
knockRequestsBannerView = {},
|
||||||
|
customReactionBottomSheet = {},
|
||||||
onThreadsListClick = {},
|
onThreadsListClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.plus
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -54,6 +55,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||||
|
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
@@ -129,14 +132,15 @@ fun ThreadsListView(
|
|||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
BackButton(onBackClick)
|
BackButton(onBackClick)
|
||||||
}
|
},
|
||||||
|
windowInsets = scaffoldScrollableContentInsets,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = padding,
|
contentPadding = padding + lazyColumnContentPadding,
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
) {
|
) {
|
||||||
itemsIndexed(state.threads, key = { _, row -> row.item.threadId }) { index, row ->
|
itemsIndexed(state.threads, key = { _, row -> row.item.threadId }) { index, row ->
|
||||||
@@ -383,7 +387,12 @@ fun aThreadListItem(
|
|||||||
fun aThreadListItemEvent(
|
fun aThreadListItemEvent(
|
||||||
threadId: ThreadId = ThreadId("\$a-thread-id"),
|
threadId: ThreadId = ThreadId("\$a-thread-id"),
|
||||||
senderId: UserId = UserId("@a-user-id:server"),
|
senderId: UserId = UserId("@a-user-id:server"),
|
||||||
senderProfile: ProfileDetails = ProfileDetails.Ready(displayName = USER_NAME_ALICE, displayNameAmbiguous = false, avatarUrl = null),
|
senderProfile: ProfileDetails = ProfileDetails.Ready(
|
||||||
|
displayName = USER_NAME_ALICE,
|
||||||
|
displayNameAmbiguous = false,
|
||||||
|
avatarUrl = null,
|
||||||
|
displayedStatus = null,
|
||||||
|
),
|
||||||
isOwn: Boolean = false,
|
isOwn: Boolean = false,
|
||||||
content: EventContent = MessageContent(
|
content: EventContent = MessageContent(
|
||||||
body = "Hello world!",
|
body = "Hello world!",
|
||||||
|
|||||||
+8
@@ -13,7 +13,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
sealed interface TimelineEvent {
|
sealed interface TimelineEvent {
|
||||||
@@ -61,4 +63,10 @@ sealed interface TimelineEvent {
|
|||||||
) : TimelineItemPollEvent
|
) : TimelineItemPollEvent
|
||||||
|
|
||||||
data object StopLiveLocationShare : TimelineItemEvent
|
data object StopLiveLocationShare : TimelineItemEvent
|
||||||
|
|
||||||
|
data class ValidateMedia(
|
||||||
|
val eventId: EventId,
|
||||||
|
val mediaSources: List<MediaSource>,
|
||||||
|
val validationState: ContentValidationState
|
||||||
|
) : TimelineItemEvent
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -35,6 +35,8 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState
|
|||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel
|
||||||
|
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
|
||||||
|
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
import io.element.android.features.messages.impl.userEventPermissions
|
import io.element.android.features.messages.impl.userEventPermissions
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
@@ -64,6 +66,7 @@ import io.element.android.services.analytics.api.finishLongRunningTransaction
|
|||||||
import io.element.android.services.analyticsproviders.api.AnalyticsUserData
|
import io.element.android.services.analyticsproviders.api.AnalyticsUserData
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -98,6 +101,7 @@ class TimelinePresenter(
|
|||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||||
private val markAsFullyRead: MarkAsFullyRead,
|
private val markAsFullyRead: MarkAsFullyRead,
|
||||||
|
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
private val tag = "TimelinePresenter"
|
private val tag = "TimelinePresenter"
|
||||||
|
|
||||||
@@ -157,6 +161,8 @@ class TimelinePresenter(
|
|||||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.JumpToUnread)
|
value = featureFlagService.isFeatureEnabled(FeatureFlags.JumpToUnread)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val timelineProtectionState = timelineProtectionPresenter.present()
|
||||||
|
|
||||||
fun handleEvent(event: TimelineEvent) {
|
fun handleEvent(event: TimelineEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is TimelineEvent.LoadMore -> {
|
is TimelineEvent.LoadMore -> {
|
||||||
@@ -252,6 +258,9 @@ class TimelinePresenter(
|
|||||||
focusedEventId = event.focusedEvent,
|
focusedEventId = event.focusedEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is TimelineEvent.ValidateMedia -> {
|
||||||
|
timelineProtectionState.eventSink(TimelineProtectionEvent.ValidateContent(event.mediaSources, event.validationState))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +268,7 @@ class TimelinePresenter(
|
|||||||
timelineItemsFactory.timelineItems
|
timelineItemsFactory.timelineItems
|
||||||
.onEach { newTimelineItems ->
|
.onEach { newTimelineItems ->
|
||||||
timelineItemIndexer.process(newTimelineItems)
|
timelineItemIndexer.process(newTimelineItems)
|
||||||
timelineItems = newTimelineItems
|
timelineItems = newTimelineItems.toImmutableList()
|
||||||
|
|
||||||
analyticsService.run {
|
analyticsService.run {
|
||||||
finishLongRunningTransaction(DisplayFirstTimelineItems)
|
finishLongRunningTransaction(DisplayFirstTimelineItems)
|
||||||
|
|||||||
+11
-1
@@ -27,9 +27,15 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.plus
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -130,6 +136,7 @@ fun TimelineView(
|
|||||||
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (emoji: String, TimelineItem.Event) -> Unit,
|
||||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||||
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
lazyListState: LazyListState = rememberLazyListState(),
|
lazyListState: LazyListState = rememberLazyListState(),
|
||||||
forceJumpToBottomVisibility: Boolean = false,
|
forceJumpToBottomVisibility: Boolean = false,
|
||||||
@@ -192,7 +199,7 @@ fun TimelineView(
|
|||||||
.testTag(TestTags.timeline),
|
.testTag(TestTags.timeline),
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
reverseLayout = true,
|
reverseLayout = true,
|
||||||
contentPadding = PaddingValues(top = 64.dp, bottom = 8.dp),
|
contentPadding = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal).asPaddingValues() + PaddingValues(top = 64.dp, bottom = 8.dp),
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = state.timelineItems,
|
items = state.timelineItems,
|
||||||
@@ -219,6 +226,7 @@ fun TimelineView(
|
|||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onSwipeToReply = onSwipeToReply,
|
onSwipeToReply = onSwipeToReply,
|
||||||
|
onJoinCallClick = onJoinCallClick,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -599,6 +607,7 @@ internal fun TimelineViewPreview(
|
|||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
onReactionClick = { _, _ -> },
|
onReactionClick = { _, _ -> },
|
||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
|
onJoinCallClick = {},
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
onReadReceiptClick = {},
|
onReadReceiptClick = {},
|
||||||
onGalleryItemClick = { _, _ -> },
|
onGalleryItemClick = { _, _ -> },
|
||||||
@@ -649,6 +658,7 @@ private fun TimelineViewWithReadMarker(
|
|||||||
onReadReceiptClick = {},
|
onReadReceiptClick = {},
|
||||||
forceJumpToBottomVisibility = true,
|
forceJumpToBottomVisibility = true,
|
||||||
forceJumpToReadMarkerVisibility = true,
|
forceJumpToReadMarkerVisibility = true,
|
||||||
|
onJoinCallClick = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -49,6 +49,7 @@ internal fun TimelineViewMessageShieldPreview() = ElementPreview {
|
|||||||
onReactionLongClick = { _, _ -> },
|
onReactionLongClick = { _, _ -> },
|
||||||
onMoreReactionsClick = {},
|
onMoreReactionsClick = {},
|
||||||
onReadReceiptClick = {},
|
onReadReceiptClick = {},
|
||||||
|
onJoinCallClick = {},
|
||||||
onGalleryItemClick = { _, _ -> },
|
onGalleryItemClick = { _, _ -> },
|
||||||
forceJumpToBottomVisibility = true,
|
forceJumpToBottomVisibility = true,
|
||||||
)
|
)
|
||||||
|
|||||||
+378
@@ -0,0 +1,378 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent
|
||||||
|
import io.element.android.libraries.dateformatter.api.toHumanReadableDuration
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarRow
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||||
|
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
||||||
|
import io.element.android.libraries.designsystem.preview.USER_NAME_BOB
|
||||||
|
import io.element.android.libraries.designsystem.preview.USER_NAME_CAROL
|
||||||
|
import io.element.android.libraries.designsystem.preview.USER_NAME_CHARLIE
|
||||||
|
import io.element.android.libraries.designsystem.preview.USER_NAME_DAVID
|
||||||
|
import io.element.android.libraries.designsystem.preview.USER_NAME_JOHN_DOE
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||||
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ActiveCallTimelineItemView(
|
||||||
|
timelineRoomInfo: TimelineRoomInfo,
|
||||||
|
event: TimelineItem.Event,
|
||||||
|
state: RtcNotificationState.Active,
|
||||||
|
isLastOutgoingMessage: Boolean,
|
||||||
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptsClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onJoinCallClick: (isAudioCall: Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
|
||||||
|
.border(1.dp, ElementTheme.colors.borderInteractivePrimary, RoundedCornerShape(8.dp))
|
||||||
|
.combinedClickable(
|
||||||
|
enabled = true,
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = { onLongClick(event) },
|
||||||
|
onLongClickLabel = stringResource(CommonStrings.action_open_context_menu),
|
||||||
|
)
|
||||||
|
.onKeyboardContextMenuAction { onLongClick(event) }
|
||||||
|
.padding(start = 12.dp, end = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 12.dp, bottom = 12.dp)
|
||||||
|
.size(40.dp)
|
||||||
|
.background(ElementTheme.colors.bgSubtleSecondary, RoundedCornerShape(8.dp))
|
||||||
|
.padding(10.dp),
|
||||||
|
imageVector = if (state.callIntent == CallIntent.AUDIO) CompoundIcons.VoiceCallSolid() else CompoundIcons.VideoCallSolid(),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = ElementTheme.colors.iconPrimary,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (timelineRoomInfo.isDm) {
|
||||||
|
DirectMessageCallBody(state, Modifier.weight(1f))
|
||||||
|
} else {
|
||||||
|
GroupCallBody(
|
||||||
|
event,
|
||||||
|
state,
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(top = 12.dp, bottom = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.padding(top = 12.dp, bottom = 12.dp)
|
||||||
|
) {
|
||||||
|
if (!state.isJoined) {
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_join),
|
||||||
|
size = ButtonSize.Small,
|
||||||
|
onClick = { onJoinCallClick(state.callIntent == CallIntent.AUDIO) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (state.callStartTsMillis != null) {
|
||||||
|
CallDurationText(
|
||||||
|
state.callStartTsMillis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TimelineItemReadReceiptView(
|
||||||
|
state = ReadReceiptViewState(
|
||||||
|
sendState = event.localSendState,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
|
receipts = event.readReceiptState.receipts,
|
||||||
|
),
|
||||||
|
onReadReceiptsClick = { onReadReceiptsClick(event) },
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DirectMessageCallBody(state: RtcNotificationState.Active, modifier: Modifier = Modifier) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
if (state.joinedMembers.size == 1) {
|
||||||
|
val caller = state.joinedMembers.first()
|
||||||
|
Avatar(
|
||||||
|
avatarData = AvatarData(
|
||||||
|
id = caller.userId.value,
|
||||||
|
name = caller.displayName,
|
||||||
|
url = caller.avatarUrl,
|
||||||
|
size = AvatarSize.ActiveCallItem,
|
||||||
|
),
|
||||||
|
avatarType = AvatarType.User
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 6.dp),
|
||||||
|
text = stringResource(CommonStrings.common_user_started_a_call, caller.displayName ?: caller.userId),
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AvatarRow(
|
||||||
|
avatarDataList = state.joinedMembers
|
||||||
|
.map { user ->
|
||||||
|
AvatarData(
|
||||||
|
id = user.userId.value,
|
||||||
|
name = user.displayName,
|
||||||
|
url = user.avatarUrl,
|
||||||
|
size = AvatarSize.ActiveCallItem
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toImmutableList(),
|
||||||
|
avatarType = AvatarType.User,
|
||||||
|
overlapRatio = 0.5f,
|
||||||
|
lastOnTop = true,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 6.dp),
|
||||||
|
text = stringResource(CommonStrings.common_call_in_progress),
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GroupCallBody(event: TimelineItem.Event, state: RtcNotificationState.Active, modifier: Modifier = Modifier) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(CommonStrings.common_group_call_in_progress),
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (state.joinedMembers.size == 1) {
|
||||||
|
val caller = state.joinedMembers.first()
|
||||||
|
Avatar(
|
||||||
|
avatarData = AvatarData(
|
||||||
|
id = caller.userId.value,
|
||||||
|
name = caller.displayName,
|
||||||
|
url = caller.avatarUrl,
|
||||||
|
size = AvatarSize.ActiveCallItem,
|
||||||
|
),
|
||||||
|
avatarType = AvatarType.User
|
||||||
|
)
|
||||||
|
// Only show started a call if the last remaining user in the call
|
||||||
|
// is really the one who started the call
|
||||||
|
if (caller.userId == event.senderId) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 6.dp),
|
||||||
|
text = stringResource(CommonStrings.common_user_started_a_call, caller.displayName ?: caller.userId),
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val displayMembers = state.joinedMembers.take(3)
|
||||||
|
AvatarRow(
|
||||||
|
avatarDataList = displayMembers
|
||||||
|
.map { user ->
|
||||||
|
AvatarData(
|
||||||
|
id = user.userId.value,
|
||||||
|
name = user.displayName,
|
||||||
|
url = user.avatarUrl,
|
||||||
|
size = AvatarSize.ActiveCallItem
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toImmutableList(),
|
||||||
|
avatarType = AvatarType.User,
|
||||||
|
overlapRatio = 0.5f,
|
||||||
|
lastOnTop = true,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = pluralStringResource(
|
||||||
|
CommonPlurals.screen_timeline_active_call_joined_count,
|
||||||
|
count = state.joinedMembers.size,
|
||||||
|
state.joinedMembers.size
|
||||||
|
),
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CallDurationText(
|
||||||
|
startTsMillis: Long,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
fun formatElapsed(startTsMillis: Long): String =
|
||||||
|
(System.currentTimeMillis() - startTsMillis).coerceAtLeast(0).toHumanReadableDuration()
|
||||||
|
|
||||||
|
val duration by produceState(
|
||||||
|
initialValue = formatElapsed(startTsMillis),
|
||||||
|
startTsMillis,
|
||||||
|
) {
|
||||||
|
while (true) {
|
||||||
|
delay(1.seconds)
|
||||||
|
value = formatElapsed(startTsMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
modifier = modifier,
|
||||||
|
text = "($duration)",
|
||||||
|
style = ElementTheme.typography.fontBodyXsRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@PreviewWithLargeHeight
|
||||||
|
@Composable
|
||||||
|
internal fun ActiveCallTimelineItemViewPreview() = ElementPreview(
|
||||||
|
drawableFallbackForImages = CommonDrawables.sample_avatar
|
||||||
|
) {
|
||||||
|
val readReceiptState = mutableListOf(
|
||||||
|
aTimelineItemReadReceipts(
|
||||||
|
receipts = List(3) { aReadReceiptData(it) },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.padding(bottom = 16.dp)) {
|
||||||
|
listOf(CallIntent.AUDIO, CallIntent.VIDEO).forEach { callIntent ->
|
||||||
|
listOf(
|
||||||
|
RtcNotificationState.Active(
|
||||||
|
listOf(aMatrixUser(displayName = USER_NAME_ALICE, avatarUrl = "anUrl")),
|
||||||
|
isJoined = true,
|
||||||
|
callStartTsMillis = System.currentTimeMillis() - 60_000,
|
||||||
|
callIntent = callIntent
|
||||||
|
) to true,
|
||||||
|
RtcNotificationState.Active(
|
||||||
|
listOf(
|
||||||
|
aMatrixUser(displayName = USER_NAME_ALICE, avatarUrl = "anUrl"),
|
||||||
|
aMatrixUser(displayName = USER_NAME_BOB)
|
||||||
|
),
|
||||||
|
isJoined = true,
|
||||||
|
callStartTsMillis = System.currentTimeMillis() - 60_000,
|
||||||
|
callIntent = callIntent
|
||||||
|
) to true,
|
||||||
|
RtcNotificationState.Active(
|
||||||
|
listOf(
|
||||||
|
aMatrixUser(displayName = USER_NAME_ALICE),
|
||||||
|
aMatrixUser(displayName = USER_NAME_BOB, avatarUrl = "anUrlB"),
|
||||||
|
aMatrixUser(displayName = USER_NAME_CAROL),
|
||||||
|
aMatrixUser(displayName = USER_NAME_DAVID),
|
||||||
|
aMatrixUser(displayName = USER_NAME_CHARLIE),
|
||||||
|
aMatrixUser(displayName = USER_NAME_JOHN_DOE, avatarUrl = "anUrlC"),
|
||||||
|
),
|
||||||
|
isJoined = false,
|
||||||
|
callStartTsMillis = System.currentTimeMillis(),
|
||||||
|
callIntent = callIntent
|
||||||
|
) to false,
|
||||||
|
// Group call only one member still in call, but it is not the creator
|
||||||
|
RtcNotificationState.Active(
|
||||||
|
listOf(
|
||||||
|
aMatrixUser(displayName = USER_NAME_ALICE, avatarUrl = "aliceURl"),
|
||||||
|
),
|
||||||
|
isJoined = true,
|
||||||
|
callStartTsMillis = System.currentTimeMillis(),
|
||||||
|
callIntent = callIntent
|
||||||
|
) to false,
|
||||||
|
// Group call only one member that is the call creator
|
||||||
|
RtcNotificationState.Active(
|
||||||
|
listOf(
|
||||||
|
aMatrixUser(
|
||||||
|
id = "@senderId:domain",
|
||||||
|
displayName = "Sender"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isJoined = true,
|
||||||
|
callStartTsMillis = System.currentTimeMillis(),
|
||||||
|
callIntent = callIntent
|
||||||
|
) to false,
|
||||||
|
).forEach { (state, isDm) ->
|
||||||
|
val content = TimelineItemRtcNotificationContent(callIntent, state)
|
||||||
|
ActiveCallTimelineItemView(
|
||||||
|
timelineRoomInfo = aTimelineRoomInfo(isDm = isDm),
|
||||||
|
event = aTimelineItemEvent(
|
||||||
|
content = content,
|
||||||
|
// Only display read receipts for the first item
|
||||||
|
readReceiptState = readReceiptState.removeFirstOrNull() ?: aTimelineItemReadReceipts(),
|
||||||
|
),
|
||||||
|
state,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
|
onLongClick = {},
|
||||||
|
onReadReceiptsClick = {},
|
||||||
|
onJoinCallClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -11,5 +11,6 @@ package io.element.android.features.messages.impl.timeline.components
|
|||||||
enum class ContentPadding {
|
enum class ContentPadding {
|
||||||
Textual,
|
Textual,
|
||||||
Media,
|
Media,
|
||||||
CaptionedMedia
|
CaptionedMedia,
|
||||||
|
InvalidContent,
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -19,7 +19,9 @@ import androidx.compose.foundation.layout.widthIn
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ripple
|
import androidx.compose.material3.ripple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawWithCache
|
import androidx.compose.ui.draw.drawWithCache
|
||||||
@@ -27,6 +29,8 @@ import androidx.compose.ui.geometry.Offset
|
|||||||
import androidx.compose.ui.graphics.BlendMode
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.drawOutline
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
@@ -62,6 +66,8 @@ fun MessageEventBubble(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
customBackgroundColor: Color? = null,
|
||||||
|
borderColor: Color? = null,
|
||||||
content: @Composable BoxScope.() -> Unit = {},
|
content: @Composable BoxScope.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val clickableModifier = if (isTalkbackActive()) {
|
val clickableModifier = if (isTalkbackActive()) {
|
||||||
@@ -79,10 +85,12 @@ fun MessageEventBubble(
|
|||||||
|
|
||||||
val cutTopStart = state.cutTopStart
|
val cutTopStart = state.cutTopStart
|
||||||
// Ignore state.isHighlighted for now, we need a design decision on it.
|
// Ignore state.isHighlighted for now, we need a design decision on it.
|
||||||
val backgroundBubbleColor = MessageEventBubbleDefaults.backgroundBubbleColor(state.isMine)
|
val backgroundBubbleColor by rememberUpdatedState(customBackgroundColor ?: MessageEventBubbleDefaults.backgroundBubbleColor(state.isMine))
|
||||||
val bubbleShape = remember(state) { MessageEventBubbleDefaults.shape(state.cutTopStart, state.groupPosition, state.isMine) }
|
val bubbleShape = remember(state) { MessageEventBubbleDefaults.shape(state.cutTopStart, state.groupPosition, state.isMine) }
|
||||||
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
||||||
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
||||||
|
|
||||||
|
val updatedBorderColor by rememberUpdatedState(borderColor)
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.drawWithCache {
|
.drawWithCache {
|
||||||
@@ -103,6 +111,9 @@ fun MessageEventBubble(
|
|||||||
// Then draw the content on top of it
|
// Then draw the content on top of it
|
||||||
drawContent()
|
drawContent()
|
||||||
|
|
||||||
|
// Draw border color, if any
|
||||||
|
updatedBorderColor?.let { drawOutline(outline, it, style = Stroke(width = 1.dp.toPx())) }
|
||||||
|
|
||||||
// And then clip the top start corner if needed to make room for the avatar
|
// And then clip the top start corner if needed to make room for the avatar
|
||||||
if (cutTopStart) {
|
if (cutTopStart) {
|
||||||
drawCircle(
|
drawCircle(
|
||||||
|
|||||||
+1
@@ -166,6 +166,7 @@ internal fun MessageShieldViewPreview() {
|
|||||||
displayName = USER_NAME_ALICE,
|
displayName = USER_NAME_ALICE,
|
||||||
displayNameAmbiguous = false,
|
displayNameAmbiguous = false,
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
|
displayedStatus = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+7
-5
@@ -52,6 +52,7 @@ internal fun TimelineItemCallNotifyView(
|
|||||||
timelineRoomInfo: TimelineRoomInfo,
|
timelineRoomInfo: TimelineRoomInfo,
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
content: TimelineItemRtcNotificationContent,
|
content: TimelineItemRtcNotificationContent,
|
||||||
|
state: RtcNotificationState.Tombstoned,
|
||||||
isLastOutgoingMessage: Boolean,
|
isLastOutgoingMessage: Boolean,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
onReadReceiptsClick: (TimelineItem.Event) -> Unit,
|
onReadReceiptsClick: (TimelineItem.Event) -> Unit,
|
||||||
@@ -83,7 +84,7 @@ internal fun TimelineItemCallNotifyView(
|
|||||||
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
text = stringResource(getTextRes(timelineRoomInfo, content)),
|
text = stringResource(getTextRes(timelineRoomInfo, state)),
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
color = ElementTheme.colors.textSecondary,
|
color = ElementTheme.colors.textSecondary,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -114,13 +115,13 @@ internal fun TimelineItemCallNotifyView(
|
|||||||
@StringRes
|
@StringRes
|
||||||
private fun getTextRes(
|
private fun getTextRes(
|
||||||
timelineRoomInfo: TimelineRoomInfo,
|
timelineRoomInfo: TimelineRoomInfo,
|
||||||
content: TimelineItemRtcNotificationContent
|
state: RtcNotificationState.Tombstoned
|
||||||
): Int = if (timelineRoomInfo.isDm) {
|
): Int = if (timelineRoomInfo.isDm) {
|
||||||
when (content.state) {
|
when (state) {
|
||||||
is RtcNotificationState.Declined -> {
|
is RtcNotificationState.Declined -> {
|
||||||
if (content.state.byMe) CommonStrings.common_call_you_declined else CommonStrings.common_call_declined
|
if (state.byMe) CommonStrings.common_call_you_declined else CommonStrings.common_call_declined
|
||||||
}
|
}
|
||||||
RtcNotificationState.Started -> CommonStrings.common_call_started
|
else -> CommonStrings.common_call_started
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// In Rooms, do not show declined info.
|
// In Rooms, do not show declined info.
|
||||||
@@ -166,6 +167,7 @@ internal fun TimelineItemCallNotifyViewPreview() = ElementPreview {
|
|||||||
readReceiptState = readReceiptState.removeFirstOrNull() ?: aTimelineItemReadReceipts(),
|
readReceiptState = readReceiptState.removeFirstOrNull() ?: aTimelineItemReadReceipts(),
|
||||||
),
|
),
|
||||||
content = content,
|
content = content,
|
||||||
|
state,
|
||||||
isLastOutgoingMessage = false,
|
isLastOutgoingMessage = false,
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
onReadReceiptsClick = {},
|
onReadReceiptsClick = {},
|
||||||
|
|||||||
+346
@@ -0,0 +1,346 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.event.ElementTimelineItemPreview
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.event.aGalleryItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.event.aTimelineItemGalleryContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
|
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAttachmentsContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAudioContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStickerContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.anAttachmentItem
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.InMemoryEventContentValidationCache
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.LocalEventContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||||
|
|
||||||
|
private val AN_EVENT_ID = EventId($$"$eventId")
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemImageViewScanningContentPreview() = ElementPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(initial = mapOf(AN_EVENT_ID to NoopContentValidationState(ContentValidationValue.Loading)))
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalEventContentValidationState provides cache) {
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(eventId = AN_EVENT_ID, content = aTimelineItemImageContent()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemStickerViewScanningContentPreview() = ElementPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(initial = mapOf(AN_EVENT_ID to NoopContentValidationState(ContentValidationValue.Loading)))
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalEventContentValidationState provides cache) {
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(eventId = AN_EVENT_ID, content = aTimelineItemStickerContent(aspectRatio = 1.5f)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemVideoViewScanningContentPreview() = ElementPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(initial = mapOf(AN_EVENT_ID to NoopContentValidationState(ContentValidationValue.Loading)))
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalEventContentValidationState provides cache) {
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(eventId = AN_EVENT_ID, content = aTimelineItemVideoContent()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemFileViewScanningContentPreview() {
|
||||||
|
ElementTimelineItemPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(initial = mapOf(AN_EVENT_ID to NoopContentValidationState(ContentValidationValue.Loading)))
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalEventContentValidationState provides cache
|
||||||
|
) {
|
||||||
|
ATimelineItemEventRow(event = aTimelineItemEvent(eventId = AN_EVENT_ID, content = aTimelineItemFileContent()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemAudioViewScanningContentPreview() {
|
||||||
|
ElementTimelineItemPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(initial = mapOf(AN_EVENT_ID to NoopContentValidationState(ContentValidationValue.Loading)))
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalEventContentValidationState provides cache
|
||||||
|
) {
|
||||||
|
ATimelineItemEventRow(event = aTimelineItemEvent(eventId = AN_EVENT_ID, content = aTimelineItemAudioContent()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemVoiceViewScanningContentPreview() = ElementPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(initial = mapOf(AN_EVENT_ID to NoopContentValidationState(ContentValidationValue.Loading)))
|
||||||
|
}
|
||||||
|
ElementTimelineItemPreview {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalEventContentValidationState provides cache,
|
||||||
|
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
|
||||||
|
) {
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(eventId = AN_EVENT_ID, content = aTimelineItemVoiceContent()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemScanningContentFailedPreview() = ElementPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(initial = mapOf(AN_EVENT_ID to NoopContentValidationState(ContentValidationValue.Invalid)))
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalEventContentValidationState provides cache) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
ATimelineItemEventRow(event = aTimelineItemEvent(eventId = AN_EVENT_ID, content = aTimelineItemImageContent()))
|
||||||
|
ATimelineItemEventRow(event = aTimelineItemEvent(eventId = AN_EVENT_ID, content = aTimelineItemImageContent(caption = "A caption")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemScanningContentNotFoundPreview() = ElementPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(initial = mapOf(
|
||||||
|
AN_EVENT_ID to NoopContentValidationState(ContentValidationValue.UnrecoverableError(IllegalStateException("Not found")))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalEventContentValidationState provides cache) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
ATimelineItemEventRow(event = aTimelineItemEvent(eventId = AN_EVENT_ID, content = aTimelineItemImageContent()))
|
||||||
|
ATimelineItemEventRow(event = aTimelineItemEvent(eventId = AN_EVENT_ID, content = aTimelineItemImageContent(caption = "A caption")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemScanningContentWithInvalidRepliesPreview() = ElementPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(initial = mapOf(AN_EVENT_ID to NoopContentValidationState(ContentValidationValue.Invalid)))
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalEventContentValidationState provides cache) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(
|
||||||
|
content = aTimelineItemTextContent(),
|
||||||
|
inReplyTo = inReplyToInvalidContent(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = aTimelineItemImageContent(),
|
||||||
|
inReplyTo = inReplyToInvalidContent(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = aTimelineItemImageContent(),
|
||||||
|
inReplyTo = inReplyToTextContent(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemScanningContentWithRepliesFailedPreview() = ElementPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(
|
||||||
|
initial = mapOf(AN_EVENT_ID to NoopContentValidationState(ContentValidationValue.UnrecoverableError(IllegalStateException("BOOM"))))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalEventContentValidationState provides cache) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(
|
||||||
|
content = aTimelineItemTextContent(),
|
||||||
|
inReplyTo = inReplyToInvalidContent(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = aTimelineItemImageContent(),
|
||||||
|
inReplyTo = inReplyToInvalidContent(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = aTimelineItemImageContent(),
|
||||||
|
inReplyTo = inReplyToTextContent(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemGalleryViewScanningContentFailedPreview() = ElementPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(
|
||||||
|
initial =
|
||||||
|
mapOf(
|
||||||
|
AN_EVENT_ID to DefaultContentValidationState(mapOf(
|
||||||
|
"invalid" to ContentValidationValue.Invalid,
|
||||||
|
"error" to ContentValidationValue.UnrecoverableError(IllegalStateException("BOOM")),
|
||||||
|
"" to ContentValidationValue.Valid
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalEventContentValidationState provides cache) {
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = aTimelineItemGalleryContent(
|
||||||
|
items = listOf(
|
||||||
|
aGalleryItem(),
|
||||||
|
aGalleryItem(mediaSource = MediaSource("invalid", "{}")),
|
||||||
|
aGalleryItem(),
|
||||||
|
aGalleryItem(mediaSource = MediaSource("invalid", "{}")),
|
||||||
|
aGalleryItem(mediaSource = MediaSource("error", "{}")),
|
||||||
|
aGalleryItem(mediaSource = MediaSource("invalid", "{}")),
|
||||||
|
aGalleryItem(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemAttachmentsViewScanningContentFailedPreview() = ElementPreview {
|
||||||
|
val cache = remember {
|
||||||
|
InMemoryEventContentValidationCache(
|
||||||
|
initial =
|
||||||
|
mapOf(
|
||||||
|
AN_EVENT_ID to DefaultContentValidationState(mapOf(
|
||||||
|
"invalid" to ContentValidationValue.Invalid,
|
||||||
|
"error" to ContentValidationValue.UnrecoverableError(IllegalStateException("BOOM")),
|
||||||
|
"" to ContentValidationValue.Valid
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalEventContentValidationState provides cache) {
|
||||||
|
ATimelineItemEventRow(
|
||||||
|
event = aTimelineItemEvent(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
content = aTimelineItemAttachmentsContent(
|
||||||
|
attachments = listOf(
|
||||||
|
anAttachmentItem(),
|
||||||
|
anAttachmentItem(mediaSource = MediaSource("invalid", "{}")),
|
||||||
|
anAttachmentItem(),
|
||||||
|
anAttachmentItem(),
|
||||||
|
anAttachmentItem(mediaSource = MediaSource("error", "{}")),
|
||||||
|
anAttachmentItem(mediaSource = MediaSource("invalid", "{}")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inReplyToInvalidContent(): InReplyToDetails.Ready = InReplyToDetails.Ready(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
senderId = UserId("@sender:matrix.org"),
|
||||||
|
eventContent = MessageContent(
|
||||||
|
body = "A body",
|
||||||
|
inReplyTo = null,
|
||||||
|
isEdited = false,
|
||||||
|
threadInfo = null,
|
||||||
|
type = ImageMessageType(
|
||||||
|
filename = "A file",
|
||||||
|
caption = "A caption",
|
||||||
|
formattedCaption = null,
|
||||||
|
source = MediaSource("", ""),
|
||||||
|
info = null,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
textContent = "A text content",
|
||||||
|
senderProfile = ProfileDetails.Ready(
|
||||||
|
displayName = "Sender",
|
||||||
|
displayNameAmbiguous = false,
|
||||||
|
avatarUrl = null,
|
||||||
|
displayedStatus = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun inReplyToTextContent(): InReplyToDetails.Ready = InReplyToDetails.Ready(
|
||||||
|
eventId = EventId($$"$text_eventId"),
|
||||||
|
senderId = UserId("@sender:matrix.org"),
|
||||||
|
eventContent = MessageContent(
|
||||||
|
body = "A body",
|
||||||
|
inReplyTo = null,
|
||||||
|
isEdited = false,
|
||||||
|
threadInfo = null,
|
||||||
|
type = TextMessageType(
|
||||||
|
body = "A body",
|
||||||
|
formatted = null,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
textContent = "A text content",
|
||||||
|
senderProfile = ProfileDetails.Ready(
|
||||||
|
displayName = "Sender",
|
||||||
|
displayNameAmbiguous = false,
|
||||||
|
avatarUrl = null,
|
||||||
|
displayedStatus = null
|
||||||
|
)
|
||||||
|
)
|
||||||
+87
-28
@@ -33,9 +33,12 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.movableContentOf
|
import androidx.compose.runtime.movableContentOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -88,7 +91,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.ensureActiveLiveLocation
|
import io.element.android.features.messages.impl.timeline.model.event.ensureActiveLiveLocation
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
|
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||||
import io.element.android.features.messages.impl.timeline.protection.mustBeProtected
|
import io.element.android.features.messages.impl.timeline.protection.mustBeProtected
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
@@ -122,9 +124,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
|
|||||||
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.mediaSources
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState
|
||||||
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.rememberEventContentValidationState
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToView
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToView
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.reply.content
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.eventId
|
import io.element.android.libraries.matrix.ui.messages.reply.eventId
|
||||||
import io.element.android.libraries.matrix.ui.messages.sender.SenderName
|
import io.element.android.libraries.matrix.ui.messages.sender.SenderName
|
||||||
import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode
|
import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode
|
||||||
@@ -176,12 +182,12 @@ fun TimelineItemEventRow(
|
|||||||
val onContentClick = onEventClick.takeUnless { event.isWholeContentClickable }
|
val onContentClick = onEventClick.takeUnless { event.isWholeContentClickable }
|
||||||
|
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
|
eventId = event.eventId,
|
||||||
content = event.content,
|
content = event.content,
|
||||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
|
timelineProtectionState = timelineProtectionState,
|
||||||
onContentClick = onContentClick,
|
onContentClick = onContentClick,
|
||||||
onGalleryItemClick = onGalleryItemClick,
|
onGalleryItemClick = onGalleryItemClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onLinkLongClick = onLinkLongClick,
|
onLinkLongClick = onLinkLongClick,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
@@ -468,6 +474,23 @@ private fun TimelineItemEventRowContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val currentContentValidationState by rememberEventContentValidationState(eventId = event.eventId, needsValidation = event.content.isMedia)
|
||||||
|
.collectOverallState()
|
||||||
|
val needsInvalidContentCustomisations =
|
||||||
|
// Gallery events should not apply the custom bubble color, instead each item will apply some custom color if needed
|
||||||
|
event.content !is TimelineItemGalleryContent &&
|
||||||
|
event.content !is TimelineItemAttachmentsContent &&
|
||||||
|
currentContentValidationState.hasError() &&
|
||||||
|
event.content.isMedia
|
||||||
|
|
||||||
|
// If the event has a dangerous media content we need to set custom message bubble background and border colors
|
||||||
|
val themeColors = ElementTheme.colors
|
||||||
|
val (dangerousContentBubbleColor, borderColor) = remember(themeColors.isLight, needsInvalidContentCustomisations, event.content.type) {
|
||||||
|
val background = themeColors.bgCriticalSubtle.takeIf { needsInvalidContentCustomisations }
|
||||||
|
val border = themeColors.borderCriticalSubtle.takeIf { needsInvalidContentCustomisations }
|
||||||
|
background to border
|
||||||
|
}
|
||||||
|
|
||||||
// Message bubble
|
// Message bubble
|
||||||
val bubbleState = BubbleState(
|
val bubbleState = BubbleState(
|
||||||
groupPosition = event.groupPosition,
|
groupPosition = event.groupPosition,
|
||||||
@@ -494,6 +517,8 @@ private fun TimelineItemEventRowContent(
|
|||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onContentClick,
|
onClick = onContentClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
customBackgroundColor = dangerousContentBubbleColor,
|
||||||
|
borderColor = borderColor,
|
||||||
) {
|
) {
|
||||||
MessageEventBubbleContent(
|
MessageEventBubbleContent(
|
||||||
event = event,
|
event = event,
|
||||||
@@ -747,6 +772,7 @@ private fun MessageEventBubbleContent(
|
|||||||
}
|
}
|
||||||
ContentPadding.CaptionedMedia ->
|
ContentPadding.CaptionedMedia ->
|
||||||
Modifier.padding(start = 8.dp, end = 8.dp, top = topPadding, bottom = 8.dp)
|
Modifier.padding(start = 8.dp, end = 8.dp, top = topPadding, bottom = 8.dp)
|
||||||
|
ContentPadding.InvalidContent -> Modifier.padding(top = topPadding, bottom = 8.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
val threadDecoration = @Composable {
|
val threadDecoration = @Composable {
|
||||||
@@ -770,10 +796,13 @@ private fun MessageEventBubbleContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
|
val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
|
||||||
|
val currentContentValidationState by rememberEventContentValidationState(eventId = inReplyTo.eventId(), eventContent = inReplyTo.content())
|
||||||
|
.collectOverallState()
|
||||||
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
||||||
|
val shape = RoundedCornerShape(6.dp)
|
||||||
val inReplyToModifier = Modifier
|
val inReplyToModifier = Modifier
|
||||||
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
||||||
.clip(RoundedCornerShape(6.dp))
|
.clip(shape)
|
||||||
|
|
||||||
val talkbackCompatModifier = if (isTalkbackActive()) {
|
val talkbackCompatModifier = if (isTalkbackActive()) {
|
||||||
// Use z-index to make the replied to text being read after the message
|
// Use z-index to make the replied to text being read after the message
|
||||||
@@ -782,14 +811,25 @@ private fun MessageEventBubbleContent(
|
|||||||
} else {
|
} else {
|
||||||
inReplyToModifier.clickable(onClick = inReplyToClick)
|
inReplyToModifier.clickable(onClick = inReplyToClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val contentHasError = currentContentValidationState.hasError()
|
||||||
|
val borderColor = if (contentHasError) ElementTheme.colors.borderCriticalSubtle else ElementTheme.colors.separatorPrimary
|
||||||
|
val backgroundColor = if (contentHasError) ElementTheme.colors.bgCriticalSubtle else ElementTheme.colors.bgCanvasDefault
|
||||||
Box(
|
Box(
|
||||||
modifier = talkbackCompatModifier
|
modifier = talkbackCompatModifier
|
||||||
.border(1.dp, ElementTheme.colors.separatorPrimary, RoundedCornerShape(6.dp))
|
.border(1.dp, borderColor, shape)
|
||||||
.background(ElementTheme.colors.bgCanvasDefault, RoundedCornerShape(6.dp))
|
.background(backgroundColor, shape)
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
) {
|
) {
|
||||||
|
val contentValidationState = rememberEventContentValidationState(eventId = inReplyTo.eventId(), eventContent = inReplyTo.content())
|
||||||
|
val updatedEventSink by rememberUpdatedState(eventSink)
|
||||||
|
LaunchedEffect(inReplyTo) {
|
||||||
|
val mediaSources = inReplyTo.content()?.mediaSources() ?: return@LaunchedEffect
|
||||||
|
updatedEventSink(TimelineEvent.ValidateMedia(inReplyTo.eventId(), mediaSources, contentValidationState))
|
||||||
|
}
|
||||||
InReplyToView(
|
InReplyToView(
|
||||||
inReplyTo = inReplyTo,
|
inReplyTo = inReplyTo,
|
||||||
|
contentValidationValue = currentContentValidationState,
|
||||||
hideImage = timelineProtectionState.hideMediaContent(inReplyTo.eventId()),
|
hideImage = timelineProtectionState.hideMediaContent(inReplyTo.eventId()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -809,30 +849,47 @@ private fun MessageEventBubbleContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val timestampPosition = when (val content = event.content) {
|
val contentValidationState by rememberEventContentValidationState(eventId = event.eventId, needsValidation = event.content.isMedia).collectOverallState()
|
||||||
is TimelineItemImageContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
val needsInvalidContentLayout =
|
||||||
is TimelineItemVideoContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
// Gallery events should not apply custom paddings or layout dispositions
|
||||||
is TimelineItemGalleryContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Below
|
event.content !is TimelineItemGalleryContent &&
|
||||||
is TimelineItemAttachmentsContent -> TimestampPosition.Below
|
event.content !is TimelineItemAttachmentsContent &&
|
||||||
is TimelineItemStickerContent -> TimestampPosition.Overlay
|
contentValidationState.hasError()
|
||||||
is TimelineItemLocationContent -> {
|
|
||||||
val content = content.ensureActiveLiveLocation()
|
val timestampPosition = if (needsInvalidContentLayout) {
|
||||||
val shouldHide = content.mode is TimelineItemLocationContent.Mode.Live &&
|
// The invalid content view will be displayed in all these cases, independent of the event content
|
||||||
content.mode.isActive &&
|
TimestampPosition.Aligned
|
||||||
content.mode.isOwnUser
|
} else {
|
||||||
if (shouldHide) TimestampPosition.Hidden else TimestampPosition.Overlay
|
when (val content = event.content) {
|
||||||
|
is TimelineItemImageContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
||||||
|
is TimelineItemVideoContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
||||||
|
is TimelineItemGalleryContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Below
|
||||||
|
is TimelineItemAttachmentsContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Below
|
||||||
|
is TimelineItemStickerContent -> TimestampPosition.Overlay
|
||||||
|
is TimelineItemLocationContent -> {
|
||||||
|
val content = content.ensureActiveLiveLocation()
|
||||||
|
val shouldHide = content.mode is TimelineItemLocationContent.Mode.Live &&
|
||||||
|
content.mode.isActive &&
|
||||||
|
content.mode.isOwnUser
|
||||||
|
if (shouldHide) TimestampPosition.Hidden else TimestampPosition.Overlay
|
||||||
|
}
|
||||||
|
is TimelineItemPollContent -> TimestampPosition.Below
|
||||||
|
else -> TimestampPosition.Default
|
||||||
}
|
}
|
||||||
is TimelineItemPollContent -> TimestampPosition.Below
|
|
||||||
else -> TimestampPosition.Default
|
|
||||||
}
|
}
|
||||||
val paddingBehaviour = when (event.content) {
|
|
||||||
is TimelineItemImageContent -> if (event.content.showCaption) ContentPadding.CaptionedMedia else ContentPadding.Media
|
val paddingBehaviour = if (needsInvalidContentLayout) {
|
||||||
is TimelineItemVideoContent -> if (event.content.showCaption) ContentPadding.CaptionedMedia else ContentPadding.Media
|
ContentPadding.InvalidContent
|
||||||
is TimelineItemGalleryContent -> ContentPadding.CaptionedMedia
|
} else {
|
||||||
is TimelineItemAttachmentsContent -> ContentPadding.CaptionedMedia
|
when (event.content) {
|
||||||
is TimelineItemStickerContent,
|
is TimelineItemImageContent -> if (event.content.showCaption) ContentPadding.CaptionedMedia else ContentPadding.Media
|
||||||
is TimelineItemLocationContent -> ContentPadding.Media
|
is TimelineItemVideoContent -> if (event.content.showCaption) ContentPadding.CaptionedMedia else ContentPadding.Media
|
||||||
else -> ContentPadding.Textual
|
is TimelineItemGalleryContent -> ContentPadding.CaptionedMedia
|
||||||
|
is TimelineItemAttachmentsContent -> ContentPadding.CaptionedMedia
|
||||||
|
is TimelineItemStickerContent,
|
||||||
|
is TimelineItemLocationContent -> ContentPadding.Media
|
||||||
|
else -> ContentPadding.Textual
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CommonLayout(
|
CommonLayout(
|
||||||
showThreadDecoration = timelineMode !is Timeline.Mode.Thread && event.threadInfo is TimelineItemThreadInfo.ThreadResponse,
|
showThreadDecoration = timelineMode !is Timeline.Mode.Thread && event.threadInfo is TimelineItemThreadInfo.ThreadResponse,
|
||||||
@@ -905,6 +962,7 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
|
|||||||
displayName = USER_NAME_ALICE,
|
displayName = USER_NAME_ALICE,
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
displayNameAmbiguous = false,
|
displayNameAmbiguous = false,
|
||||||
|
displayedStatus = null,
|
||||||
),
|
),
|
||||||
timestamp = 0L,
|
timestamp = 0L,
|
||||||
)
|
)
|
||||||
@@ -1083,6 +1141,7 @@ internal fun ThreadSummaryViewPreview() {
|
|||||||
displayName = USER_NAME_ALICE,
|
displayName = USER_NAME_ALICE,
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
displayNameAmbiguous = true,
|
displayNameAmbiguous = true,
|
||||||
|
displayedStatus = null,
|
||||||
),
|
),
|
||||||
timestamp = 0L,
|
timestamp = 0L,
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-5
@@ -28,7 +28,6 @@ import io.element.android.features.messages.impl.timeline.components.receipt.Rea
|
|||||||
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||||
import io.element.android.features.messages.impl.timeline.groups.isRedactedMessagesGroup
|
import io.element.android.features.messages.impl.timeline.groups.isRedactedMessagesGroup
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
|
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||||
import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
|
import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
@@ -62,9 +61,9 @@ fun TimelineItemGroupedEventsRow(
|
|||||||
eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit =
|
eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit =
|
||||||
{ event, contentModifier, onContentLayoutChange ->
|
{ event, contentModifier, onContentLayoutChange ->
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
|
eventId = event.eventId,
|
||||||
content = event.content,
|
content = event.content,
|
||||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine),
|
timelineProtectionState = timelineProtectionState,
|
||||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onLinkLongClick = onLinkLongClick,
|
onLinkLongClick = onLinkLongClick,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
@@ -134,9 +133,9 @@ private fun TimelineItemGroupedEventsRowContent(
|
|||||||
eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit =
|
eventContentView: @Composable (TimelineItem.Event, Modifier, (ContentAvoidingLayoutData) -> Unit) -> Unit =
|
||||||
{ event, contentModifier, onContentLayoutChange ->
|
{ event, contentModifier, onContentLayoutChange ->
|
||||||
TimelineItemEventContentView(
|
TimelineItemEventContentView(
|
||||||
|
eventId = event.eventId,
|
||||||
content = event.content,
|
content = event.content,
|
||||||
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId, event.isMine),
|
timelineProtectionState = timelineProtectionState,
|
||||||
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
|
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
onLinkLongClick = onLinkLongClick,
|
onLinkLongClick = onLinkLongClick,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
@@ -187,6 +186,7 @@ private fun TimelineItemGroupedEventsRowContent(
|
|||||||
onReactionLongClick = onReactionLongClick,
|
onReactionLongClick = onReactionLongClick,
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
|
onJoinCallClick = {},
|
||||||
onSwipeToReply = {},
|
onSwipeToReply = {},
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
eventContentView = eventContentView,
|
eventContentView = eventContentView,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user