Compare commits

..

198 Commits

Author SHA1 Message Date
Sowjanya Kota
95f8b08a19
Merge pull request #5022 from nextcloud/bugfix/4921/avoidDupMessagesByBetterSentStatus
avoid duplicate messages
2025-06-18 18:36:17 +02:00
Nextcloud bot
2a7359c1e9
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-18 03:08:20 +00:00
Marcel Hibbe
86bfaa8657
improve send status handling
replace sendingFailed with SendStatus

add auto migration (incl deleting of column sendingFailed)

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-17 17:21:36 +02:00
Marcel Hibbe
8c066eb521
avoid duplicate messages by introducing a sendStatus
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-17 17:16:53 +02:00
Andy Scherzinger
9e08af3306
Merge pull request #4468 from nextcloud/renovate/ghcr.io-nextcloud-continuous-integration-android8-4.x
chore(deps): update ghcr.io/nextcloud/continuous-integration-android8 docker tag to v4
2025-06-17 15:27:52 +02:00
Nextcloud bot
95f7a1e312
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-17 11:01:35 +00:00
Marcel Hibbe
73271018f5
Merge pull request #5065 from nextcloud/testFixAnalysisForMergeCommits
Try to run analysis also for merge commits
2025-06-17 10:08:18 +00:00
Marcel Hibbe
7111109ac0
Try to run analysis also for merge commits
Trying to fix
https://github.com/nextcloud/android-config/pull/248/files

which caused that analysis is not only blocked for forks, bit also for merge commits.

Solution: If it's not a PR the step should be omitted..

This commit will be checked when workflow runs on github. It will be merged just to test it's working..

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-17 11:24:01 +02:00
Andy Scherzinger
e3c83823e6
Merge pull request #5064 from nextcloud/renovate/gradle-actions-4.x
chore(deps): update gradle/actions action to v4.4.1
2025-06-17 10:50:08 +02:00
renovate[bot]
3b11a90aac
chore(deps): update gradle/actions action to v4.4.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 08:40:59 +00:00
Marcel Hibbe
bc29c67269
Merge pull request #5061 from nextcloud/fixWrongPluralsTranslation
fix wrong plurals implementation
2025-06-17 07:33:03 +00:00
Marcel Hibbe
4a93551ef9
fix wrong plurals implementation
- key was duplicated
- kotlin handling of plurals was missing

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-16 10:10:02 +02:00
Nextcloud Android Bot
d0ff4320a8 Weekly 22.0.0 Alpha 06 2025-06-16 03:11:01 +00:00
Marcel Hibbe
20b70c2728
Merge pull request #5032 from nextcloud/issue-5016-app-shortcuts
Adding a short cut to note-to-self
2025-06-13 17:24:48 +00:00
Marcel Hibbe
6aab2e27cd
Merge pull request #5053 from nextcloud/issue-5051-seekbar-illegal-state-exception
Better null and error handling when playing voice messages
2025-06-13 12:44:44 +00:00
Marcel Hibbe
a34ad80a90
remove comment
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-13 14:14:06 +02:00
rapterjet2004
5a22f27b64
Adding a Short cut to note-to-self
Translations, and focuses on edittext upon opening

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-06-13 13:43:00 +02:00
Nextcloud bot
776ba77c3a
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-13 03:04:33 +00:00
Andy Scherzinger
3cf01d9123
Merge pull request #5056 from nextcloud/repo-sync/android-config/master
🔄 synced file(s) with nextcloud/android-config
2025-06-12 07:28:45 +02:00
nextcloud-android-bot
2ab9f168d2 🔄 synced local '.github/workflows/' with remote 'config/workflows/'
Signed-off-by: nextcloud-android-bot <android@nextcloud.com>
2025-06-12 03:14:09 +00:00
github-actions[bot]
038a30dcca
Merge pull request #5055 from nextcloud/renovate/com.github.spotbugs.snom-spotbugs-gradle-plugin-6.x
fix(deps): update dependency com.github.spotbugs.snom:spotbugs-gradle-plugin to v6.2.0
2025-06-11 10:53:34 +02:00
github-actions[bot]
ddc40537d5
Merge pull request #5054 from nextcloud/renovate/ksp-monorepo
chore(deps): update plugin com.google.devtools.ksp to v2.1.21-2.0.2
2025-06-11 10:53:07 +02:00
renovate[bot]
bd23a05a88
fix(deps): update dependency com.github.spotbugs.snom:spotbugs-gradle-plugin to v6.2.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 07:21:21 +00:00
renovate[bot]
76f1e1c005
chore(deps): update plugin com.google.devtools.ksp to v2.1.21-2.0.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 07:17:38 +00:00
Nextcloud bot
40c9816827
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-11 03:07:38 +00:00
rapterjet2004
a7f742931e
better null and error handling in the seekbar update observer
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-06-10 11:20:42 -05:00
Marcel Hibbe
a361240692
Merge pull request #5050 from nextcloud/ci/noid/metaData
ci(chksm): Add meta-data
2025-06-10 10:21:50 +00:00
Andy Scherzinger
fe6897baf4
ci(chksm): Add meta-data
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-06-10 11:46:53 +02:00
Nextcloud bot
18578521cf
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-10 03:05:47 +00:00
Nextcloud Android Bot
4b71c50fb2 Weekly 22.0.0 Alpha 05 2025-06-09 03:23:12 +00:00
github-actions[bot]
7ec7904691
Merge pull request #5047 from nextcloud/renovate/junit5-monorepo
fix(deps): update dependency org.junit.vintage:junit-vintage-engine to v5.13.1
2025-06-08 20:20:57 +02:00
github-actions[bot]
34992438ed
Merge pull request #5046 from nextcloud/renovate/com.mebigfatguy.fb-contrib-fb-contrib-7.x
fix(deps): update dependency com.mebigfatguy.fb-contrib:fb-contrib to v7.6.10
2025-06-08 20:17:24 +02:00
github-actions[bot]
af721510e1
Merge pull request #5045 from nextcloud/renovate/androidx.appcompat-appcompat-1.x
fix(deps): update dependency androidx.appcompat:appcompat to v1.7.1
2025-06-08 20:04:51 +02:00
renovate[bot]
393c70f6e6
fix(deps): update dependency org.junit.vintage:junit-vintage-engine to v5.13.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 17:57:26 +00:00
renovate[bot]
d28bcbd5e9
fix(deps): update dependency com.mebigfatguy.fb-contrib:fb-contrib to v7.6.10
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 17:56:11 +00:00
renovate[bot]
03e3737e69
fix(deps): update dependency androidx.appcompat:appcompat to v1.7.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 17:52:40 +00:00
github-actions[bot]
a5fe9ec133
Merge pull request #5044 from nextcloud/renovate/gradle-8.x
chore(deps): update dependency gradle to v8.14.2
2025-06-08 17:20:36 +00:00
renovate[bot]
6633ae223c
chore(deps): update dependency gradle to v8.14.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 16:43:20 +00:00
Andy Scherzinger
1dd9d6ba7b
Merge pull request #5043 from nextcloud/renovate/ubuntu-noble
chore(deps): update ubuntu:noble docker digest to b59d215
2025-06-08 17:53:21 +02:00
renovate[bot]
1a00678e46
chore(deps): update ubuntu:noble docker digest to b59d215
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 15:52:12 +00:00
github-actions[bot]
77944b4367
Merge pull request #4993 from nextcloud/renovate/retrofit-monorepo
fix(deps): update retrofit monorepo to v2.12.0
2025-06-08 15:35:04 +00:00
Andy Scherzinger
9b889ef751
ci(lint): Bump score to reflect main branch state
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-06-08 16:57:57 +02:00
Andy Scherzinger
ccecb9005c
ci(chksm): Add meta-data
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-06-08 16:15:26 +02:00
renovate[bot]
ccb700376a
fix(deps): update retrofit monorepo to v2.12.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 16:00:22 +02:00
Nextcloud bot
f574359432
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-08 03:05:03 +00:00
Andy Scherzinger
ba690c0615
Merge pull request #5040 from nextcloud/repo-sync/android-config/master
🔄 synced file(s) with nextcloud/android-config
2025-06-07 08:13:45 +02:00
nextcloud-android-bot
ccf955f43e 🔄 synced local '.github/workflows/' with remote 'config/workflows/'
Signed-off-by: nextcloud-android-bot <android@nextcloud.com>
2025-06-07 03:11:25 +00:00
Nextcloud bot
856b1ebf8c
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-07 03:03:38 +00:00
Nextcloud bot
b1448e13b5
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-06 03:12:36 +00:00
Marcel Hibbe
224eb3d0e8
update changelog for v21.1.0
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-05 12:38:49 +02:00
Nextcloud bot
4158628475
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-05 03:07:45 +00:00
Sowjanya Kota
e88bd1caf7
Merge pull request #5035 from nextcloud/bugfix/4983/fixCrashForHttp405ForProfileData
Profile data in ConversationInfo: catch http 405 if endpoint is not available
2025-06-04 13:43:59 +02:00
Marcel Hibbe
27ba2acf86
catch http 405 if endpoint is not available.
E.g. for older server versions

Without this fix there would be the crash:

 E  FATAL EXCEPTION: main
 Process: com.nextcloud.talk2, PID: 7161
  retrofit2.HttpException: HTTP 405
  at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
  at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:164)
  at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
  at java.lang.Thread.run(Thread.java:1012)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@4a67b41, Dispatchers.Main.immediate]

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-04 13:10:42 +02:00
Marcel Hibbe
4efba2b953
Merge pull request #5028 from nextcloud/ui_enhancements
UI improvements to automatic conversation deletion information for event, sip and instant meetings conversation
2025-06-04 07:51:27 +00:00
Marcel Hibbe
f64e9e7c66
Merge pull request #5030 from nextcloud/issue-5018-dark-mode-bug-fix
Fixes message timestamp color
2025-06-04 07:35:54 +00:00
Nextcloud bot
6cd8718ac8
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-04 03:06:30 +00:00
Marcel Hibbe
eb72c70520
Merge pull request #5024 from nextcloud/fixToNotShowContextSearchAccidentally
simplify how search results are shown
2025-06-03 17:24:12 +00:00
rapterjet2004
ca06333c48
Fixes message timestamp color
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-06-03 12:09:52 -05:00
Sowjanya Kota
325ffc4443
Merge pull request #5023 from nextcloud/fixCanDeleteCanLeaveForConvMenu
Fix & rearrange options in conversation button menu
2025-06-03 18:50:22 +02:00
sowjanyakch
89bddbd8fd
format
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-06-03 18:35:10 +02:00
sowjanyakch
de62d2776c
remove menu item after pressing keep button
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-06-03 18:35:10 +02:00
sowjanyakch
9aa1622929
popup menu color
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-06-03 18:35:10 +02:00
sowjanyakch
9dbb7ab703
consistent color to text and icon
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-06-03 18:35:09 +02:00
Marcel Hibbe
8307a9a0df
Merge pull request #5020 from nextcloud/mentions_complete
Include mentions in the attachments
2025-06-03 12:24:16 +00:00
Marcel Hibbe
a354ca197e
simplify how search results are shown
1. reverting changes from commit 971cc79b76. The scroll to methods are used in other places and a user reported that the search window suddenly opened without to search.
2. remove checkIfMessageIsSaved method. If the message is saved or not does not matter. The only fact for "scroll to" would be that it's loaded in the adapter.
3. As the adapter is always null with the current implementation (when coming back from the first search window), the "scroll to" is replaced with the startContextChatWindowForMessage. This could be changed again when adapter is not null anymore after coming back from the other screen...

startContextChatWindowForMessage

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-03 14:10:07 +02:00
Marcel Hibbe
12cb0825ca
show "Archive conversation" just before Leave & Delete
same as for web/iOS

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-03 12:17:32 +02:00
Marcel Hibbe
cc8e241213
show "Delete conversation" as last entry
same as for web/iOS

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-03 12:16:16 +02:00
Marcel Hibbe
14fd9b4af8
rely on conversationOperationLeave to show "Leave conversation" button
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-03 12:12:04 +02:00
Marcel Hibbe
9fe39603c3
rely on canDeleteConversation to show "Delete conversation" button
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-03 12:09:50 +02:00
Nextcloud bot
707371603d
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-03 03:05:04 +00:00
sowjanyakch
632a26d3cb
format
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-06-02 15:28:50 +02:00
sowjanyakch
262205b615
Include mentions in the attachments
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-06-02 14:59:32 +02:00
Nextcloud bot
131723317b
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-02 03:39:23 +00:00
Nextcloud Android Bot
e299023ef1 Weekly 22.0.0 Alpha 04 2025-06-02 03:11:10 +00:00
Nextcloud bot
92e1028354
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-01 03:03:48 +00:00
Andy Scherzinger
ebc41c33d0
Merge pull request #5015 from nextcloud/repo-sync/android-config/master
🔄 synced file(s) with nextcloud/android-config
2025-05-31 09:05:48 +02:00
nextcloud-android-bot
b11e5a474c 🔄 synced local '.github/workflows/' with remote 'config/workflows/'
Signed-off-by: nextcloud-android-bot <android@nextcloud.com>
2025-05-31 03:08:55 +00:00
Nextcloud bot
54ac15d089
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-31 03:04:30 +00:00
github-actions[bot]
b3c7990c88
Merge pull request #5013 from nextcloud/renovate/junit5-monorepo
Update dependency org.junit.vintage:junit-vintage-engine to v5.13.0
2025-05-30 21:17:46 +02:00
github-actions[bot]
88ed309220
Merge pull request #5012 from nextcloud/renovate/com.github.spotbugs.snom-spotbugs-gradle-plugin-6.x
Update dependency com.github.spotbugs.snom:spotbugs-gradle-plugin to v6.1.13
2025-05-30 21:15:00 +02:00
github-actions[bot]
5855ec7b38
Merge pull request #5011 from nextcloud/renovate/com.android.tools.build-gradle-8.x
Update dependency com.android.tools.build:gradle to v8.10.1
2025-05-30 21:07:58 +02:00
Marcel Hibbe
6f4cc903f8
Merge pull request #5014 from nextcloud/bugfix/4983/tryToAvoidCrashInConvInfo
try to avoid crash in conversation info
2025-05-30 14:17:02 +00:00
Marcel Hibbe
c01e058ff9
Merge pull request #5010 from nextcloud/initializeRootEglBaseEarlier
initialize rootEglBase earlier (hopefully fixes crash)
2025-05-30 14:15:17 +00:00
Marcel Hibbe
a559fd5ea6
try to avoid crash in conversation info
could not reproduce.
Code copied from Julius' PR https://github.com/nextcloud/talk-android/pull/5004/

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-30 16:10:20 +02:00
renovate[bot]
d33081c387
Update dependency org.junit.vintage:junit-vintage-engine to v5.13.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-30 14:02:57 +00:00
renovate[bot]
b2e9429cb5
Update dependency com.github.spotbugs.snom:spotbugs-gradle-plugin to v6.1.13
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-30 14:01:21 +00:00
renovate[bot]
073a0462d9
Update dependency com.android.tools.build:gradle to v8.10.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-30 13:58:22 +00:00
Marcel Hibbe
c8b33a380a
initialize rootEglBase earlier (hopefully fixes crash)
There was a report that the app crashed after coming back from PIP mode.

And i once saw in logs

"call to OpenGL ES API with no current context (logged once per thread)"

By initializing rootEglBase very early there is a chance this fixes these issues.

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-30 15:34:20 +02:00
Marcel Hibbe
78a88a9ce5
Merge pull request #5009 from nextcloud/issue-4966-fix-fetch-users
Getting users to show up on query
2025-05-30 13:22:00 +00:00
rapterjet2004
1ccc3ebb94
Getting users to show up on query
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-30 14:43:22 +02:00
Marcel Hibbe
89d51837b7
Merge pull request #4898 from nextcloud/add_button_to_add_participants
"Add" button to add participants
2025-05-30 12:07:39 +00:00
Marcel Hibbe
6ec4e05cb1
suppress kltint warning about Property name
not sure why "Property name should start with a lowercase letter and use camel case (cannot be auto-corrected)" is shown.

also see commit
523b8080d1

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-30 09:49:00 +02:00
sowjanyakch
87f8272b10
ktlintFormat
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:49:00 +02:00
sowjanyakch
05815ebeae
bug with search view alignment
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:59 +02:00
sowjanyakch
6d4e5d2774
hide added groups/circles
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:59 +02:00
sowjanyakch
f0cbe5113f
remove add width ratio
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:59 +02:00
sowjanyakch
4f8584bc75
remove already selected participants from the list
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:59 +02:00
sowjanyakch
652dd5033a
use constants and proper names
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:59 +02:00
sowjanyakch
6e48b86940
suppress lint - property name
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:58 +02:00
sowjanyakch
18b75233a5
lintformat
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:58 +02:00
sowjanyakch
b9e9d0ccd1
show selected participants when clicking on add button
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:58 +02:00
sowjanyakch
c3ebeebcb0
update contacts list after clicking add button
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:58 +02:00
sowjanyakch
c5c129b706
show add button even when search query is empty
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:58 +02:00
sowjanyakch
96541b7ad8
enable add button only when you select contacts
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:57 +02:00
sowjanyakch
e689e4f7f8
adjust search component width
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:57 +02:00
sowjanyakch
ebe374c9cb
lint format
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:57 +02:00
sowjanyakch
350bba1b95
detekt
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:57 +02:00
sowjanyakch
29d5293587
Add button to add participants
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-30 09:48:57 +02:00
Julius Linus
eb75f488c5
Merge pull request #5008 from nextcloud/simplifyCallGridLayoutForVoiceOnly
simplify call grid design logic for voice only calls
2025-05-29 10:20:56 -05:00
Marcel Hibbe
c349f120dc
Merge pull request #5006 from nextcloud/sensitive_conversation
Follow-up fixes for Sensitive conversation
2025-05-28 13:34:03 +00:00
Marcel Hibbe
6445633c94
simplify call grid design for voice only calls
this will remove the workaround regarding heightForNonGridComponents by using a BoxWithConstraints

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-28 15:04:05 +02:00
sowjanyakch
ac170f0803
adjust layout
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-28 10:21:50 +02:00
sowjanyakch
1fe356eee2
apply theming to switch
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-28 08:37:34 +02:00
Marcel Hibbe
b35bfc1ee7
Merge pull request #4973 from nextcloud/important_conversations
Important conversations
2025-05-27 13:51:22 +00:00
Marcel Hibbe
25639702f6
minor refactoring
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-27 15:25:08 +02:00
sowjanyakch
6c5347ef72
ktlintFormat + suppress property name
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-27 14:38:21 +02:00
sowjanyakch
3f512c7b54
modify layout
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-27 14:38:21 +02:00
sowjanyakch
a943dc1070
modify database scheme
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-27 14:38:21 +02:00
sowjanyakch
2e41b27dc8
revert 14.json
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-27 14:38:21 +02:00
sowjanyakch
38482b8bb5
mark conversation as important
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-27 14:38:21 +02:00
sowjanyakch
62041d2581
refactor
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-27 14:38:20 +02:00
sowjanyakch
7548ce58b5
mark conversation as important / unimportant
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-27 14:38:20 +02:00
sowjanyakch
799d108708
add repo and repo implementation
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-27 14:38:20 +02:00
sowjanyakch
4803712ca3
add endpoints and hasImportant variable
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-27 14:38:19 +02:00
Marcel Hibbe
8db97fe0d9
Merge pull request #4996 from nextcloud/archive_conversation
hide archive conversations from conversation list in default view
2025-05-27 10:54:01 +00:00
Nextcloud bot
635624006d
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-27 10:36:30 +00:00
Marcel Hibbe
d3acf3078b
Merge pull request #4998 from nextcloud/fixStringsxmlNoLinebreak
remove linebreak
2025-05-27 10:33:40 +00:00
Marcel Hibbe
85a7bd010b
remove linebreak
...should fix transifex

also add comment for this

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-27 12:31:44 +02:00
sowjanyakch
e7d3af1a21
hide archive conversation initially
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-26 20:04:23 +02:00
github-actions[bot]
6a8cb99aeb
Merge pull request #4989 from nextcloud/renovate/com.github.spotbugs.snom-spotbugs-gradle-plugin-6.x
fix(deps): update dependency com.github.spotbugs.snom:spotbugs-gradle-plugin to v6.1.12
2025-05-26 16:17:49 +00:00
Andy Scherzinger
29054d9062
Merge pull request #4990 from nextcloud/renovate/gradle-actions-4.x
chore(deps): update gradle/actions action to v4.4.0
2025-05-26 18:11:05 +02:00
Nextcloud Android Bot
ccd33dd578 Weekly 22.0.0 Alpha 03 2025-05-26 03:11:14 +00:00
github-actions[bot]
39ac9974b6
Merge pull request #4988 from nextcloud/renovate/androidx.datastore-datastore-preferences-1.x
fix(deps): update dependency androidx.datastore:datastore-preferences to v1.1.7
2025-05-25 19:29:57 +00:00
renovate[bot]
ba430c0c3c
fix(deps): update dependency androidx.datastore:datastore-preferences to v1.1.7
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-25 19:15:37 +00:00
github-actions[bot]
85d9d2cb77
Merge pull request #4991 from nextcloud/renovate/org.jlleitschuh.gradle-ktlint-gradle-12.x
fix(deps): update dependency org.jlleitschuh.gradle:ktlint-gradle to v12.3.0
2025-05-25 19:11:01 +00:00
github-actions[bot]
ed2903a139
Merge pull request #4987 from nextcloud/renovate/androidx.datastore-datastore-core-1.x
fix(deps): update dependency androidx.datastore:datastore-core to v1.1.7
2025-05-25 19:07:39 +00:00
github-actions[bot]
7239af603d
Merge pull request #4992 from nextcloud/renovate/mockito-monorepo
fix(deps): update mockito monorepo to v5.18.0
2025-05-25 19:03:05 +00:00
github-actions[bot]
1718b1faa6
Merge pull request #4986 from nextcloud/renovate/gradle-8.x
chore(deps): update dependency gradle to v8.14.1
2025-05-25 20:41:49 +02:00
renovate[bot]
d5c8d4743d
fix(deps): update mockito monorepo to v5.18.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-25 18:33:03 +00:00
renovate[bot]
2af86c7bcc
fix(deps): update dependency org.jlleitschuh.gradle:ktlint-gradle to v12.3.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-25 18:31:42 +00:00
renovate[bot]
ff408cd988
chore(deps): update gradle/actions action to v4.4.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-25 18:28:50 +00:00
renovate[bot]
9289f3662d
fix(deps): update dependency com.github.spotbugs.snom:spotbugs-gradle-plugin to v6.1.12
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-25 18:28:45 +00:00
renovate[bot]
6aed879d57
fix(deps): update dependency androidx.datastore:datastore-core to v1.1.7
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-25 18:26:00 +00:00
renovate[bot]
aaf8000a31
chore(deps): update dependency gradle to v8.14.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-25 18:22:55 +00:00
Julius Linus
7f863e543a
Merge pull request #4977 from nextcloud/retain_state
Filter does not retain its state - Conversations
2025-05-23 15:00:03 -05:00
Marcel Hibbe
ac116c91f5
Merge pull request #4971 from nextcloud/sensitive_information
Allow to mark conversation as sensitive
2025-05-23 09:05:43 +00:00
Marcel Hibbe
523b8080d1
Suppress strange ktlint warning about PropertyName
i have absolutely no idea why
 "Property name should start with a lowercase letter and use camel case (cannot be auto-corrected)"
 is shown for these properties. It should be allowed for backing properties, just like for the others in this class?!
 Thus, for now the suppress

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-23 10:15:44 +02:00
sowjanyakch
f28bf02380
merge conflict
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-23 10:15:44 +02:00
sowjanyakch
d159a577ba
improve UI
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-23 10:15:44 +02:00
sowjanyakch
59bfaa6cd2
ktlintFormat
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-23 10:15:44 +02:00
sowjanyakch
61af44f3f4
api call
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-23 10:15:43 +02:00
sowjanyakch
8f46531699
add logic to conversationInfoActivity
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-23 10:15:43 +02:00
sowjanyakch
98d5b3da72
modify repo, repo implementation and viewModel
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-23 10:15:43 +02:00
sowjanyakch
d899824ebc
add layout and modify the variable name
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-23 10:15:43 +02:00
sowjanyakch
b4de86b84e
add database migration
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-23 10:15:43 +02:00
sowjanyakch
c337d5087b
add endpoints and add isSensitive parameter
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-23 10:15:42 +02:00
Nextcloud bot
07dc25d3bf
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-23 03:05:12 +00:00
Marcel Hibbe
2585439ddc
Merge pull request #4972 from nextcloud/issue-4941-support-heic
Allows .HEIC files to be opened in app
2025-05-22 16:56:19 +00:00
Marcel Hibbe
97f793ffbf
Merge pull request #4969 from nextcloud/improvements_event_conversations
Improvements event conversations
2025-05-22 15:23:29 +00:00
Marcel Hibbe
8836195f92
delete useless string & logic for conversationDeleteNotice
... string text was exactly the same

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-22 17:03:31 +02:00
Marcel Hibbe
38eba2f6c9
avoid NPE for eventEndTimeStamp
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-22 16:45:21 +02:00
Marcel Hibbe
9493e7889f
fix to avoid linebreaks in strings.xml
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-22 16:22:53 +02:00
sowjanyakch
ab8a41182a
add different object types for phone
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:16:00 +02:00
sowjanyakch
354b107e1c
objectId instead of objectType
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:16:00 +02:00
sowjanyakch
c185563794
ktlintFormat
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:59 +02:00
sowjanyakch
e72701c219
ktlintFormat
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:59 +02:00
sowjanyakch
3007633873
only show delete and keep options for moderators of a conversation
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:59 +02:00
sowjanyakch
5512a47d32
color change
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:59 +02:00
sowjanyakch
6659b664d8
add meaningful names
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:59 +02:00
sowjanyakch
d6a2a1fe27
modify strings
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:58 +02:00
sowjanyakch
1670ff181f
add unbind-conversation capability
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:58 +02:00
sowjanyakch
01b80a0753
retain conversation successfully
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:58 +02:00
sowjanyakch
1ed89b2a53
delete event conversation
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:58 +02:00
sowjanyakch
4d3acdb2f5
don't show deletion warning initially
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:58 +02:00
sowjanyakch
0104989eef
show warning dialog when call ends
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:57 +02:00
sowjanyakch
7a4c98db01
add different room types and their retention
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:57 +02:00
sowjanyakch
ccb2fcfcad
create a layout to show conversation delete notice
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 15:15:57 +02:00
Marcel Hibbe
7d1308b718
Merge pull request #4854 from nextcloud/issue-4815-markdown-table
🧮 Markdown Tables
2025-05-22 13:12:37 +00:00
rapterjet2004
8f0ef1900e
Allows .HEIC files to be opened in app + themes the lock conversation switch
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-05-22 15:08:33 +02:00
rapterjet2004
62d9a47c37
Tables work
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-05-22 14:53:54 +02:00
Marcel Hibbe
39b7931534
Merge pull request #4877 from nextcloud/issue-4762-search-bug
Fixing bugs with Conversation search
2025-05-22 12:48:57 +00:00
sowjanyakch
6f9522456f
ktlintFormat
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 09:13:43 +02:00
sowjanyakch
29523cf0da
get filter states before getting conversations for adapter
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-05-22 08:57:20 +02:00
Nextcloud bot
d41f5d449a
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-22 03:06:09 +00:00
rapterjet2004
11b25324b4
small fix with messages
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-05-21 17:32:40 +02:00
rapterjet2004
cd79275475
archive filter UI is disabled when searching
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-05-21 17:32:39 +02:00
Marcel Hibbe
0ba959fb69
Merge pull request #4889 from nextcloud/issue-4885-followup-improvements
Follow up fixes for Improving Chat Message Search
2025-05-21 15:32:06 +00:00
Marcel Hibbe
9c651abbb8
fix SPDX-FileCopyrightTexts
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-21 17:15:41 +02:00
Marcel Hibbe
225291fe20
comment out call icons and menu
As this is (at least for now) only used for the search results window, calls button and menu doesn't make sense.
At some later point in time this could be used when chat activity is migrated to compose..

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-21 17:14:28 +02:00
Marcel Hibbe
971cc79b76
open context search when message was not found or adapter was null
Opening it when adapter was null is a bit hacky but it works (better would be to make sure adapter is not null of course)

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-05-21 16:56:22 +02:00
rapterjet2004
0b40e06f46
Appended to work with replies
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-05-21 14:28:58 +02:00
rapterjet2004
fd7afccbc4
Follow up improvements
- Added ComposePreviewUtils
- Added ComposePreviewUtilsDao (both for previewing w/ dependencies)
- Additional fixes

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-05-21 14:28:57 +02:00
Marcel Hibbe
fdfa58dcdd
Merge pull request #4950 from nextcloud/issue-4714-improve-sidebar
👤 Improving right sidebar
2025-05-21 11:51:24 +00:00
rapterjet2004
9a2049d8d4
improve UI
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-05-21 13:37:22 +02:00
rapterjet2004
19b8dc7ce7
Added profile fields to conversation info + API functions
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-05-21 13:37:21 +02:00
Nextcloud bot
3c2fc8f34f
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-20 03:09:14 +00:00
Andy Scherzinger
71c8719f88
ci: Update signature
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-02-17 19:09:06 +01:00
renovate[bot]
2afa9ca80a chore(deps): update ghcr.io/nextcloud/continuous-integration-android8 docker tag to v4
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 19:07:42 +01:00
131 changed files with 5960 additions and 628 deletions

View File

@ -1,4 +1,4 @@
FROM ubuntu:noble@sha256:6015f66923d7afbc53558d7ccffd325d43b4e249f41a6e93eef074c9505d2233
FROM ubuntu:noble@sha256:b59d21599a2b151e23eea5f6602f4af4d7d31c4e236d22bf0b62b86d2e386b8f
ARG DEBIAN_FRONTEND=noninteractive
ENV ANDROID_HOME=/usr/lib/android-sdk

View File

@ -8,7 +8,7 @@ name: generic
steps:
- name: generic
image: ghcr.io/nextcloud/continuous-integration-android8:3
image: ghcr.io/nextcloud/continuous-integration-android8:4
commands:
- ./gradlew --console=plain assembleGeneric
@ -27,7 +27,7 @@ name: gplay
steps:
- name: gplay
image: ghcr.io/nextcloud/continuous-integration-android8:3
image: ghcr.io/nextcloud/continuous-integration-android8:4
commands:
- ./gradlew --console=plain assembleGplay
@ -46,7 +46,7 @@ name: tests
steps:
- name: all
image: ghcr.io/nextcloud/continuous-integration-android8:3
image: ghcr.io/nextcloud/continuous-integration-android8:4
privileged: true
commands:
- emulator -avd android -no-snapshot -gpu swiftshader_indirect -no-window -no-audio -skin 500x833 &
@ -81,4 +81,6 @@ trigger:
- pull_request
---
kind: signature
hmac: cdce3f7eea46ef85c0223f62f66d1fe53d7dad007ef095c9f70fa063450d8c75
hmac: cf0c19e54fa45d1ee226f5f05202a32329b90aaf46711ea073c566a4c4a8a6c5
...

View File

@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Disabled on forks
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
run: |
echo 'Can not analyze PRs from forks'
exit 1

View File

@ -34,7 +34,7 @@ jobs:
java-version: 17
- name: Gradle validate
uses: gradle/actions/wrapper-validation@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
uses: gradle/actions/wrapper-validation@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
- name: Build ${{ matrix.flavor }}
run: |

View File

@ -43,7 +43,7 @@ jobs:
with:
swap-size-gb: 10
- name: Initialize CodeQL
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
with:
languages: ${{ matrix.language }}
- name: Set up JDK 17
@ -57,4 +57,4 @@ jobs:
echo "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties"
./gradlew assembleDebug
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0

View File

@ -36,7 +36,7 @@ jobs:
blocklist=$(curl https://raw.githubusercontent.com/nextcloud/.github/master/non-community-usernames.txt | paste -s -d, -)
echo "blocklist=$blocklist" >> "$GITHUB_OUTPUT"
- uses: nextcloud/pr-feedback-action@1883b38a033fb16f576875e0cf45f98b857655c4 # main
- uses: nextcloud/pr-feedback-action@f0cab224dea8e1f282f9451de322f323c78fc7a5 # main
with:
feedback-message: |
Hello there,

View File

@ -34,7 +34,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
with:
results_file: results.sarif
results_format: sarif
@ -42,6 +42,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
with:
sarif_file: results.sarif

View File

@ -33,7 +33,7 @@ jobs:
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
- name: Run unit tests with coverage
run: ./gradlew testGplayDebugUnit

View File

@ -9,6 +9,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Types of changes can be: Added/Changed/Deprecated/Removed/Fixed/Security
## [21.1.0] - 2025-06-05
### Added
- Allow adding participants to one-to-one chats creating a new conversation
- Handling of event conversations
- Show info about participant (organization, role, timezone, ...) in 1:1 conversation info screen
- Added gallery option in chat attachment menu (access photos and videos with one click without giving permissions)
- Add self-test button for push notifications in diagnosis screen
- Edit checkbox in chat messages
- Team mentions in chat
- Add option to mark a conversation as sensitive
- Allow bluetooth headset to be discovered and used during a call (@gavine99)
### Changed
- Design of participants grid in call
- Improve subline in conversations screen when last message is attachment
- Improve message search
- In search window, show messages at last
- Switch video capture for calls between 4:3 and 16:9 ratio depending on portrait/ landscape mode
### Fixed
- Crashes
- Videos in videocall lost after app comes back to foreground
- Open conversations not being shown in search
- Minor bugs (@MmAaXx500)
Minimum: NC 17 Server, Android 8.0 Oreo
For a full list, please see https://github.com/nextcloud/talk-android/milestone/94?closed=1
## [21.0.1] - 2025-04-15
### Fixed

View File

@ -15,7 +15,7 @@ import com.github.spotbugs.snom.SpotBugsTask
plugins {
id "org.jetbrains.kotlin.plugin.compose" version "2.1.21"
id "org.jetbrains.kotlin.kapt"
id 'com.google.devtools.ksp' version '2.1.21-2.0.1'
id 'com.google.devtools.ksp' version '2.1.21-2.0.2'
}
apply plugin: 'com.android.application'
@ -39,8 +39,8 @@ android {
// mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable)
// xx .xxx .xx .xx
versionCode 220000002
versionName "22.0.0 Alpha 02"
versionCode 220000006
versionName "22.0.0 Alpha 06"
flavorDimensions "default"
renderscriptTargetApi 19
@ -161,7 +161,7 @@ ext {
materialDialogsVersion = "3.3.0"
parcelerVersion = "1.1.13"
prismVersion = "2.0.0"
retrofit2Version = "2.11.0"
retrofit2Version = "2.12.0"
roomVersion = "2.7.1"
workVersion = "2.9.1"
espressoVersion = "3.6.1"
@ -180,20 +180,20 @@ configurations.configureEach {
dependencies {
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0'
spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.9'
spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.10'
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.8")
implementation("androidx.compose.runtime:runtime:1.7.8")
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.datastore:datastore-core:1.1.6'
implementation 'androidx.datastore:datastore-preferences:1.1.6'
implementation 'androidx.datastore:datastore-core:1.1.7'
implementation 'androidx.datastore:datastore-preferences:1.1.7'
implementation 'androidx.test.ext:junit-ktx:1.2.1'
implementation fileTree(include: ['*'], dir: 'libs')
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1"
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation "com.vanniktech:emoji-google:0.21.0"
@ -291,6 +291,7 @@ dependencies {
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:ext-tasklist:$markwonVersion"
implementation "io.noties.markwon:ext-tables:$markwonVersion"
implementation 'com.github.nextcloud-deps:ImagePicker:2.1.0.2'
implementation 'io.github.elye:loaderviewlibrary:3.0.0'
@ -321,14 +322,14 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-test-manifest")
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.17.0'
testImplementation 'org.mockito:mockito-core:5.18.0'
testImplementation 'androidx.arch.core:core-testing:2.2.0'
androidTestImplementation "androidx.test:core:1.6.1"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2"
androidTestImplementation 'androidx.test:core-ktx:1.6.1'
androidTestImplementation 'org.mockito:mockito-android:5.17.0'
androidTestImplementation 'org.mockito:mockito-android:5.18.0'
androidTestImplementation "androidx.work:work-testing:${workVersion}"
// Espresso core
androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", {
@ -346,7 +347,7 @@ dependencies {
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
testImplementation 'org.junit.vintage:junit-vintage-engine:5.12.2'
testImplementation 'org.junit.vintage:junit-vintage-engine:5.13.1'
}
tasks.register('installGitHooks', Copy) {

View File

@ -0,0 +1,725 @@
{
"formatVersion": 1,
"database": {
"version": 15,
"identityHash": "acac3fd21e35762b90f65f213be38ccd",
"entities": [
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT"
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT"
},
{
"fieldPath": "baseUrl",
"columnName": "baseUrl",
"affinity": "TEXT"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT"
},
{
"fieldPath": "pushConfigurationState",
"columnName": "pushConfigurationState",
"affinity": "TEXT"
},
{
"fieldPath": "capabilities",
"columnName": "capabilities",
"affinity": "TEXT"
},
{
"fieldPath": "serverVersion",
"columnName": "serverVersion",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "clientCertificate",
"columnName": "clientCertificate",
"affinity": "TEXT"
},
{
"fieldPath": "externalSignalingServer",
"columnName": "externalSignalingServer",
"affinity": "TEXT"
},
{
"fieldPath": "current",
"columnName": "current",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scheduledForDeletion",
"columnName": "scheduledForDeletion",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "ArbitraryStorage",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
"fields": [
{
"fieldPath": "accountIdentifier",
"columnName": "accountIdentifier",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "storageObject",
"columnName": "object",
"affinity": "TEXT"
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"accountIdentifier",
"key"
]
}
},
{
"tableName": "Conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatarVersion",
"columnName": "avatarVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "callFlag",
"columnName": "callFlag",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callRecording",
"columnName": "callRecording",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callStartTime",
"columnName": "callStartTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canDeleteConversation",
"columnName": "canDeleteConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canLeaveConversation",
"columnName": "canLeaveConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canStartCall",
"columnName": "canStartCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasCall",
"columnName": "hasCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasPassword",
"columnName": "hasPassword",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasCustomAvatar",
"columnName": "isCustomAvatar",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favorite",
"columnName": "isFavorite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastActivity",
"columnName": "lastActivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCommonReadMessage",
"columnName": "lastCommonReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessage",
"columnName": "lastMessage",
"affinity": "TEXT"
},
{
"fieldPath": "lastPing",
"columnName": "lastPing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastReadMessage",
"columnName": "lastReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lobbyState",
"columnName": "lobbyState",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lobbyTimer",
"columnName": "lobbyTimer",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageExpiration",
"columnName": "messageExpiration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationCalls",
"columnName": "notificationCalls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLevel",
"columnName": "notificationLevel",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectType",
"columnName": "objectType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectId",
"columnName": "objectId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "participantType",
"columnName": "participantType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "conversationReadOnlyState",
"columnName": "readOnly",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recordingConsentRequired",
"columnName": "recordingConsent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteServer",
"columnName": "remoteServer",
"affinity": "TEXT"
},
{
"fieldPath": "remoteToken",
"columnName": "remoteToken",
"affinity": "TEXT"
},
{
"fieldPath": "sessionId",
"columnName": "sessionId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT"
},
{
"fieldPath": "statusClearAt",
"columnName": "statusClearAt",
"affinity": "INTEGER"
},
{
"fieldPath": "statusIcon",
"columnName": "statusIcon",
"affinity": "TEXT"
},
{
"fieldPath": "statusMessage",
"columnName": "statusMessage",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unreadMention",
"columnName": "unreadMention",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMentionDirect",
"columnName": "unreadMentionDirect",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMessages",
"columnName": "unreadMessages",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasArchived",
"columnName": "hasArchived",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasSensitive",
"columnName": "hasSensitive",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_Conversations_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
}
],
"foreignKeys": [
{
"table": "User",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ChatMessages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendingFailed` INTEGER NOT NULL, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorDisplayName",
"columnName": "actorDisplayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationTimestamp",
"columnName": "expirationTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "replyable",
"columnName": "isReplyable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTemporary",
"columnName": "isTemporary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastEditActorDisplayName",
"columnName": "lastEditActorDisplayName",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorId",
"columnName": "lastEditActorId",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorType",
"columnName": "lastEditActorType",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditTimestamp",
"columnName": "lastEditTimestamp",
"affinity": "INTEGER"
},
{
"fieldPath": "renderMarkdown",
"columnName": "markdown",
"affinity": "INTEGER"
},
{
"fieldPath": "messageParameters",
"columnName": "messageParameters",
"affinity": "TEXT"
},
{
"fieldPath": "messageType",
"columnName": "messageType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentMessageId",
"columnName": "parent",
"affinity": "INTEGER"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT"
},
{
"fieldPath": "reactionsSelf",
"columnName": "reactionsSelf",
"affinity": "TEXT"
},
{
"fieldPath": "referenceId",
"columnName": "referenceId",
"affinity": "TEXT"
},
{
"fieldPath": "sendingFailed",
"columnName": "sendingFailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "silent",
"columnName": "silent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "systemMessageType",
"columnName": "systemMessage",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_ChatMessages_internalId",
"unique": true,
"columnNames": [
"internalId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
},
{
"name": "index_ChatMessages_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
},
{
"tableName": "ChatBlocks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "oldestMessageId",
"columnName": "oldestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newestMessageId",
"columnName": "newestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasHistory",
"columnName": "hasHistory",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ChatBlocks_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'acac3fd21e35762b90f65f213be38ccd')"
]
}
}

View File

@ -0,0 +1,731 @@
{
"formatVersion": 1,
"database": {
"version": 16,
"identityHash": "bbf526d5c78a99eb951635cc46f4c59f",
"entities": [
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT"
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT"
},
{
"fieldPath": "baseUrl",
"columnName": "baseUrl",
"affinity": "TEXT"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT"
},
{
"fieldPath": "pushConfigurationState",
"columnName": "pushConfigurationState",
"affinity": "TEXT"
},
{
"fieldPath": "capabilities",
"columnName": "capabilities",
"affinity": "TEXT"
},
{
"fieldPath": "serverVersion",
"columnName": "serverVersion",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "clientCertificate",
"columnName": "clientCertificate",
"affinity": "TEXT"
},
{
"fieldPath": "externalSignalingServer",
"columnName": "externalSignalingServer",
"affinity": "TEXT"
},
{
"fieldPath": "current",
"columnName": "current",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scheduledForDeletion",
"columnName": "scheduledForDeletion",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "ArbitraryStorage",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
"fields": [
{
"fieldPath": "accountIdentifier",
"columnName": "accountIdentifier",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "storageObject",
"columnName": "object",
"affinity": "TEXT"
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"accountIdentifier",
"key"
]
}
},
{
"tableName": "Conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatarVersion",
"columnName": "avatarVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "callFlag",
"columnName": "callFlag",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callRecording",
"columnName": "callRecording",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callStartTime",
"columnName": "callStartTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canDeleteConversation",
"columnName": "canDeleteConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canLeaveConversation",
"columnName": "canLeaveConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canStartCall",
"columnName": "canStartCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasCall",
"columnName": "hasCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasPassword",
"columnName": "hasPassword",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasCustomAvatar",
"columnName": "isCustomAvatar",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favorite",
"columnName": "isFavorite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastActivity",
"columnName": "lastActivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCommonReadMessage",
"columnName": "lastCommonReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessage",
"columnName": "lastMessage",
"affinity": "TEXT"
},
{
"fieldPath": "lastPing",
"columnName": "lastPing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastReadMessage",
"columnName": "lastReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lobbyState",
"columnName": "lobbyState",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lobbyTimer",
"columnName": "lobbyTimer",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageExpiration",
"columnName": "messageExpiration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationCalls",
"columnName": "notificationCalls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLevel",
"columnName": "notificationLevel",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectType",
"columnName": "objectType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectId",
"columnName": "objectId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "participantType",
"columnName": "participantType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "conversationReadOnlyState",
"columnName": "readOnly",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recordingConsentRequired",
"columnName": "recordingConsent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteServer",
"columnName": "remoteServer",
"affinity": "TEXT"
},
{
"fieldPath": "remoteToken",
"columnName": "remoteToken",
"affinity": "TEXT"
},
{
"fieldPath": "sessionId",
"columnName": "sessionId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT"
},
{
"fieldPath": "statusClearAt",
"columnName": "statusClearAt",
"affinity": "INTEGER"
},
{
"fieldPath": "statusIcon",
"columnName": "statusIcon",
"affinity": "TEXT"
},
{
"fieldPath": "statusMessage",
"columnName": "statusMessage",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unreadMention",
"columnName": "unreadMention",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMentionDirect",
"columnName": "unreadMentionDirect",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMessages",
"columnName": "unreadMessages",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasArchived",
"columnName": "hasArchived",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasSensitive",
"columnName": "hasSensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasImportant",
"columnName": "hasImportant",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_Conversations_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
}
],
"foreignKeys": [
{
"table": "User",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ChatMessages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendingFailed` INTEGER NOT NULL, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorDisplayName",
"columnName": "actorDisplayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationTimestamp",
"columnName": "expirationTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "replyable",
"columnName": "isReplyable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTemporary",
"columnName": "isTemporary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastEditActorDisplayName",
"columnName": "lastEditActorDisplayName",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorId",
"columnName": "lastEditActorId",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorType",
"columnName": "lastEditActorType",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditTimestamp",
"columnName": "lastEditTimestamp",
"affinity": "INTEGER"
},
{
"fieldPath": "renderMarkdown",
"columnName": "markdown",
"affinity": "INTEGER"
},
{
"fieldPath": "messageParameters",
"columnName": "messageParameters",
"affinity": "TEXT"
},
{
"fieldPath": "messageType",
"columnName": "messageType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentMessageId",
"columnName": "parent",
"affinity": "INTEGER"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT"
},
{
"fieldPath": "reactionsSelf",
"columnName": "reactionsSelf",
"affinity": "TEXT"
},
{
"fieldPath": "referenceId",
"columnName": "referenceId",
"affinity": "TEXT"
},
{
"fieldPath": "sendingFailed",
"columnName": "sendingFailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "silent",
"columnName": "silent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "systemMessageType",
"columnName": "systemMessage",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_ChatMessages_internalId",
"unique": true,
"columnNames": [
"internalId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
},
{
"name": "index_ChatMessages_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
},
{
"tableName": "ChatBlocks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "oldestMessageId",
"columnName": "oldestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newestMessageId",
"columnName": "newestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasHistory",
"columnName": "hasHistory",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ChatBlocks_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bbf526d5c78a99eb951635cc46f4c59f')"
]
}
}

View File

@ -0,0 +1,730 @@
{
"formatVersion": 1,
"database": {
"version": 17,
"identityHash": "5bc4247e179307faa995552da5d34324",
"entities": [
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT"
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT"
},
{
"fieldPath": "baseUrl",
"columnName": "baseUrl",
"affinity": "TEXT"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT"
},
{
"fieldPath": "pushConfigurationState",
"columnName": "pushConfigurationState",
"affinity": "TEXT"
},
{
"fieldPath": "capabilities",
"columnName": "capabilities",
"affinity": "TEXT"
},
{
"fieldPath": "serverVersion",
"columnName": "serverVersion",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "clientCertificate",
"columnName": "clientCertificate",
"affinity": "TEXT"
},
{
"fieldPath": "externalSignalingServer",
"columnName": "externalSignalingServer",
"affinity": "TEXT"
},
{
"fieldPath": "current",
"columnName": "current",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scheduledForDeletion",
"columnName": "scheduledForDeletion",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "ArbitraryStorage",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
"fields": [
{
"fieldPath": "accountIdentifier",
"columnName": "accountIdentifier",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "storageObject",
"columnName": "object",
"affinity": "TEXT"
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"accountIdentifier",
"key"
]
}
},
{
"tableName": "Conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, `hasSensitive` INTEGER NOT NULL, `hasImportant` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatarVersion",
"columnName": "avatarVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "callFlag",
"columnName": "callFlag",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callRecording",
"columnName": "callRecording",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callStartTime",
"columnName": "callStartTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canDeleteConversation",
"columnName": "canDeleteConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canLeaveConversation",
"columnName": "canLeaveConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canStartCall",
"columnName": "canStartCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasCall",
"columnName": "hasCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasPassword",
"columnName": "hasPassword",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasCustomAvatar",
"columnName": "isCustomAvatar",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favorite",
"columnName": "isFavorite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastActivity",
"columnName": "lastActivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCommonReadMessage",
"columnName": "lastCommonReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessage",
"columnName": "lastMessage",
"affinity": "TEXT"
},
{
"fieldPath": "lastPing",
"columnName": "lastPing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastReadMessage",
"columnName": "lastReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lobbyState",
"columnName": "lobbyState",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lobbyTimer",
"columnName": "lobbyTimer",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageExpiration",
"columnName": "messageExpiration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationCalls",
"columnName": "notificationCalls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLevel",
"columnName": "notificationLevel",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectType",
"columnName": "objectType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectId",
"columnName": "objectId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "participantType",
"columnName": "participantType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "conversationReadOnlyState",
"columnName": "readOnly",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recordingConsentRequired",
"columnName": "recordingConsent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteServer",
"columnName": "remoteServer",
"affinity": "TEXT"
},
{
"fieldPath": "remoteToken",
"columnName": "remoteToken",
"affinity": "TEXT"
},
{
"fieldPath": "sessionId",
"columnName": "sessionId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT"
},
{
"fieldPath": "statusClearAt",
"columnName": "statusClearAt",
"affinity": "INTEGER"
},
{
"fieldPath": "statusIcon",
"columnName": "statusIcon",
"affinity": "TEXT"
},
{
"fieldPath": "statusMessage",
"columnName": "statusMessage",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unreadMention",
"columnName": "unreadMention",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMentionDirect",
"columnName": "unreadMentionDirect",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMessages",
"columnName": "unreadMessages",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasArchived",
"columnName": "hasArchived",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasSensitive",
"columnName": "hasSensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasImportant",
"columnName": "hasImportant",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_Conversations_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
}
],
"foreignKeys": [
{
"table": "User",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ChatMessages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorDisplayName",
"columnName": "actorDisplayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationTimestamp",
"columnName": "expirationTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "replyable",
"columnName": "isReplyable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTemporary",
"columnName": "isTemporary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastEditActorDisplayName",
"columnName": "lastEditActorDisplayName",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorId",
"columnName": "lastEditActorId",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorType",
"columnName": "lastEditActorType",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditTimestamp",
"columnName": "lastEditTimestamp",
"affinity": "INTEGER"
},
{
"fieldPath": "renderMarkdown",
"columnName": "markdown",
"affinity": "INTEGER"
},
{
"fieldPath": "messageParameters",
"columnName": "messageParameters",
"affinity": "TEXT"
},
{
"fieldPath": "messageType",
"columnName": "messageType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentMessageId",
"columnName": "parent",
"affinity": "INTEGER"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT"
},
{
"fieldPath": "reactionsSelf",
"columnName": "reactionsSelf",
"affinity": "TEXT"
},
{
"fieldPath": "referenceId",
"columnName": "referenceId",
"affinity": "TEXT"
},
{
"fieldPath": "sendStatus",
"columnName": "sendStatus",
"affinity": "TEXT"
},
{
"fieldPath": "silent",
"columnName": "silent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "systemMessageType",
"columnName": "systemMessage",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_ChatMessages_internalId",
"unique": true,
"columnNames": [
"internalId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
},
{
"name": "index_ChatMessages_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
},
{
"tableName": "ChatBlocks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "oldestMessageId",
"columnName": "oldestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newestMessageId",
"columnName": "newestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasHistory",
"columnName": "hasHistory",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ChatBlocks_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bc4247e179307faa995552da5d34324')"
]
}
}

View File

@ -286,6 +286,7 @@ class WebViewLoginActivity : BaseActivity() {
}
}
@SuppressLint("DiscouragedPrivateApi")
@Suppress("Detekt.TooGenericExceptionCaught")
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
try {

View File

@ -376,6 +376,8 @@ class CallActivity : CallBaseActivity() {
Log.d(TAG, "onCreate")
super.onCreate(savedInstanceState)
sharedApplication!!.componentApplication.inject(this)
rootEglBase = EglBase.create()
binding = CallActivityBinding.inflate(layoutInflater)
setContentView(binding!!.root)
hideNavigationIfNoPipAvailable()
@ -765,7 +767,6 @@ class CallActivity : CallBaseActivity() {
}
private fun basicInitialization() {
rootEglBase = EglBase.create()
createCameraEnumerator()
// Create a new PeerConnectionFactory instance.
@ -947,8 +948,7 @@ class CallActivity : CallBaseActivity() {
ParticipantGrid(
participantUiStates = participantUiStates,
eglBase = rootEglBase!!,
isVoiceOnlyCall = isVoiceOnlyCall,
isInPipMode = isInPipMode
isVoiceOnlyCall = isVoiceOnlyCall
) {
animateCallControls(true, 0)
}

View File

@ -19,6 +19,7 @@ import android.text.TextUtils
import android.text.format.DateUtils
import android.text.style.ImageSpan
import android.view.View
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import com.nextcloud.talk.R
@ -155,6 +156,30 @@ class ConversationItem(
} else {
holder.binding.userStatusImage.visibility = View.GONE
}
val dialogNameParams = holder.binding.dialogName.layoutParams as RelativeLayout.LayoutParams
val unreadBubbleParams = holder.binding.dialogUnreadBubble.layoutParams as RelativeLayout.LayoutParams
val relativeLayoutParams = holder.binding.relativeLayout.layoutParams as RelativeLayout.LayoutParams
if (model.hasSensitive == true) {
dialogNameParams.addRule(RelativeLayout.CENTER_VERTICAL)
relativeLayoutParams.addRule(RelativeLayout.ALIGN_TOP, R.id.dialogAvatarFrameLayout)
dialogNameParams.marginEnd =
context.resources.getDimensionPixelSize(R.dimen.standard_double_padding)
unreadBubbleParams.topMargin =
context.resources.getDimensionPixelSize(R.dimen.double_margin_between_elements)
unreadBubbleParams.addRule(RelativeLayout.CENTER_VERTICAL)
} else {
dialogNameParams.removeRule(RelativeLayout.CENTER_VERTICAL)
relativeLayoutParams.removeRule(RelativeLayout.ALIGN_TOP)
dialogNameParams.marginEnd = 0
unreadBubbleParams.topMargin = 0
unreadBubbleParams.removeRule(RelativeLayout.CENTER_VERTICAL)
}
holder.binding.relativeLayout.layoutParams = relativeLayoutParams
holder.binding.dialogUnreadBubble.layoutParams = unreadBubbleParams
holder.binding.dialogName.layoutParams = dialogNameParams
setLastMessage(holder, appContext)
showAvatar(holder)
}
@ -406,9 +431,9 @@ class ConversationItem(
)
return lastMessage
} else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == chatMessage?.getCalculateMessageType()) {
var attachmentName = chatMessage.message
var attachmentName = chatMessage.text
if (attachmentName == "{file}") {
attachmentName = chatMessage.messageParameters?.get("file")?.get("name")
attachmentName = chatMessage.messageParameters?.get("file")?.get("name") ?: ""
}
val author = authorName(chatMessage)

View File

@ -18,7 +18,9 @@ import androidx.core.content.ContextCompat
import androidx.core.text.toSpanned
import autodagger.AutoInjector
import coil.load
import com.google.android.flexbox.FlexboxLayout
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
@ -103,10 +105,33 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
true,
viewThemeUtils
)
val spansFromString: Array<Any> = processedMessageText!!.getSpans(
0,
processedMessageText.length,
Any::class.java
)
if (spansFromString.isNotEmpty()) {
binding.bubble.layoutParams.apply {
width = FlexboxLayout.LayoutParams.MATCH_PARENT
}
binding.messageText.layoutParams.apply {
width = FlexboxLayout.LayoutParams.MATCH_PARENT
}
} else {
binding.bubble.layoutParams.apply {
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
}
binding.messageText.layoutParams.apply {
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
}
}
processedMessageText = messageUtils.processMessageParameters(
binding.messageText.context,
viewThemeUtils,
processedMessageText!!,
processedMessageText,
message,
itemView
)
@ -133,7 +158,7 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
binding.messageEditIndicator.visibility = View.GONE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
}
binding.messageTime.setTextColor(ContextCompat.getColor(context, R.color.no_emphasis_text))
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
// parent message handling
if (!message.isDeleted && message.parentMessageId != null) {
processParentMessage(message)

View File

@ -29,6 +29,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.ChatMessageRepository
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
@ -105,7 +106,6 @@ class OutcomingTextMessageViewHolder(itemView: View) :
if (!hasCheckboxes) {
realView.isSelected = false
layoutParams.isWrapBefore = false
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
binding.messageText.visibility = View.VISIBLE
binding.checkboxContainer.visibility = View.GONE
@ -116,10 +116,33 @@ class OutcomingTextMessageViewHolder(itemView: View) :
false,
viewThemeUtils
)
val spansFromString: Array<Any> = processedMessageText!!.getSpans(
0,
processedMessageText.length,
Any::class.java
)
if (spansFromString.isNotEmpty()) {
binding.bubble.layoutParams.apply {
width = FlexboxLayout.LayoutParams.MATCH_PARENT
}
binding.messageText.layoutParams.apply {
width = FlexboxLayout.LayoutParams.MATCH_PARENT
}
} else {
binding.bubble.layoutParams.apply {
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
}
binding.messageText.layoutParams.apply {
width = FlexboxLayout.LayoutParams.WRAP_CONTENT
}
}
processedMessageText = messageUtils.processMessageParameters(
binding.messageText.context,
viewThemeUtils,
processedMessageText!!,
processedMessageText,
message,
itemView
)
@ -149,7 +172,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
binding.messageEditIndicator.visibility = View.GONE
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
}
binding.messageTime.setTextColor(ContextCompat.getColor(context, R.color.no_emphasis_text))
viewThemeUtils.platform.colorTextView(binding.messageTime, ColorRole.ON_SURFACE_VARIANT)
setBubbleOnChatMessage(message)
// parent message handling
if (!message.isDeleted && message.parentMessageId != null) {
@ -162,7 +185,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
binding.checkMark.visibility = View.INVISIBLE
binding.sendingProgress.visibility = View.GONE
if (message.sendingFailed) {
if (message.sendStatus == SendStatus.FAILED) {
updateStatus(R.drawable.baseline_error_outline_24, context.resources?.getString(R.string.nc_message_failed))
} else if (message.isTemporary) {
updateStatus(R.drawable.baseline_schedule_24, context.resources?.getString(R.string.nc_message_sending))

View File

@ -17,6 +17,7 @@ import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.AddParticipantOverall
import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.participants.TalkBanOverall
import com.nextcloud.talk.models.json.profile.ProfileOverall
import com.nextcloud.talk.models.json.testNotification.TestNotificationOverall
import com.nextcloud.talk.models.json.userAbsence.UserAbsenceOverall
import okhttp3.MultipartBody
@ -178,12 +179,36 @@ interface NcApiCoroutines {
@Url url: String
): GenericOverall
@POST
suspend fun markConversationAsImportant(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@DELETE
suspend fun markConversationAsUnimportant(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@DELETE
suspend fun removeConversationFromFavorites(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@POST
suspend fun markConversationAsSensitive(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@DELETE
suspend fun markConversationAsInsensitive(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@FormUrlEncoded
@POST
suspend fun notificationCalls(
@ -254,4 +279,10 @@ interface NcApiCoroutines {
@GET
suspend fun getNoteToSelfRoom(@Header("Authorization") authorization: String, @Url url: String): RoomOverall
@GET
suspend fun getProfile(@Header("Authorization") authorization: String, @Url url: String): ProfileOverall
@DELETE
suspend fun unbindRoom(@Header("Authorization") authorization: String, @Url url: String): GenericOverall
}

View File

@ -33,7 +33,6 @@ import coil.decode.SvgDecoder
import coil.memory.MemoryCache
import coil.util.DebugLogger
import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.filebrowser.webdav.DavUtils
import com.nextcloud.talk.dagger.modules.BusModule
import com.nextcloud.talk.dagger.modules.ContextModule
import com.nextcloud.talk.dagger.modules.DaosModule
@ -43,6 +42,7 @@ import com.nextcloud.talk.dagger.modules.RepositoryModule
import com.nextcloud.talk.dagger.modules.RestModule
import com.nextcloud.talk.dagger.modules.UtilsModule
import com.nextcloud.talk.dagger.modules.ViewModelModule
import com.nextcloud.talk.filebrowser.webdav.DavUtils
import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.jobs.CapabilitiesWorker
import com.nextcloud.talk.jobs.SignalingSettingsWorker

View File

@ -6,6 +6,7 @@
*/
package com.nextcloud.talk.bottomsheet.items
import android.annotation.SuppressLint
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
@ -65,6 +66,7 @@ internal class ListIconDialogAdapter<IT : ListItemWithImage>(
}
}
@SuppressLint("RestrictedApi")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemViewHolder {
val listItemView: View = parent.inflate(dialog.windowContext, R.layout.menu_item_sheet)
val viewHolder = ListItemViewHolder(

View File

@ -12,11 +12,11 @@ package com.nextcloud.talk.call.components
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
@ -36,7 +36,6 @@ fun ParticipantGrid(
eglBase: EglBase?,
participantUiStates: List<ParticipantUiState>,
isVoiceOnlyCall: Boolean,
isInPipMode: Boolean,
onClick: () -> Unit
) {
val configuration = LocalConfiguration.current
@ -44,63 +43,59 @@ fun ParticipantGrid(
val minItemHeight = 100.dp
val columns =
if (isPortrait) {
when (participantUiStates.size) {
1, 2, 3 -> 1
else -> 2
}
} else {
when (participantUiStates.size) {
1 -> 1
2, 4 -> 2
else -> 3
}
if (participantUiStates.isEmpty()) return
val columns = if (isPortrait) {
when (participantUiStates.size) {
1, 2, 3 -> 1
else -> 2
}
val rows = ceil(participantUiStates.size / columns.toFloat()).toInt()
val heightForNonGridComponents = if (isVoiceOnlyCall && !isInPipMode) {
// this is a workaround for now. It should ~summarize the height of callInfosLinearLayout and callControls
// Once everything is migrated to jetpack, this workaround should be obsolete or solved in a better way
240.dp
} else {
0.dp
}
when (participantUiStates.size) {
1 -> 1
2, 4 -> 2
else -> 3
}
}.coerceAtLeast(1) // Prevent 0
val rows = ceil(participantUiStates.size / columns.toFloat()).toInt().coerceAtLeast(1)
val gridHeight = LocalConfiguration.current.screenHeightDp.dp - heightForNonGridComponents
val itemSpacing = 8.dp
val edgePadding = 8.dp
val totalVerticalSpacing = itemSpacing * (rows - 1)
val totalVerticalPadding = edgePadding * 2
val availableHeight = gridHeight - totalVerticalSpacing - totalVerticalPadding
val rawItemHeight = availableHeight / rows
val itemHeight = maxOf(rawItemHeight, minItemHeight)
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
modifier = Modifier
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.padding(horizontal = edgePadding)
.clickable { onClick() },
verticalArrangement = Arrangement.spacedBy(itemSpacing),
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
contentPadding = PaddingValues(vertical = edgePadding)
.clickable { onClick() }
) {
items(
participantUiStates,
key = { it.sessionKey }
) { participant ->
ParticipantTile(
participantUiState = participant,
modifier = Modifier
.height(itemHeight)
.fillMaxWidth(),
eglBase = eglBase,
isVoiceOnlyCall = isVoiceOnlyCall
)
val availableHeight = maxHeight
val gridAvailableHeight = availableHeight - totalVerticalSpacing - totalVerticalPadding
val rawItemHeight = gridAvailableHeight / rows
val itemHeight = maxOf(rawItemHeight, minItemHeight)
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(itemSpacing),
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
contentPadding = PaddingValues(vertical = edgePadding, horizontal = edgePadding)
) {
items(
participantUiStates,
key = { it.sessionKey }
) { participant ->
ParticipantTile(
participantUiState = participant,
modifier = Modifier
.height(itemHeight)
.fillMaxWidth(),
eglBase = eglBase,
isVoiceOnlyCall = isVoiceOnlyCall
)
}
}
}
}
@ -111,8 +106,7 @@ fun ParticipantGridPreview() {
ParticipantGrid(
participantUiStates = getTestParticipants(1),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -122,8 +116,7 @@ fun TwoParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(2),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -133,8 +126,7 @@ fun ThreeParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(3),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -144,8 +136,7 @@ fun FourParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(4),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -155,8 +146,7 @@ fun FiveParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(5),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -166,8 +156,7 @@ fun SevenParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(7),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -177,8 +166,7 @@ fun FiftyParticipants() {
ParticipantGrid(
participantUiStates = getTestParticipants(50),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -192,8 +180,7 @@ fun OneParticipantLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(1),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -207,8 +194,7 @@ fun TwoParticipantsLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(2),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -222,8 +208,7 @@ fun ThreeParticipantsLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(3),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -237,8 +222,7 @@ fun FourParticipantsLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(4),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -252,8 +236,7 @@ fun SevenParticipantsLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(7),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}
@ -267,8 +250,7 @@ fun FiftyParticipantsLandscape() {
ParticipantGrid(
participantUiStates = getTestParticipants(50),
eglBase = null,
isVoiceOnlyCall = false,
isInPipMode = false
isVoiceOnlyCall = false
) {}
}

View File

@ -16,7 +16,6 @@ package com.nextcloud.talk.chat
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@ -53,6 +52,7 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper
import androidx.cardview.widget.CardView
@ -85,6 +85,7 @@ import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.target.Target
import coil.transform.CircleCropTransformation
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.color.ColorUtil
@ -124,6 +125,7 @@ import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
import com.nextcloud.talk.conversationinfo.ConversationInfoActivity
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.conversationlist.ConversationsListActivity
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
@ -142,6 +144,7 @@ import com.nextcloud.talk.models.domain.ConversationModel
import com.nextcloud.talk.models.json.capabilities.SpreedCapability
import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.models.json.conversations.ConversationEnums
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
@ -153,6 +156,7 @@ import com.nextcloud.talk.ui.PlaybackSpeed
import com.nextcloud.talk.ui.PlaybackSpeedControl
import com.nextcloud.talk.ui.StatusDrawable
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
import com.nextcloud.talk.ui.dialog.ContextChatCompose
import com.nextcloud.talk.ui.dialog.DateTimeCompose
import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
@ -164,6 +168,10 @@ import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.AudioUtils
import com.nextcloud.talk.utils.CapabilitiesUtil
import com.nextcloud.talk.utils.CapabilitiesUtil.hasSpreedFeatureCapability
import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfEventRooms
import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfInstantMeetingRoom
import com.nextcloud.talk.utils.CapabilitiesUtil.retentionOfSIPRoom
import com.nextcloud.talk.utils.ContactUtils
import com.nextcloud.talk.utils.ConversationUtils
import com.nextcloud.talk.utils.DateConstants
@ -208,6 +216,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@ -224,10 +233,8 @@ import java.util.Locale
import java.util.concurrent.ExecutionException
import javax.inject.Inject
import kotlin.math.roundToInt
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.models.json.participants.Participant
@Suppress("TooManyFunctions")
@AutoInjector(NextcloudTalkApplication::class)
class ChatActivity :
BaseActivity(),
@ -268,6 +275,8 @@ class ChatActivity :
lateinit var conversationInfoViewModel: ConversationInfoViewModel
lateinit var messageInputViewModel: MessageInputViewModel
private var chatMenu: Menu? = null
private val startSelectContactForResult = registerForActivityResult(
ActivityResultContracts
.StartActivityForResult()
@ -296,10 +305,34 @@ class ChatActivity :
private val startMessageSearchForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
executeIfResultOk(it) { intent ->
onMessageSearchResult(intent)
runBlocking {
val id = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
id?.let {
startContextChatWindowForMessage(id)
}
}
}
}
private fun startContextChatWindowForMessage(id: String?) {
binding.genericComposeView.apply {
val shouldDismiss = mutableStateOf(false)
setContent {
val bundle = bundleOf()
bundle.putString(BundleKeys.KEY_CREDENTIALS, credentials!!)
bundle.putString(BundleKeys.KEY_BASE_URL, conversationUser!!.baseUrl)
bundle.putString(KEY_ROOM_TOKEN, roomToken)
bundle.putString(BundleKeys.KEY_MESSAGE_ID, id)
bundle.putString(
KEY_CONVERSATION_NAME,
currentConversation!!.displayName
)
ContextChatCompose(bundle).GetDialogView(shouldDismiss, context)
}
}
Log.d(TAG, "Should open something else")
}
private val startPickCameraIntentForResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
@ -329,6 +362,7 @@ class ChatActivity :
var startCallFromRoomSwitch: Boolean = false
var voiceOnly: Boolean = true
var focusInput: Boolean = false
private lateinit var path: String
var myFirstMessage: CharSequence? = null
@ -512,6 +546,8 @@ class ChatActivity :
startCallFromRoomSwitch = extras?.getBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, false) == true
voiceOnly = extras?.getBoolean(KEY_CALL_VOICE_ONLY, false) == true
focusInput = extras?.getBoolean(BundleKeys.KEY_FOCUS_INPUT) == true
}
override fun onStart() {
@ -603,12 +639,17 @@ class ChatActivity :
supportFragmentManager.commit {
setReorderingAllowed(true) // optimizes out redundant replace operations
replace(R.id.fragment_container_activity_chat, messageInputFragment)
runOnCommit {
if (focusInput) {
messageInputFragment.binding.fragmentMessageInputView.requestFocus()
}
}
}
joinRoomWithPassword()
if (conversationUser?.userId != "?" &&
CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)
hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)
) {
binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() }
}
@ -632,6 +673,59 @@ class ChatActivity :
}
}
if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT &&
hasSpreedFeatureCapability(
conversationUser?.capabilities!!.spreedCapability!!,
SpreedFeatures.UNBIND_CONVERSATION
)
) {
val eventEndTimeStamp =
currentConversation?.objectId
?.split("#")
?.getOrNull(1)
?.toLongOrNull()
val currentTimeStamp = (System.currentTimeMillis() / 1000).toLong()
val retentionPeriod = retentionOfEventRooms(spreedCapabilities)
val isPastEvent = eventEndTimeStamp?.let { it < currentTimeStamp }
if (isPastEvent == true && retentionPeriod != 0) {
showConversationDeletionWarning(retentionPeriod)
}
}
if (currentConversation?.objectType == ConversationEnums.ObjectType.PHONE_TEMPORARY &&
hasSpreedFeatureCapability(
conversationUser?.capabilities!!.spreedCapability!!,
SpreedFeatures.UNBIND_CONVERSATION
)
) {
val retentionPeriod = retentionOfSIPRoom(spreedCapabilities)
val systemMessage = currentConversation?.lastMessage?.systemMessageType
if (retentionPeriod != 0 && (
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED ||
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE
)
) {
showConversationDeletionWarning(retentionPeriod)
}
}
if (currentConversation?.objectType == ConversationEnums.ObjectType.INSTANT_MEETING &&
hasSpreedFeatureCapability(
conversationUser?.capabilities!!.spreedCapability!!,
SpreedFeatures.UNBIND_CONVERSATION
)
) {
val retentionPeriod = retentionOfInstantMeetingRoom(spreedCapabilities)
val systemMessage = currentConversation?.lastMessage?.systemMessageType
if (retentionPeriod != 0 && (
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED ||
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE
)
) {
showConversationDeletionWarning(retentionPeriod)
}
}
updateRoomTimerHandler(MILLIS_250)
val urlForChatting =
@ -958,8 +1052,10 @@ class ChatActivity :
val newString = state.messageEdited.ocs?.data?.parentMessage?.message ?: "(null)"
val id = state.messageEdited.ocs?.data?.parentMessage?.id.toString()
val index = adapter?.getMessagePositionById(id) ?: 0
val message = adapter?.items?.get(index)?.item as ChatMessage
setMessageAsEdited(message, newString)
val item = adapter?.items?.get(index)?.item
item?.let {
setMessageAsEdited(item as ChatMessage, newString)
}
}
is MessageInputViewModel.EditMessageErrorState -> {
@ -1001,6 +1097,29 @@ class ChatActivity :
binding.voiceRecordingLock.y -= y
}
chatViewModel.unbindRoomResult.observe(this) { uiState ->
when (uiState) {
is ChatViewModel.UnbindRoomUiState.Success -> {
binding.conversationDeleteNotice.visibility = View.GONE
Snackbar.make(
binding.root,
context.getString(R.string.nc_room_retention),
Snackbar.LENGTH_LONG
).show()
chatMenu?.removeItem(R.id.conversation_event)
}
is ChatViewModel.UnbindRoomUiState.Error -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_common_error_sorry),
Snackbar.LENGTH_LONG
).show()
}
else -> { }
}
}
chatViewModel.outOfOfficeViewState.observe(this) { uiState ->
when (uiState) {
is ChatViewModel.OutOfOfficeUIState.Error -> {
@ -1116,6 +1235,69 @@ class ChatActivity :
}
}
fun showConversationDeletionWarning(retentionPeriod: Int) {
binding.conversationDeleteNotice.visibility = View.VISIBLE
binding.conversationDeleteNotice.apply {
isClickable = false
isFocusable = false
bringToFront()
}
val deleteNoticeText = binding.conversationDeleteNotice.findViewById<TextView>(R.id.deletion_message)
viewThemeUtils.material.themeCardView(binding.conversationDeleteNotice)
deleteNoticeText.text = resources.getQuantityString(
R.plurals.nc_conversation_auto_delete_info,
retentionPeriod,
retentionPeriod
)
viewThemeUtils.material.colorMaterialButtonPrimaryTonal(
binding.conversationDeleteNotice
.findViewById<MaterialButton>(R.id.keep_button)
)
if (ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!)) {
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.delete_now_button).visibility =
View.VISIBLE
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.keep_button).visibility = View.VISIBLE
} else {
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.delete_now_button).visibility =
View.GONE
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.keep_button).visibility = View.GONE
}
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.delete_now_button).setOnClickListener {
deleteConversationDialog(it.context)
}
binding.conversationDeleteNotice.findViewById<MaterialButton>(R.id.keep_button).setOnClickListener {
chatViewModel.unbindRoom(credentials!!, conversationUser?.baseUrl!!, currentConversation?.token!!)
}
}
fun deleteConversationDialog(context: Context) {
val dialogBuilder = MaterialAlertDialogBuilder(context)
.setIcon(
viewThemeUtils.dialog
.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)
)
.setTitle(R.string.nc_delete_call)
.setMessage(R.string.nc_delete_conversation_more)
.setPositiveButton(R.string.nc_delete) { _, _ ->
currentConversation?.let { conversation ->
deleteConversation(conversation)
}
}
.setNegativeButton(R.string.nc_cancel) { _, _ ->
}
viewThemeUtils.dialog
.colorMaterialAlertDialogBackground(context, dialogBuilder)
val dialog = dialogBuilder.show()
viewThemeUtils.platform.colorTextButtons(
dialog.getButton(AlertDialog.BUTTON_POSITIVE),
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
)
}
@Suppress("Detekt.TooGenericExceptionCaught")
override fun onResume() {
super.onResume()
@ -1872,7 +2054,7 @@ class ChatActivity :
private fun shouldShowLobby(): Boolean {
if (currentConversation != null) {
return CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
return hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
currentConversation?.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
!ConversationUtils.canModerate(currentConversation!!, spreedCapabilities) &&
!participantPermissions.canIgnoreLobby()
@ -2127,15 +2309,8 @@ class ChatActivity :
}
}
private fun onMessageSearchResult(intent: Intent?) {
val messageId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
messageId?.let { id ->
scrollToAndCenterMessageWithId(id)
}
}
private fun executeIfResultOk(result: ActivityResult, onResult: (intent: Intent?) -> Unit) {
if (result.resultCode == Activity.RESULT_OK) {
if (result.resultCode == RESULT_OK) {
onResult(result.data)
} else {
Log.e(TAG, "resultCode for received intent was != ok")
@ -2164,8 +2339,8 @@ class ChatActivity :
} else {
Log.d(
TAG,
"message $messageId that should be scrolled to was not found " +
"(scrollToAndCenterMessageWithId)"
"message $messageId that should be scrolled " +
"to was not found (scrollToAndCenterMessageWithId)"
)
}
}
@ -2629,7 +2804,7 @@ class ChatActivity :
}
if (this::spreedCapabilities.isInitialized) {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
deleteExpiredMessages()
}
} else {
@ -2876,6 +3051,7 @@ class ChatActivity :
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.menu_conversation, menu)
chatMenu = menu
if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) {
eventConversationMenuItem = menu.findItem(R.id.conversation_event)
@ -2889,7 +3065,6 @@ class ChatActivity :
loadAvatarForStatusBar()
setActionBarTitle()
}
return true
}
@ -2897,7 +3072,7 @@ class ChatActivity :
super.onPrepareOptionsMenu(menu)
if (this::spreedCapabilities.isInitialized) {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.READ_ONLY_ROOMS)) {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.READ_ONLY_ROOMS)) {
checkShowCallButtons()
}
@ -2918,7 +3093,7 @@ class ChatActivity :
}.collect()
}
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL)) {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL)) {
Handler().post {
findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener {
showCallButtonMenu(true)
@ -2977,10 +3152,10 @@ class ChatActivity :
else -> super.onOptionsItemSelected(item)
}
@SuppressLint("InflateParams")
private fun showPopupWindow(anchorView: View) {
val popupView = layoutInflater.inflate(R.layout.item_event_schedule, null)
val titleTextView = popupView.findViewById<TextView>(R.id.event_scheduled)
val subtitleTextView = popupView.findViewById<TextView>(R.id.meetingTime)
val popupWindow = PopupWindow(
@ -3009,28 +3184,7 @@ class ChatActivity :
deleteConversation.visibility = View.VISIBLE
deleteConversation.setOnClickListener {
val dialogBuilder = MaterialAlertDialogBuilder(it.context)
.setIcon(
viewThemeUtils.dialog
.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)
)
.setTitle(R.string.nc_delete_call)
.setMessage(R.string.nc_delete_conversation_more)
.setPositiveButton(R.string.nc_delete) { _, _ ->
currentConversation?.let { conversation ->
deleteConversation(conversation)
}
}
.setNegativeButton(R.string.nc_cancel) { _, _ ->
}
viewThemeUtils.dialog
.colorMaterialAlertDialogBackground(it.context, dialogBuilder)
val dialog = dialogBuilder.show()
viewThemeUtils.platform.colorTextButtons(
dialog.getButton(AlertDialog.BUTTON_POSITIVE),
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
)
deleteConversationDialog(it.context)
popupWindow.dismiss()
}
} else {
@ -3159,9 +3313,11 @@ class ChatActivity :
context.resources.getString(R.string.nc_tomorrow_meeting),
startDateTime.format(DateTimeFormatter.ofPattern("HH:mm"))
)
else -> startDateTime.format(DateTimeFormatter.ofPattern("MMM d, yyyy, HH:mm"))
}
}
currentTime.isAfter(endDateTime) -> context.resources.getString(R.string.nc_meeting_ended)
else -> context.resources.getString(R.string.nc_ongoing_meeting)
}
@ -3448,7 +3604,7 @@ class ChatActivity :
fun copyMessage(message: IMessage?) {
val clipboardManager =
getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText(
resources?.getString(R.string.nc_app_product_name),
message?.text
@ -3624,6 +3780,7 @@ class ChatActivity :
)
showSnackBar(roomToken)
}
else -> {}
}
}
@ -3644,6 +3801,7 @@ class ChatActivity :
chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(chatIntent)
}
fun openInFilesApp(message: ChatMessage) {
val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]
val link = message.selectedIndividualHashMap!!["link"]
@ -3760,7 +3918,7 @@ class ChatActivity :
val isOlderThanSixHours = message
.createdAt
.before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE))
val hasDeleteMessagesUnlimitedCapability = CapabilitiesUtil.hasSpreedFeatureCapability(
val hasDeleteMessagesUnlimitedCapability = hasSpreedFeatureCapability(
spreedCapabilities,
SpreedFeatures.DELETE_MESSAGES_UNLIMITED
)
@ -3770,7 +3928,7 @@ class ChatActivity :
!hasDeleteMessagesUnlimitedCapability && isOlderThanSixHours -> false
message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false
message.isDeleted -> false
!CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES) -> false
!hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES) -> false
!participantPermissions.hasChatPermission() -> false
hasDeleteMessagesUnlimitedCapability -> true
else -> true
@ -3927,9 +4085,7 @@ class ChatActivity :
}
if (!foundMessage) {
Log.d(TAG, "quoted message with id " + parentMessage.id + " was not found in adapter")
// TODO: show better info
// TODO: improve handling how this can be avoided. E.g. loading chat until message is reached...
Snackbar.make(binding.root, "Message was not found", Snackbar.LENGTH_LONG).show()
startContextChatWindowForMessage(parentMessage.id)
}
}

View File

@ -200,7 +200,7 @@ class MessageInputFragment : Fragment() {
val connectionGained = (!wasOnline && isOnline)
Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained")
if (connectionGained) {
chatActivity.messageInputViewModel.sendTempMessages(
chatActivity.messageInputViewModel.sendUnsentMessages(
chatActivity.conversationUser!!.getCredentials(),
ApiUtils.getUrlForChat(
chatActivity.chatApiVersion,

View File

@ -110,7 +110,7 @@ interface ChatMessageRepository : LifecycleAwareManager {
suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow<Boolean>
suspend fun sendTempChatMessages(credentials: String, url: String)
suspend fun sendUnsentChatMessages(credentials: String, url: String)
suspend fun deleteTempMessage(chatMessage: ChatMessage)
}

View File

@ -183,19 +183,26 @@ class MediaPlayerManager : LifecycleAwareManager {
continue
}
if (mediaPlayer != null && mediaPlayer?.isPlaying == true) {
val pos = mediaPlayer!!.currentPosition
mediaPlayer?.let { player ->
try {
if (!player.isPlaying) return@let
} catch (e: IllegalStateException) {
Log.e(TAG, "Seekbar updated during an improper state: $e")
return@let
}
val pos = player.currentPosition
mediaPlayerPosition = pos
val progress = (pos.toFloat() / mediaPlayerDuration) * DIVIDER
val progressI = ceil(progress).toInt()
val seconds = (pos / ONE_SEC)
_mediaPlayerSeekBarPosition.emit(progressI)
currentCycledMessage?.let {
it.isPlayingVoiceMessage = true
it.voiceMessageSeekbarProgress = progressI
it.voiceMessagePlayedSeconds = seconds
if (progressI >= IS_PLAYED_CUTOFF) it.wasPlayedVoiceMessage = true
_mediaPlayerSeekBarPositionMsg.emit(it)
currentCycledMessage?.let { msg ->
msg.isPlayingVoiceMessage = true
msg.voiceMessageSeekbarProgress = progressI
msg.voiceMessagePlayedSeconds = seconds
if (progressI >= IS_PLAYED_CUTOFF) msg.wasPlayedVoiceMessage = true
_mediaPlayerSeekBarPositionMsg.emit(msg)
}
}

View File

@ -14,6 +14,7 @@ import android.util.Log
import com.bluelinelabs.logansquare.annotation.JsonIgnore
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
@ -119,7 +120,7 @@ data class ChatMessage(
var referenceId: String? = null,
var sendingFailed: Boolean = true,
var sendStatus: SendStatus? = null,
var silent: Boolean = false

View File

@ -76,4 +76,5 @@ interface ChatNetworkDataSource {
limit: Int
): List<ChatMessageJson>
suspend fun getOpenGraph(credentials: String, baseUrl: String, extractedLinkToPreview: String): Reference?
suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall
}

View File

@ -19,6 +19,7 @@ import com.nextcloud.talk.data.database.mappers.asEntity
import com.nextcloud.talk.data.database.mappers.asModel
import com.nextcloud.talk.data.database.model.ChatBlockEntity
import com.nextcloud.talk.data.database.model.ChatMessageEntity
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.extensions.toIntOrZero
@ -214,7 +215,8 @@ class OfflineFirstChatRepository @Inject constructor(
)
}
sendTempChatMessages(credentials, urlForChatting)
// this call could be deleted when we have a worker to send messages..
sendUnsentChatMessages(credentials, urlForChatting)
// delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing
// with them (otherwise there is a race condition).
@ -365,11 +367,18 @@ class OfflineFirstChatRepository @Inject constructor(
lookIntoFuture: Boolean,
showUnreadMessagesMarker: Boolean
) {
receivedChatMessages.forEach {
Log.d(TAG, "receivedChatMessage: " + it.message)
}
// remove all temp messages from UI
val oldTempMessages = chatDao.getTempMessagesForConversation(internalConversationId)
.first()
.map(ChatMessageEntity::asModel)
oldTempMessages.forEach { _removeMessageFlow.emit(it) }
oldTempMessages.forEach {
Log.d(TAG, "oldTempMessage to be removed from UI: " + it.message)
_removeMessageFlow.emit(it)
}
// add new messages to UI
val tripleChatMessages = Triple(lookIntoFuture, showUnreadMessagesMarker, receivedChatMessages)
@ -378,6 +387,9 @@ class OfflineFirstChatRepository @Inject constructor(
// remove temp messages from DB that are now found in the new messages
val chatMessagesReferenceIds = receivedChatMessages.mapTo(HashSet(receivedChatMessages.size)) { it.referenceId }
val tempChatMessagesThatCanBeReplaced = oldTempMessages.filter { it.referenceId in chatMessagesReferenceIds }
tempChatMessagesThatCanBeReplaced.forEach {
Log.d(TAG, "oldTempMessage that was identified in newMessages: " + it.message)
}
chatDao.deleteTempChatMessages(
internalConversationId,
tempChatMessagesThatCanBeReplaced.map { it.referenceId!! }
@ -389,6 +401,10 @@ class OfflineFirstChatRepository @Inject constructor(
.sortedBy { it.internalId }
.map(ChatMessageEntity::asModel)
remainingTempMessages.forEach {
Log.d(TAG, "remainingTempMessage: " + it.message)
}
val triple = Triple(true, false, remainingTempMessages)
_messageFlow.emit(triple)
}
@ -843,6 +859,17 @@ class OfflineFirstChatRepository @Inject constructor(
val chatMessageModel = response.ocs?.data?.asModel()
val sentMessage = chatDao.getTempMessageForConversation(
internalConversationId,
referenceId
).firstOrNull()
sentMessage?.let {
it.sendStatus = SendStatus.SENT_PENDING_ACK
chatDao.updateChatMessage(it)
}
Log.d(TAG, "sending chat message succeeded: " + message)
emit(Result.success(chatMessageModel))
}
.catch { e ->
@ -853,7 +880,7 @@ class OfflineFirstChatRepository @Inject constructor(
referenceId
).firstOrNull()
failedMessage?.let {
it.sendingFailed = true
it.sendStatus = SendStatus.FAILED
chatDao.updateChatMessage(it)
val failedMessageModel = it.asModel()
@ -874,7 +901,7 @@ class OfflineFirstChatRepository @Inject constructor(
referenceId: String
): Flow<Result<ChatMessage?>> {
val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first()
messageToResend.sendingFailed = false
messageToResend.sendStatus = SendStatus.PENDING
chatDao.updateChatMessage(messageToResend)
val messageToResendModel = messageToResend.asModel()
@ -930,8 +957,8 @@ class OfflineFirstChatRepository @Inject constructor(
}
}
override suspend fun sendTempChatMessages(credentials: String, url: String) {
val tempMessages = chatDao.getTempMessagesForConversation(internalConversationId).first()
override suspend fun sendUnsentChatMessages(credentials: String, url: String) {
val tempMessages = chatDao.getTempUnsentMessagesForConversation(internalConversationId).first()
tempMessages.sortedBy { it.internalId }.onEach {
sendChatMessage(
credentials,
@ -1025,7 +1052,7 @@ class OfflineFirstChatRepository @Inject constructor(
actorDisplayName = currentUser.displayName!!,
referenceId = referenceId,
isTemporary = true,
sendingFailed = false,
sendStatus = SendStatus.PENDING,
silent = sendWithoutNotification
)
return entity

View File

@ -217,4 +217,9 @@ class RetrofitChatNetwork(
extractedLinkToPreview
).blockingFirst().ocs?.data?.references?.entries?.iterator()?.next()?.value
}
override suspend fun unbindRoom(credentials: String, baseUrl: String, roomToken: String): GenericOverall {
val url = ApiUtils.getUrlForUnbindingRoom(baseUrl, roomToken)
return ncApiCoroutines.unbindRoom(credentials, url)
}
}

View File

@ -146,6 +146,10 @@ class ChatViewModel @Inject constructor(
val outOfOfficeViewState: LiveData<OutOfOfficeUIState>
get() = _outOfOfficeViewState
private val _unbindRoomResult = MutableLiveData<UnbindRoomUiState>(UnbindRoomUiState.None)
val unbindRoomResult: LiveData<UnbindRoomUiState>
get() = _unbindRoomResult
private val _voiceMessagePlaybackSpeedPreferences: MutableLiveData<Map<String, PlaybackSpeed>> = MutableLiveData()
val voiceMessagePlaybackSpeedPreferences: LiveData<Map<String, PlaybackSpeed>>
get() = _voiceMessagePlaybackSpeedPreferences
@ -800,6 +804,18 @@ class ChatViewModel @Inject constructor(
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun unbindRoom(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = chatNetworkDataSource.unbindRoom(credentials, baseUrl, roomToken)
_unbindRoomResult.value = UnbindRoomUiState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_unbindRoomResult.value = UnbindRoomUiState.Error(exception.message.toString())
}
}
}
fun resendMessage(credentials: String, urlForChat: String, message: ChatMessage) {
viewModelScope.launch {
chatRepository.resendChatMessage(
@ -851,4 +867,10 @@ class ChatViewModel @Inject constructor(
data class Success(val userAbsence: UserAbsenceData) : OutOfOfficeUIState()
data class Error(val exception: Exception) : OutOfOfficeUIState()
}
sealed class UnbindRoomUiState {
data object None : UnbindRoomUiState()
data class Success(val statusCode: Int) : UnbindRoomUiState()
data class Error(val message: String) : UnbindRoomUiState()
}
}

View File

@ -169,9 +169,9 @@ class MessageInputViewModel @Inject constructor(
}
}
fun sendTempMessages(credentials: String, url: String) {
fun sendUnsentMessages(credentials: String, url: String) {
viewModelScope.launch {
chatRepository.sendTempChatMessages(
chatRepository.sendUnsentChatMessages(
credentials,
url
)

View File

@ -1,7 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/

View File

@ -29,6 +29,7 @@ fun ContactsScreen(contactsViewModel: ContactsViewModel, uiState: ContactsUiStat
val isSearchActive by contactsViewModel.isSearchActive.collectAsStateWithLifecycle()
val isAddParticipants by contactsViewModel.isAddParticipantsView.collectAsStateWithLifecycle()
val autocompleteUsers by contactsViewModel.selectedParticipantsList.collectAsStateWithLifecycle()
val enableAddButton by contactsViewModel.enableAddButton.collectAsStateWithLifecycle()
Scaffold(
topBar = {
@ -49,6 +50,10 @@ fun ContactsScreen(contactsViewModel: ContactsViewModel, uiState: ContactsUiStat
},
onUpdateAutocompleteUsers = {
contactsViewModel.getContactsFromSearchParams()
},
enableAddButton = enableAddButton,
clickAddButton = {
contactsViewModel.modifyClickAddButton(it)
}
)
},

View File

@ -36,6 +36,15 @@ class ContactsViewModel @Inject constructor(
private val _isAddParticipantsView = MutableStateFlow(false)
val isAddParticipantsView: StateFlow<Boolean> = _isAddParticipantsView
private val _enableAddButton = MutableStateFlow(false)
val enableAddButton: StateFlow<Boolean> = _enableAddButton
@Suppress("PropertyName")
private val _selectedContacts = MutableStateFlow<List<AutocompleteUser>>(emptyList())
@Suppress("PropertyName")
private val _clickAddButton = MutableStateFlow(false)
private var hideAlreadyAddedParticipants: Boolean = false
init {
@ -46,14 +55,28 @@ class ContactsViewModel @Inject constructor(
_searchQuery.value = query
}
fun modifyClickAddButton(value: Boolean) {
_clickAddButton.value = value
}
fun selectContact(contact: AutocompleteUser) {
val updatedParticipants = selectedParticipants.value + contact
selectedParticipants.value = updatedParticipants
_selectedContacts.value = _selectedContacts.value + contact
}
fun updateAddButtonState() {
if (_selectedContacts.value.isEmpty()) {
_enableAddButton.value = false
} else {
_enableAddButton.value = true
}
}
fun deselectContact(contact: AutocompleteUser) {
val updatedParticipants = selectedParticipants.value - contact
selectedParticipants.value = updatedParticipants
_selectedContacts.value = _selectedContacts.value - contact
}
fun updateSelectedParticipants(participants: List<AutocompleteUser>) {
@ -76,20 +99,23 @@ class ContactsViewModel @Inject constructor(
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun getContactsFromSearchParams() {
fun getContactsFromSearchParams(query: String = "") {
_contactsViewState.value = ContactsUiState.Loading
viewModelScope.launch {
try {
val contacts = repository.getContacts(
searchQuery.value,
if (query != "") query else searchQuery.value,
shareTypeList
)
val contactsList: MutableList<AutocompleteUser>? = contacts.ocs!!.data?.toMutableList()
if (hideAlreadyAddedParticipants) {
if (hideAlreadyAddedParticipants && !_clickAddButton.value) {
contactsList?.removeAll(selectedParticipants.value)
}
if (_clickAddButton.value) {
contactsList?.removeAll(selectedParticipants.value)
contactsList?.addAll(_selectedContacts.value)
}
_contactsViewState.value = ContactsUiState.Success(contactsList)
} catch (exception: Exception) {
_contactsViewState.value = ContactsUiState.Error(exception.message ?: "")

View File

@ -13,6 +13,8 @@ import android.app.Activity
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Search
@ -20,8 +22,10 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@ -30,6 +34,7 @@ import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
@SuppressLint("UnrememberedMutableState")
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongParameterList", "LongMethod")
@Composable
fun AppBar(
title: String,
@ -40,12 +45,18 @@ fun AppBar(
onEnableSearch: () -> Unit,
onDisableSearch: () -> Unit,
onUpdateSearchQuery: (String) -> Unit,
onUpdateAutocompleteUsers: () -> Unit
onUpdateAutocompleteUsers: () -> Unit,
enableAddButton: Boolean,
clickAddButton: (Boolean) -> Unit
) {
val context = LocalContext.current
val appTitle = if (!isSearchActive) {
title
} else {
""
}
TopAppBar(
title = { Text(text = title) },
title = { Text(text = appTitle) },
navigationIcon = {
IconButton(onClick = {
(context as? Activity)?.finish()
@ -54,36 +65,53 @@ fun AppBar(
}
},
actions = {
IconButton(onClick = onEnableSearch) {
Icon(Icons.Filled.Search, contentDescription = stringResource(R.string.search_icon))
}
if (isAddParticipants) {
Text(
text = stringResource(id = R.string.nc_contacts_done),
modifier = Modifier.clickable {
val resultIntent = Intent().apply {
putParcelableArrayListExtra(
"selectedParticipants",
ArrayList(autocompleteUsers)
)
if (!isSearchActive) {
IconButton(onClick = onEnableSearch) {
Icon(Icons.Filled.Search, contentDescription = stringResource(R.string.search_icon))
}
if (isAddParticipants) {
Text(
text = stringResource(id = R.string.nc_contacts_done),
modifier = Modifier.clickable {
val resultIntent = Intent().apply {
putParcelableArrayListExtra(
"selectedParticipants",
ArrayList(autocompleteUsers)
)
}
(context as? Activity)?.setResult(Activity.RESULT_OK, resultIntent)
(context as? Activity)?.finish()
}
(context as? Activity)?.setResult(Activity.RESULT_OK, resultIntent)
(context as? Activity)?.finish()
}
)
)
}
}
}
)
if (isSearchActive) {
Row {
Row(modifier = Modifier.fillMaxWidth()) {
SearchComponent(
text = searchQuery,
onTextChange = { searchQuery ->
onUpdateSearchQuery(searchQuery)
onUpdateAutocompleteUsers()
},
onDisableSearch = onDisableSearch
onDisableSearch = onDisableSearch,
modifier = Modifier.weight(1f)
)
if (isAddParticipants) {
TextButton(
modifier = Modifier.align(Alignment.CenterVertically).wrapContentWidth(),
onClick = {
onDisableSearch()
onUpdateSearchQuery("")
clickAddButton(true)
onUpdateAutocompleteUsers()
},
enabled = enableAddButton
) {
Text(text = context.getString(R.string.add_participants))
}
}
}
}
}

View File

@ -65,8 +65,10 @@ fun ContactItemRow(contact: AutocompleteUser, contactsViewModel: ContactsViewMod
isSelected = !isSelected
if (isSelected) {
contactsViewModel.selectContact(contact)
contactsViewModel.updateAddButtonState()
} else {
contactsViewModel.deselectContact(contact)
contactsViewModel.updateAddButtonState()
}
}
}

View File

@ -1,14 +1,13 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@ -34,16 +33,19 @@ import androidx.compose.ui.unit.sp
import com.nextcloud.talk.R
@Composable
fun SearchComponent(text: String, onTextChange: (String) -> Unit, onDisableSearch: () -> Unit) {
fun SearchComponent(
text: String,
onTextChange: (String) -> Unit,
onDisableSearch: () -> Unit,
modifier: Modifier = Modifier
) {
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth()
.height(60.dp),
value = text,
onValueChange = { onTextChange(it) },
modifier = modifier
.background(MaterialTheme.colorScheme.background)
.height(60.dp),
placeholder = { Text(text = stringResource(R.string.nc_search)) },
textStyle = TextStyle(fontSize = 16.sp),
singleLine = true,

View File

@ -103,6 +103,11 @@ import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Calendar
import java.util.Collections
import java.util.Locale
@ -150,7 +155,7 @@ class ConversationInfoActivity :
get() {
if (!TextUtils.isEmpty(conversationToken)) {
val data = Data.Builder()
data.putString(BundleKeys.KEY_ROOM_TOKEN, conversationToken)
data.putString(KEY_ROOM_TOKEN, conversationToken)
data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, conversationUser.id!!)
return data.build()
}
@ -192,7 +197,7 @@ class ConversationInfoActivity :
conversationUser = currentUserProvider.currentUser.blockingGet()
conversationToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!!
conversationToken = intent.getStringExtra(KEY_ROOM_TOKEN)!!
hasAvatarSpacing = intent.getBooleanExtra(BundleKeys.KEY_ROOM_ONE_TO_ONE, false)
credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)!!
}
@ -245,8 +250,51 @@ class ConversationInfoActivity :
initBanActorObserver()
initConversationReadOnlyObserver()
initClearChatHistoryObserver()
initMarkConversationAsSensitiveObserver()
initMarkConversationAsInsensitiveObserver()
initMarkConversationAsImportantObserver()
initMarkConversationAsUnimportantObserver()
}
private fun initMarkConversationAsSensitiveObserver() {
viewModel.markAsSensitiveResult.observe(this) { uiState ->
when (uiState) {
is ConversationInfoViewModel.MarkConversationAsSensitiveViewState.Success -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_mark_conversation_as_sensitive),
Snackbar.LENGTH_LONG
).show()
}
is ConversationInfoViewModel.MarkConversationAsSensitiveViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to mark conversation as insensitive", uiState.exception)
}
else -> {
}
}
}
}
private fun initMarkConversationAsInsensitiveObserver() {
viewModel.markAsInsensitiveResult.observe(this) { uiState ->
when (uiState) {
is ConversationInfoViewModel.MarkConversationAsInsensitiveViewState.Success -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_mark_conversation_as_insensitive),
Snackbar.LENGTH_LONG
).show()
}
is ConversationInfoViewModel.MarkConversationAsInsensitiveViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to mark conversation as sensitive", uiState.exception)
}
else -> {
}
}
}
}
private fun initClearChatHistoryObserver() {
viewModel.clearChatHistoryViewState.observe(this) { uiState ->
when (uiState) {
@ -335,6 +383,47 @@ class ConversationInfoActivity :
}
}
private fun initMarkConversationAsImportantObserver() {
viewModel.markAsImportantResult.observe(this) { uiState ->
when (uiState) {
is ConversationInfoViewModel.MarkConversationAsImportantViewState.Success -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_mark_conversation_as_important),
Snackbar.LENGTH_LONG
).show()
}
is ConversationInfoViewModel.MarkConversationAsImportantViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to mark conversation as important", uiState.exception)
}
else -> {
}
}
}
}
private fun initMarkConversationAsUnimportantObserver() {
viewModel.markAsUnimportantResult.observe(this) { uiState ->
when (uiState) {
is ConversationInfoViewModel.MarkConversationAsUnimportantViewState.Success -> {
Snackbar.make(
binding.root,
context.getString(R.string.nc_mark_conversation_as_unimportant),
Snackbar.LENGTH_LONG
).show()
}
is ConversationInfoViewModel.MarkConversationAsUnimportantViewState.Error -> {
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
Log.e(TAG, "failed to mark conversation as unimportant", uiState.exception)
}
else -> {
}
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
private fun initViewStateObserver() {
viewModel.viewState.observe(this) { state ->
when (state) {
@ -354,6 +443,12 @@ class ConversationInfoActivity :
canGeneratePrettyURL
)
}
conversation?.let {
if (it.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
viewModel.getProfileData(conversationUser, it.name)
}
}
}
is ConversationInfoViewModel.GetRoomErrorState -> {
@ -363,6 +458,57 @@ class ConversationInfoActivity :
else -> {}
}
}
viewModel.getProfileViewState.observe(this) { state ->
when (state) {
is ConversationInfoViewModel.GetProfileSuccessState -> {
try {
// Pronouns
val profile = state.profile
val pronouns = profile.pronouns ?: ""
binding.pronouns.text = pronouns
// Role @ Organization
val concat1 = if (profile.role != null && profile.company != null) " @ " else ""
val role = profile.role ?: ""
val company = profile.company ?: ""
val professionCompanyText = "$role$concat1$company"
binding.professionCompany.text = professionCompanyText
// Local Time: xX:xX · Address
val profileZoneOffset = ZoneOffset.ofTotalSeconds(0)
val secondsToAdd = profile.timezoneOffset?.toLong() ?: 0
val localTime = ZonedDateTime.ofInstant(
Instant.now().plusSeconds(secondsToAdd),
profileZoneOffset
)
val localTimeString = localTime.format(
DateTimeFormatter
.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.getDefault())
)
val concat2 = if (profile.address != null) " · " else ""
val address = profile.address ?: ""
val localTimeLocation = "$localTimeString$concat2$address"
binding.locationTime.text = resources.getString(R.string.local_time, localTimeLocation)
binding.pronouns.visibility = VISIBLE
binding.professionCompany.visibility = if (professionCompanyText.isNotEmpty()) VISIBLE else GONE
binding.locationTime.visibility = VISIBLE
} catch (e: Exception) {
Log.e(TAG, "Exception getting profile information", e)
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
}
}
is ConversationInfoViewModel.GetProfileErrorState -> {
Log.e(TAG, "Network error occurred getting profile information")
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
}
else -> {}
}
}
}
private fun setupActionBar() {
@ -404,7 +550,7 @@ class ConversationInfoActivity :
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.edit) {
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversationToken)
bundle.putString(KEY_ROOM_TOKEN, conversationToken)
val intent = Intent(this, ConversationInfoEditActivity::class.java)
intent.putExtras(bundle)
@ -421,7 +567,9 @@ class ConversationInfoActivity :
binding.notificationSettingsView.importantConversationSwitch,
binding.guestAccessView.allowGuestsSwitch,
binding.guestAccessView.passwordProtectionSwitch,
binding.recordingConsentView.recordingConsentForConversationSwitch
binding.recordingConsentView.recordingConsentForConversationSwitch,
binding.lockConversationSwitch,
binding.notificationSettingsView.sensitiveConversationSwitch
).forEach(viewThemeUtils.talk::colorSwitch)
}
}
@ -445,7 +593,7 @@ class ConversationInfoActivity :
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.putExtra(BundleKeys.KEY_CONVERSATION_NAME, conversation?.displayName)
intent.putExtra(BundleKeys.KEY_ROOM_TOKEN, conversationToken)
intent.putExtra(KEY_ROOM_TOKEN, conversationToken)
intent.putExtra(
SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR,
ConversationUtils.isParticipantOwnerOrModerator(conversation!!)
@ -538,7 +686,7 @@ class ConversationInfoActivity :
) {
binding.webinarInfoView.startTimeButtonSummary.text = (
dateUtils.getLocalDateTimeStringFromTimestamp(
conversation!!.lobbyTimer!! * DateConstants.SECOND_DIVIDER
conversation!!.lobbyTimer * DateConstants.SECOND_DIVIDER
)
)
} else {
@ -734,16 +882,13 @@ class ConversationInfoActivity :
private fun selectParticipantsToAdd() {
val bundle = Bundle()
val existingParticipants = ArrayList<AutocompleteUser>()
for (userItem in userItems) {
if (userItem.model.calculatedActorType == USERS) {
val user = AutocompleteUser(
userItem.model.calculatedActorId!!,
userItem.model.displayName,
userItem.model.calculatedActorType.name.lowercase()
)
existingParticipants.add(user)
}
val user = AutocompleteUser(
userItem.model.calculatedActorId!!,
userItem.model.displayName,
userItem.model.calculatedActorType.name.lowercase()
)
existingParticipants.add(user)
}
bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true)
@ -942,6 +1087,31 @@ class ConversationInfoActivity :
}
}
binding.notificationSettingsView.importantConversationSwitch.isChecked = conversation!!.hasImportant
binding.notificationSettingsView.notificationSettingsImportantConversation.setOnClickListener {
val isChecked = binding.notificationSettingsView.importantConversationSwitch.isChecked
binding.notificationSettingsView.importantConversationSwitch.isChecked = !isChecked
if (!isChecked) {
viewModel.markConversationAsImportant(
credentials,
conversationUser.baseUrl!!,
conversation?.token!!
)
} else {
viewModel.markConversationAsUnimportant(
credentials,
conversationUser.baseUrl!!,
conversation?.token!!
)
}
}
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.IMPORTANT_CONVERSATIONS)) {
binding.notificationSettingsView.notificationSettingsImportantConversation.visibility = VISIBLE
} else {
binding.notificationSettingsView.notificationSettingsImportantConversation.visibility = GONE
}
if (!hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.ARCHIVE_CONVERSATIONS)) {
binding.archiveConversationBtn.visibility = GONE
binding.archiveConversationTextHint.visibility = GONE
@ -977,6 +1147,31 @@ class ConversationInfoActivity :
binding.archiveConversationText.text = resources.getString(R.string.archive_conversation)
binding.archiveConversationTextHint.text = resources.getString(R.string.archive_hint)
}
binding.notificationSettingsView.sensitiveConversationSwitch.isChecked = conversation!!.hasSensitive
binding.notificationSettingsView.notificationSettingsSensitiveConversation.setOnClickListener {
val isChecked = !binding.notificationSettingsView.sensitiveConversationSwitch.isChecked
binding.notificationSettingsView.sensitiveConversationSwitch.isChecked = isChecked
if (isChecked) {
viewModel.markConversationAsSensitive(
credentials,
conversationUser.baseUrl!!,
conversation?.token!!
)
} else {
viewModel.markConversationAsInsensitive(
credentials,
conversationUser.baseUrl!!,
conversation?.token!!
)
}
}
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SENSITIVE_CONVERSATIONS)) {
binding.notificationSettingsView.notificationSettingsSensitiveConversation.visibility = VISIBLE
} else {
binding.notificationSettingsView.notificationSettingsSensitiveConversation.visibility = GONE
}
if (ConversationUtils.isConversationReadOnlyAvailable(conversationCopy, spreedCapabilities)) {
binding.lockConversation.visibility = VISIBLE
binding.lockConversationSwitch.isChecked = databaseStorageModule!!.getBoolean("lock_switch", false)
@ -1039,7 +1234,7 @@ class ConversationInfoActivity :
binding.displayNameText.text = conversation!!.displayName
if (conversation!!.description != null && conversation!!.description!!.isNotEmpty()) {
if (conversation!!.description != null && conversation!!.description.isNotEmpty()) {
binding.descriptionText.text = conversation!!.description
binding.conversationDescription.visibility = VISIBLE
}
@ -1185,7 +1380,7 @@ class ConversationInfoActivity :
val stringValue: String =
when (
DomainEnumNotificationLevelConverter()
.convertToInt(conversation!!.notificationLevel!!)
.convertToInt(conversation!!.notificationLevel)
) {
NOTIFICATION_LEVEL_ALWAYS -> resources.getString(R.string.nc_notify_me_always)
NOTIFICATION_LEVEL_MENTION -> resources.getString(R.string.nc_notify_me_mention)
@ -1234,7 +1429,7 @@ class ConversationInfoActivity :
conversation!!.name
)
) {
conversation!!.name?.let {
conversation!!.name.let {
binding.avatarImage.loadUserAvatar(
conversationUser,
it,
@ -1648,13 +1843,6 @@ class ConversationInfoActivity :
}
private fun setUpNotificationSettings(module: DatabaseStorageModule) {
binding.notificationSettingsView.notificationSettingsImportantConversation.setOnClickListener {
val isChecked = binding.notificationSettingsView.importantConversationSwitch.isChecked
binding.notificationSettingsView.importantConversationSwitch.isChecked = !isChecked
lifecycleScope.launch {
module.saveBoolean("important_conversation_switch", !isChecked)
}
}
binding.notificationSettingsView.notificationSettingsCallNotifications.setOnClickListener {
val isChecked = binding.notificationSettingsView.callNotificationsSwitch.isChecked
binding.notificationSettingsView.callNotificationsSwitch.isChecked = !isChecked
@ -1662,6 +1850,7 @@ class ConversationInfoActivity :
module.saveBoolean("call_notifications_switch", !isChecked)
}
}
binding.notificationSettingsView.conversationInfoMessageNotificationsDropdown
.setOnItemClickListener { _, _, position, _ ->
val value = resources.getStringArray(R.array.message_notification_levels_entry_values)[position]
@ -1671,9 +1860,6 @@ class ConversationInfoActivity :
}
}
binding.notificationSettingsView.importantConversationSwitch.isChecked = module
.getBoolean("important_conversation_switch", false)
if (conversation!!.remoteServer.isNullOrEmpty()) {
binding.notificationSettingsView.notificationSettingsCallNotifications.visibility = VISIBLE
binding.notificationSettingsView.callNotificationsSwitch.isChecked = module

View File

@ -28,6 +28,7 @@ import com.nextcloud.talk.models.json.participants.Participant.ActorType.EMAILS
import com.nextcloud.talk.models.json.participants.Participant.ActorType.FEDERATED
import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS
import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.profile.Profile
import com.nextcloud.talk.repositories.conversations.ConversationsRepository
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ApiUtils.getUrlForRooms
@ -123,10 +124,40 @@ class ConversationInfoViewModel @Inject constructor(
val getConversationReadOnlyState: LiveData<SetConversationReadOnlyViewState>
get() = _getConversationReadOnlyState
@Suppress("PropertyName")
private val _markConversationAsImportantResult =
MutableLiveData<MarkConversationAsImportantViewState>(MarkConversationAsImportantViewState.None)
val markAsImportantResult: LiveData<MarkConversationAsImportantViewState>
get() = _markConversationAsImportantResult
@Suppress("PropertyName")
private val _markConversationAsUnimportantResult =
MutableLiveData<MarkConversationAsUnimportantViewState>(MarkConversationAsUnimportantViewState.None)
val markAsUnimportantResult: LiveData<MarkConversationAsUnimportantViewState>
get() = _markConversationAsUnimportantResult
private val _createRoomViewState = MutableLiveData<CreateRoomUIState>(CreateRoomUIState.None)
val createRoomViewState: LiveData<CreateRoomUIState>
get() = _createRoomViewState
object GetProfileErrorState : ViewState
class GetProfileSuccessState(val profile: Profile) : ViewState
private val _getProfileViewState = MutableLiveData<ViewState>()
val getProfileViewState: LiveData<ViewState>
get() = _getProfileViewState
@Suppress("PropertyName")
private val _markConversationAsSensitiveResult =
MutableLiveData<MarkConversationAsSensitiveViewState>(MarkConversationAsSensitiveViewState.None)
val markAsSensitiveResult: LiveData<MarkConversationAsSensitiveViewState>
get() = _markConversationAsSensitiveResult
@Suppress("PropertyName")
private val _markConversationAsInsensitiveResult =
MutableLiveData<MarkConversationAsInsensitiveViewState>(MarkConversationAsInsensitiveViewState.None)
val markAsInsensitiveResult: LiveData<MarkConversationAsInsensitiveViewState>
get() = _markConversationAsInsensitiveResult
fun getRoom(user: User, token: String) {
_viewState.value = GetRoomStartState
chatNetworkDataSource.getRoom(user, token)
@ -288,6 +319,23 @@ class ConversationInfoViewModel @Inject constructor(
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun getProfileData(user: User, userId: String) {
val url = ApiUtils.getUrlForProfile(user.baseUrl!!, userId)
viewModelScope.launch {
try {
val profile = conversationsRepository.getProfile(user.getCredentials(), url)
if (profile != null) {
_getProfileViewState.value = GetProfileSuccessState(profile)
} else {
_getProfileViewState.value = GetProfileErrorState
}
} catch (e: Exception) {
Log.w(TAG, "Failed to get profile data (if not supported there wil be http405)", e)
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun allowGuests(token: String, allow: Boolean) {
viewModelScope.launch {
@ -325,6 +373,34 @@ class ConversationInfoViewModel @Inject constructor(
conversationsRepository.unarchiveConversation(user.getCredentials(), url)
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun markConversationAsImportant(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = conversationsRepository.markConversationAsImportant(credentials, baseUrl, roomToken)
_markConversationAsImportantResult.value =
MarkConversationAsImportantViewState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_markConversationAsImportantResult.value =
MarkConversationAsImportantViewState.Error(exception)
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun markConversationAsUnimportant(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = conversationsRepository.markConversationAsUnImportant(credentials, baseUrl, roomToken)
_markConversationAsUnimportantResult.value =
MarkConversationAsUnimportantViewState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_markConversationAsUnimportantResult.value =
MarkConversationAsUnimportantViewState.Error(exception)
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun clearChatHistory(apiVersion: Int, roomToken: String) {
viewModelScope.launch {
@ -337,6 +413,34 @@ class ConversationInfoViewModel @Inject constructor(
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun markConversationAsSensitive(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = conversationsRepository.markConversationAsSensitive(credentials, baseUrl, roomToken)
_markConversationAsSensitiveResult.value =
MarkConversationAsSensitiveViewState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_markConversationAsSensitiveResult.value =
MarkConversationAsSensitiveViewState.Error(exception)
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun markConversationAsInsensitive(credentials: String, baseUrl: String, roomToken: String) {
viewModelScope.launch {
try {
val response = conversationsRepository.markConversationAsInsensitive(credentials, baseUrl, roomToken)
_markConversationAsInsensitiveResult.value =
MarkConversationAsInsensitiveViewState.Success(response.ocs?.meta?.statusCode!!)
} catch (exception: Exception) {
_markConversationAsInsensitiveResult.value =
MarkConversationAsInsensitiveViewState.Error(exception)
}
}
}
inner class GetRoomObserver : Observer<ConversationModel> {
override fun onSubscribe(d: Disposable) {
// unused atm
@ -386,6 +490,18 @@ class ConversationInfoViewModel @Inject constructor(
data class Error(val exception: Exception) : ClearChatHistoryViewState()
}
sealed class MarkConversationAsSensitiveViewState {
data object None : MarkConversationAsSensitiveViewState()
data class Success(val statusCode: Int) : MarkConversationAsSensitiveViewState()
data class Error(val exception: Exception) : MarkConversationAsSensitiveViewState()
}
sealed class MarkConversationAsInsensitiveViewState {
data object None : MarkConversationAsInsensitiveViewState()
data class Success(val statusCode: Int) : MarkConversationAsInsensitiveViewState()
data class Error(val exception: Exception) : MarkConversationAsInsensitiveViewState()
}
sealed class SetConversationReadOnlyViewState {
data object None : SetConversationReadOnlyViewState()
data object Success : SetConversationReadOnlyViewState()
@ -409,4 +525,16 @@ class ConversationInfoViewModel @Inject constructor(
data object Success : PasswordUiState()
data class Error(val exception: Exception) : PasswordUiState()
}
sealed class MarkConversationAsImportantViewState {
data object None : MarkConversationAsImportantViewState()
data class Success(val statusCode: Int) : MarkConversationAsImportantViewState()
data class Error(val exception: Exception) : MarkConversationAsImportantViewState()
}
sealed class MarkConversationAsUnimportantViewState {
data object None : MarkConversationAsUnimportantViewState()
data class Success(val statusCode: Int) : MarkConversationAsUnimportantViewState()
data class Error(val exception: Exception) : MarkConversationAsUnimportantViewState()
}
}

View File

@ -16,7 +16,6 @@ import android.animation.AnimatorInflater
import android.annotation.SuppressLint
import android.app.SearchManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
@ -41,9 +40,12 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.os.bundleOf
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
@ -114,6 +116,9 @@ import com.nextcloud.talk.ui.dialog.ChooseAccountShareToDialogFragment
import com.nextcloud.talk.ui.dialog.ContextChatCompose
import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog
import com.nextcloud.talk.ui.dialog.FilterConversationFragment
import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.ARCHIVE
import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.MENTION
import com.nextcloud.talk.ui.dialog.FilterConversationFragment.Companion.UNREAD
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.BrandingUtils
@ -208,7 +213,7 @@ class ConversationsListActivity :
private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
private var conversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var conversationItemsWithHeader: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private val searchableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var searchableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var filterableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var nearFutureEventConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var searchItem: MenuItem? = null
@ -232,9 +237,9 @@ class ConversationsListActivity :
private var searchViewDisposable: Disposable? = null
private var filterState =
mutableMapOf(
FilterConversationFragment.MENTION to false,
FilterConversationFragment.UNREAD to false,
FilterConversationFragment.ARCHIVE to false,
MENTION to false,
UNREAD to false,
ARCHIVE to false,
FilterConversationFragment.DEFAULT to true
)
val searchBehaviorSubject = BehaviorSubject.createDefault(false)
@ -406,6 +411,10 @@ class ConversationsListActivity :
conversationsListViewModel.getRoomsFlow
.onEach { list ->
setConversationList(list)
val noteToSelf = list
.firstOrNull { ConversationUtils.isNoteToSelfConversation(it) }
val isNoteToSelfAvailable = noteToSelf != null
handleNoteToSelfShortcut(isNoteToSelfAvailable, noteToSelf?.token ?: "")
}.collect()
}
@ -458,7 +467,13 @@ class ConversationsListActivity :
userItems.add(contactItem)
}
searchableConversationItems.addAll(userItems)
val list = searchableConversationItems.filter {
it !is ContactItem
}.toMutableList()
list.addAll(userItems)
searchableConversationItems = list
}
else -> {}
@ -516,6 +531,29 @@ class ConversationsListActivity :
}
}
private fun handleNoteToSelfShortcut(noteToSelfAvailable: Boolean, noteToSelfToken: String) {
if (noteToSelfAvailable) {
val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, noteToSelfToken)
bundle.putBoolean(BundleKeys.KEY_FOCUS_INPUT, true)
val intent = Intent(context, ChatActivity::class.java)
intent.putExtras(bundle)
intent.action = Intent.ACTION_VIEW
val openNotesString = resources.getString(R.string.open_notes)
val shortcut = ShortcutInfoCompat.Builder(context, NOTE_TO_SELF_SHORTCUT_ID)
.setShortLabel(openNotesString)
.setLongLabel(openNotesString)
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_pencil_grey600_24dp))
.setIntent(intent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
} else {
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(NOTE_TO_SELF_SHORTCUT_ID))
}
}
private fun setConversationList(list: List<ConversationModel>) {
// Update Conversations
conversationItems.clear()
@ -523,26 +561,29 @@ class ConversationsListActivity :
nearFutureEventConversationItems.clear()
for (conversation in list) {
if (!futureEvent(conversation)) {
if (!isFutureEvent(conversation) && !conversation.hasArchived) {
addToNearFutureEventConversationItems(conversation)
}
addToConversationItems(conversation)
}
getFilterStates()
val noFiltersActive = !(
filterState[MENTION] == true ||
filterState[UNREAD] == true ||
filterState[ARCHIVE] == true
)
sortConversations(conversationItems)
sortConversations(conversationItemsWithHeader)
sortConversations(nearFutureEventConversationItems)
if (!hasFilterEnabled() && searchBehaviorSubject.value == false) {
if (noFiltersActive && searchBehaviorSubject.value == false) {
adapter?.updateDataSet(nearFutureEventConversationItems, false)
} else {
// Filter Conversations
if (!hasFilterEnabled()) {
filterableConversationItems = conversationItems
}
filterConversation()
adapter?.updateDataSet(filterableConversationItems, false)
applyFilter()
}
Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong())
// Fetch Open Conversations
@ -551,9 +592,14 @@ class ConversationsListActivity :
intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1)
)
fetchOpenConversations(apiVersion)
}
// Get users
fetchUsers()
fun applyFilter() {
if (!hasFilterEnabled()) {
filterableConversationItems = conversationItems
}
filterConversation()
adapter?.updateDataSet(filterableConversationItems, false)
}
private fun hasFilterEnabled(): Boolean {
@ -564,13 +610,14 @@ class ConversationsListActivity :
return false
}
private fun futureEvent(conversation: ConversationModel): Boolean {
private fun isFutureEvent(conversation: ConversationModel): Boolean {
if (!conversation.objectId.contains("#")) {
return false
}
return conversation.objectType == ConversationEnums.ObjectType.EVENT &&
(conversation.objectId.split("#")[0].toLong() - (System.currentTimeMillis() / LONG_1000)) >
AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS
val eventTimeStart = conversation.objectId.split("#")[0].toLong()
val currentTimeStampInSeconds = System.currentTimeMillis() / LONG_1000
val sixteenHoursAfterTimeStamp = (eventTimeStart - currentTimeStampInSeconds) > SIXTEEN_HOURS_IN_SECONDS
return conversation.objectType == ConversationEnums.ObjectType.EVENT && sixteenHoursAfterTimeStamp
}
fun showOnlyNearFutureEvents() {
@ -584,32 +631,35 @@ class ConversationsListActivity :
nearFutureEventConversationItems.add(conversationItem)
}
fun filterConversation() {
fun getFilterStates() {
val accountId = UserIdUtils.getIdForUser(currentUser)
filterState[FilterConversationFragment.UNREAD] = (
filterState[UNREAD] = (
arbitraryStorageManager.getStorageSetting(
accountId,
FilterConversationFragment.UNREAD,
UNREAD,
""
).blockingGet()?.value ?: ""
) == "true"
filterState[FilterConversationFragment.MENTION] = (
filterState[MENTION] = (
arbitraryStorageManager.getStorageSetting(
accountId,
FilterConversationFragment.MENTION,
MENTION,
""
).blockingGet()?.value ?: ""
) == "true"
filterState[FilterConversationFragment.ARCHIVE] = (
filterState[ARCHIVE] = (
arbitraryStorageManager.getStorageSetting(
accountId,
FilterConversationFragment.ARCHIVE,
ARCHIVE,
""
).blockingGet()?.value ?: ""
) == "true"
}
fun filterConversation() {
getFilterStates()
val newItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
val items = conversationItems
for (i in items) {
@ -619,7 +669,7 @@ class ConversationsListActivity :
}
}
val archiveFilterOn = filterState[FilterConversationFragment.ARCHIVE] ?: false
val archiveFilterOn = filterState[ARCHIVE] == true
if (archiveFilterOn && newItems.isEmpty()) {
binding.noArchivedConversationLayout.visibility = View.VISIBLE
} else {
@ -641,7 +691,7 @@ class ConversationsListActivity :
for ((k, v) in filterState) {
if (v) {
when (k) {
FilterConversationFragment.MENTION -> result = (result && conversation.unreadMention) ||
MENTION -> result = (result && conversation.unreadMention) ||
(
result &&
(
@ -651,10 +701,10 @@ class ConversationsListActivity :
(conversation.unreadMessages > 0)
)
FilterConversationFragment.UNREAD -> result = result && (conversation.unreadMessages > 0)
UNREAD -> result = result && (conversation.unreadMessages > 0)
FilterConversationFragment.DEFAULT -> {
result = if (filterState[FilterConversationFragment.ARCHIVE] == true) {
result = if (filterState[ARCHIVE] == true) {
result && conversation.hasArchived
} else {
result && !conversation.hasArchived
@ -734,7 +784,7 @@ class ConversationsListActivity :
}
private fun initSearchView() {
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager?
if (searchItem != null) {
searchView = MenuItemCompat.getActionView(searchItem) as SearchView
viewThemeUtils.talk.themeSearchView(searchView!!)
@ -1150,8 +1200,8 @@ class ConversationsListActivity :
}
}
private fun fetchUsers() {
contactsViewModel.getContactsFromSearchParams()
private fun fetchUsers(query: String = "") {
contactsViewModel.getContactsFromSearchParams(query)
}
private fun handleHttpExceptions(throwable: Throwable) {
@ -1196,7 +1246,7 @@ class ConversationsListActivity :
})
binding.recyclerView.setOnTouchListener { v: View, _: MotionEvent? ->
if (!isDestroyed) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v.windowToken, 0)
}
false
@ -1349,6 +1399,9 @@ class ConversationsListActivity :
private fun performFilterAndSearch(filter: String?) {
if (filter!!.length >= SEARCH_MIN_CHARS) {
clearMessageSearchResults()
binding.noArchivedConversationLayout.visibility = View.GONE
fetchUsers(filter)
if (hasFilterEnabled()) {
adapter?.updateDataSet(conversationItems)
@ -1356,6 +1409,7 @@ class ConversationsListActivity :
adapter?.filterItems()
adapter?.updateDataSet(filterableConversationItems)
} else {
adapter?.updateDataSet(searchableConversationItems)
adapter?.setFilter(filter)
adapter?.filterItems()
}
@ -1370,8 +1424,15 @@ class ConversationsListActivity :
private fun resetSearchResults() {
clearMessageSearchResults()
adapter?.updateDataSet(conversationItems)
adapter?.setFilter("")
adapter?.filterItems()
val archiveFilterOn = filterState[ARCHIVE] == true
if (archiveFilterOn && adapter!!.isEmpty) {
binding.noArchivedConversationLayout.visibility = View.VISIBLE
} else {
binding.noArchivedConversationLayout.visibility = View.GONE
}
}
private fun clearMessageSearchResults() {
@ -1380,6 +1441,7 @@ class ConversationsListActivity :
adapter?.removeSection(firstHeader)
} else {
adapter?.removeItemsOfType(MessageResultItem.VIEW_TYPE)
adapter?.removeItemsOfType(MessagesTextHeaderItem.VIEW_TYPE)
}
adapter?.removeItemsOfType(LoadMoreResultsItem.VIEW_TYPE)
}
@ -1776,7 +1838,7 @@ class ConversationsListActivity :
val callsChannelNotEnabled = !NotificationUtils.isCallsNotificationChannelEnabled(this)
val serverNotificationAppInstalled =
currentUser?.capabilities?.notificationsCapability?.features?.isNotEmpty() ?: false
currentUser?.capabilities?.notificationsCapability?.features?.isNotEmpty() == true
val settingsOfUserAreWrong = notificationPermissionNotGranted ||
batteryOptimizationNotIgnored ||
@ -2077,29 +2139,25 @@ class ConversationsListActivity :
val entries = results.messages
if (entries.isNotEmpty()) {
val adapterItems: MutableList<AbstractFlexibleItem<*>> = ArrayList(entries.size + 1)
for (i in entries.indices) {
val showHeader = i == 0
adapterItems.add(
MessageResultItem(
context,
currentUser!!,
entries[i],
showHeader,
viewThemeUtils = viewThemeUtils
)
)
}
if (results.hasMore) {
adapterItems.add(LoadMoreResultsItem)
}
adapter?.addItems(Int.MAX_VALUE, adapterItems)
val pos = adapter?.currentItems?.indexOfFirst {
it is MessageResultItem
}
val item = (adapter?.currentItems?.get(pos!!) as MessageResultItem).apply { showHeader = true }
adapter?.addItem(pos!!, item)
adapter?.notifyItemInserted(pos!!)
adapter?.removeItem(pos!! - 1)
adapter?.notifyItemRemoved(pos!! - 1)
binding.recyclerView.scrollToPosition(0)
}
}
@ -2112,8 +2170,8 @@ class ConversationsListActivity :
}
fun updateFilterState(mention: Boolean, unread: Boolean) {
filterState[FilterConversationFragment.MENTION] = mention
filterState[FilterConversationFragment.UNREAD] = unread
filterState[MENTION] = mention
filterState[UNREAD] = unread
}
fun setFilterableItems(items: MutableList<AbstractFlexibleItem<*>>) {
@ -2152,7 +2210,8 @@ class ConversationsListActivity :
const val NOTIFICATION_WARNING_DATE_NOT_SET = 0L
const val OFFSET_HEIGHT_DIVIDER: Int = 3
const val ROOM_TYPE_ONE_ONE = "1"
private const val AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS: Long = 57600
private const val SIXTEEN_HOURS_IN_SECONDS: Long = 57600
const val LONG_1000: Long = 1000
private const val NOTE_TO_SELF_SHORTCUT_ID = "NOTE_TO_SELF_SHORTCUT_ID"
}
}

View File

@ -50,6 +50,18 @@ interface ChatMessagesDao {
)
fun getTempMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>>
@Query(
"""
SELECT *
FROM ChatMessages
WHERE internalConversationId = :internalConversationId
AND isTemporary = 1
AND sendStatus != 'SENT_PENDING_ACK'
ORDER BY timestamp DESC, id DESC
"""
)
fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>>
@Query(
"""
SELECT *

View File

@ -68,7 +68,7 @@ fun ChatMessageEntity.asModel() =
isDeleted = deleted,
referenceId = referenceId,
isTemporary = isTemporary,
sendingFailed = sendingFailed,
sendStatus = sendStatus,
readStatus = ReadStatus.NONE,
silent = silent
)

View File

@ -61,7 +61,9 @@ fun ConversationModel.asEntity() =
recordingConsentRequired = recordingConsentRequired,
remoteServer = remoteServer,
remoteToken = remoteToken,
hasArchived = hasArchived
hasArchived = hasArchived,
hasSensitive = hasSensitive,
hasImportant = hasImportant
)
fun ConversationEntity.asModel() =
@ -113,7 +115,9 @@ fun ConversationEntity.asModel() =
recordingConsentRequired = recordingConsentRequired,
remoteServer = remoteServer,
remoteToken = remoteToken,
hasArchived = hasArchived
hasArchived = hasArchived,
hasSensitive = hasSensitive,
hasImportant = hasImportant
)
fun Conversation.asEntity(accountId: Long) =
@ -164,5 +168,7 @@ fun Conversation.asEntity(accountId: Long) =
recordingConsentRequired = recordingConsentRequired,
remoteServer = remoteServer,
remoteToken = remoteToken,
hasArchived = hasArchived
hasArchived = hasArchived,
hasSensitive = hasSensitive,
hasImportant = hasImportant
)

View File

@ -64,7 +64,7 @@ data class ChatMessageEntity(
@ColumnInfo(name = "reactions") var reactions: LinkedHashMap<String, Int>? = null,
@ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList<String>? = null,
@ColumnInfo(name = "referenceId") var referenceId: String? = null,
@ColumnInfo(name = "sendingFailed") var sendingFailed: Boolean = false,
@ColumnInfo(name = "sendStatus") var sendStatus: SendStatus? = null,
@ColumnInfo(name = "silent") var silent: Boolean = false,
@ColumnInfo(name = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType,
@ColumnInfo(name = "timestamp") var timestamp: Long = 0

View File

@ -94,7 +94,9 @@ data class ConversationEntity(
@ColumnInfo(name = "unreadMention") var unreadMention: Boolean = false,
@ColumnInfo(name = "unreadMentionDirect") var unreadMentionDirect: Boolean,
@ColumnInfo(name = "unreadMessages") var unreadMessages: Int = 0,
@ColumnInfo(name = "hasArchived") var hasArchived: Boolean = false
@ColumnInfo(name = "hasArchived") var hasArchived: Boolean = false,
@ColumnInfo(name = "hasSensitive") var hasSensitive: Boolean = false,
@ColumnInfo(name = "hasImportant") var hasImportant: Boolean = false
// missing/not needed: attendeeId
// missing/not needed: attendeePin
// missing/not needed: attendeePermissions

View File

@ -0,0 +1,14 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.data.database.model
enum class SendStatus {
PENDING,
SENT_PENDING_ACK,
FAILED
}

View File

@ -1,18 +1,31 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024-2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.data.source.local
import android.util.Log
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import java.sql.SQLException
@Suppress("MagicNumber")
object Migrations {
//region Auto migrations
@DeleteColumn(tableName = "ChatMessages", columnName = "sendingFailed")
class AutoMigration16To17 : AutoMigrationSpec
//endregion
//region Manual migrations
val MIGRATION_6_8 = object : Migration(6, 8) {
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 6 to 8")
@ -62,6 +75,22 @@ object Migrations {
}
}
val MIGRATION_14_15 = object : Migration(14, 15) {
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 14 to 15")
addIsSensitive(db)
}
}
val MIGRATION_15_16 = object : Migration(15, 16) {
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 15 to 16")
addIsImportant(db)
}
}
//endregion
fun migrateToRoom(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE User_new (" +
@ -283,6 +312,28 @@ object Migrations {
}
}
fun addIsSensitive(db: SupportSQLiteDatabase) {
try {
db.execSQL(
"ALTER TABLE Conversations " +
"ADD COLUMN hasSensitive INTEGER NOT NULL DEFAULT 0;"
)
} catch (e: SQLException) {
Log.i("Migrations", "Something went wrong when adding column hasSensitive to table Conversations")
}
}
fun addIsImportant(db: SupportSQLiteDatabase) {
try {
db.execSQL(
"ALTER TABLE Conversations " +
"ADD COLUMN hasImportant INTEGER NOT NULL DEFAULT 0;"
)
} catch (e: SQLException) {
Log.i("Migrations", "Something went wrong when adding column hasImportant to table Conversations")
}
}
fun addTempMessagesSupport(db: SupportSQLiteDatabase) {
try {
db.execSQL(

View File

@ -1,7 +1,7 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2023-2024 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2023-2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2020 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later
@ -23,12 +23,14 @@ import com.nextcloud.talk.data.database.dao.ConversationsDao
import com.nextcloud.talk.data.database.model.ChatBlockEntity
import com.nextcloud.talk.data.database.model.ChatMessageEntity
import com.nextcloud.talk.data.database.model.ConversationEntity
import com.nextcloud.talk.data.source.local.Migrations.AutoMigration16To17
import com.nextcloud.talk.data.source.local.converters.ArrayListConverter
import com.nextcloud.talk.data.source.local.converters.CapabilitiesConverter
import com.nextcloud.talk.data.source.local.converters.ExternalSignalingServerConverter
import com.nextcloud.talk.data.source.local.converters.HashMapHashMapConverter
import com.nextcloud.talk.data.source.local.converters.LinkedHashMapConverter
import com.nextcloud.talk.data.source.local.converters.PushConfigurationConverter
import com.nextcloud.talk.data.source.local.converters.SendStatusConverter
import com.nextcloud.talk.data.source.local.converters.ServerVersionConverter
import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter
import com.nextcloud.talk.data.storage.ArbitraryStoragesDao
@ -49,9 +51,10 @@ import java.util.Locale
ChatMessageEntity::class,
ChatBlockEntity::class
],
version = 14,
version = 17,
autoMigrations = [
AutoMigration(from = 9, to = 10)
AutoMigration(from = 9, to = 10),
AutoMigration(from = 16, to = 17, spec = AutoMigration16To17::class)
],
exportSchema = true
)
@ -63,7 +66,8 @@ import java.util.Locale
SignalingSettingsConverter::class,
HashMapHashMapConverter::class,
LinkedHashMapConverter::class,
ArrayListConverter::class
ArrayListConverter::class,
SendStatusConverter::class
)
abstract class TalkDatabase : RoomDatabase() {
@ -116,7 +120,9 @@ abstract class TalkDatabase : RoomDatabase() {
Migrations.MIGRATION_10_11,
Migrations.MIGRATION_11_12,
Migrations.MIGRATION_12_13,
Migrations.MIGRATION_13_14
Migrations.MIGRATION_13_14,
Migrations.MIGRATION_14_15,
Migrations.MIGRATION_15_16
)
.allowMainThreadQueries()
.addCallback(

View File

@ -0,0 +1,23 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.data.source.local.converters
import androidx.room.TypeConverter
import com.nextcloud.talk.data.database.model.SendStatus
class SendStatusConverter {
@TypeConverter
fun fromStatus(value: SendStatus): String {
return value.name
}
@TypeConverter
fun toStatus(value: String): SendStatus {
return SendStatus.valueOf(value)
}
}

View File

@ -190,6 +190,7 @@ class FullScreenMediaActivity : AppCompatActivity() {
supportActionBar?.show()
}
@OptIn(UnstableApi::class)
private fun applyWindowInsets() {
val playerView = binding.playerView
val exoControls = playerView.findViewById<FrameLayout>(R.id.exo_bottom_bar)

View File

@ -61,6 +61,8 @@ class ConversationModel(
var remoteServer: String? = null,
var remoteToken: String? = null,
var hasArchived: Boolean = false,
var hasSensitive: Boolean = false,
var hasImportant: Boolean = false,
// attributes that don't come from API. This should be changed?!
var password: String? = null
@ -125,7 +127,9 @@ class ConversationModel(
recordingConsentRequired = conversation.recordingConsentRequired,
remoteServer = conversation.remoteServer,
remoteToken = conversation.remoteToken,
hasArchived = conversation.hasArchived
hasArchived = conversation.hasArchived,
hasSensitive = conversation.hasSensitive,
hasImportant = conversation.hasImportant
)
}
}

View File

@ -21,7 +21,7 @@ class ChatUtils {
return message
}
@Suppress("Detekt.ComplexMethod")
@Suppress("Detekt.ComplexMethod", "Detekt.ComplexCondition")
private fun parse(messageParameters: HashMap<String?, HashMap<String?, String?>>, message: String?): String? {
var resultMessage = message
for (key in messageParameters.keys) {
@ -29,7 +29,9 @@ class ChatUtils {
if (individualHashMap != null) {
val type = individualHashMap["type"]
resultMessage = if (type == "user" || type == "guest" || type == "call" || type == "email") {
resultMessage = if (type == "user" || type == "guest" || type == "call" || type == "email" ||
type == "user-group" || type == "circle"
) {
resultMessage?.replace("{$key}", "@" + individualHashMap["name"])
} else if (type == "geo-location") {
individualHashMap["name"]

View File

@ -165,5 +165,11 @@ data class Conversation(
var remoteToken: String? = "",
@JsonField(name = ["isArchived"])
var hasArchived: Boolean = false
var hasArchived: Boolean = false,
@JsonField(name = ["isSensitive"])
var hasSensitive: Boolean = false,
@JsonField(name = ["isImportant"])
var hasImportant: Boolean = false
) : Parcelable

View File

@ -44,6 +44,9 @@ class ConversationEnums {
SHARE_PASSWORD,
FILE,
ROOM,
EVENT
EVENT,
PHONE_TEMPORARY,
PHONE_PERSIST,
INSTANT_MEETING
}
}

View File

@ -16,6 +16,9 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter<ConversationEnu
"room" -> ConversationEnums.ObjectType.ROOM
"file" -> ConversationEnums.ObjectType.FILE
"event" -> ConversationEnums.ObjectType.EVENT
"phone_persist" -> ConversationEnums.ObjectType.PHONE_PERSIST
"phone_temporary" -> ConversationEnums.ObjectType.PHONE_TEMPORARY
"instant_meeting" -> ConversationEnums.ObjectType.INSTANT_MEETING
else -> ConversationEnums.ObjectType.DEFAULT
}
}
@ -30,6 +33,9 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter<ConversationEnu
ConversationEnums.ObjectType.ROOM -> "room"
ConversationEnums.ObjectType.FILE -> "file"
ConversationEnums.ObjectType.EVENT -> "event"
ConversationEnums.ObjectType.PHONE_PERSIST -> "phone_persist"
ConversationEnums.ObjectType.PHONE_TEMPORARY -> "phone_temporary"
ConversationEnums.ObjectType.INSTANT_MEETING -> "instant_meeting"
else -> ""
}
}

View File

@ -0,0 +1,29 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.profile
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class CoreProfileAction(
@JsonField(name = ["id"])
var id: String? = null,
@JsonField(name = ["icon"])
var icon: String? = null,
@JsonField(name = ["title"])
var title: String? = null,
@JsonField(name = ["target"])
var target: String? = null
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null, null, null)
}

View File

@ -0,0 +1,30 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.profile
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class Profile(
@JsonField(name = ["userId"]) var userId: String? = null,
@JsonField(name = ["address"]) var address: String? = null,
@JsonField(name = ["biography"]) var biography: Int? = null,
@JsonField(name = ["displayname"]) var displayName: Int? = null,
@JsonField(name = ["headline"]) var headline: String? = null,
// @JsonField(name = ["isUserAvatarVisible"]) var isUserAvatarVisible: Boolean = false,
@JsonField(name = ["organisation"]) var company: String? = null,
@JsonField(name = ["pronouns"]) var pronouns: String? = null,
@JsonField(name = ["role"]) var role: String? = null,
@JsonField(name = ["actions"]) var actions: List<CoreProfileAction>? = null,
@JsonField(name = ["timezone"]) var timezone: String? = null,
@JsonField(name = ["timezoneOffset"]) var timezoneOffset: Int? = null
) : Parcelable

View File

@ -0,0 +1,26 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.profile
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import com.nextcloud.talk.models.json.generic.GenericMeta
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class ProfileOCS(
@JsonField(name = ["meta"])
var meta: GenericMeta? = null,
@JsonField(name = ["data"])
var data: Profile? = null
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null)
}

View File

@ -0,0 +1,23 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.models.json.profile
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class ProfileOverall(
@JsonField(name = ["ocs"])
var ocs: ProfileOCS? = null
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null)
}

View File

@ -6,14 +6,17 @@
*/
package com.nextcloud.talk.receivers
import android.Manifest
import android.app.Notification
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
@ -162,9 +165,15 @@ class DirectReplyReceiver : BroadcastReceiver() {
// Set the updated style
previousBuilder.setStyle(previousStyle)
// Check if notification still exists
if (findActiveNotification(systemNotificationId!!) != null) {
NotificationManagerCompat.from(context).notify(systemNotificationId!!, previousBuilder.build())
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
// Check if notification still exists
if (findActiveNotification(systemNotificationId!!) != null) {
NotificationManagerCompat.from(context).notify(systemNotificationId!!, previousBuilder.build())
}
}
}
.subscribeOn(Schedulers.io())

View File

@ -11,6 +11,7 @@ import com.nextcloud.talk.conversationinfo.CreateRoomRequest
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.profile.Profile
import io.reactivex.Observable
interface ConversationsRepository {
@ -46,4 +47,14 @@ interface ConversationsRepository {
suspend fun clearChatHistory(apiVersion: Int, roomToken: String): GenericOverall
suspend fun createRoom(credentials: String, url: String, body: CreateRoomRequest): RoomOverall
suspend fun getProfile(credentials: String, url: String): Profile?
suspend fun markConversationAsSensitive(credentials: String, baseUrl: String, roomToken: String): GenericOverall
suspend fun markConversationAsInsensitive(credentials: String, baseUrl: String, roomToken: String): GenericOverall
suspend fun markConversationAsImportant(credentials: String, baseUrl: String, roomToken: String): GenericOverall
suspend fun markConversationAsUnImportant(credentials: String, baseUrl: String, roomToken: String): GenericOverall
}

View File

@ -14,6 +14,7 @@ import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.participants.TalkBan
import com.nextcloud.talk.models.json.profile.Profile
import com.nextcloud.talk.repositories.conversations.ConversationsRepository.ResendInvitationsResult
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
@ -116,6 +117,46 @@ class ConversationsRepositoryImpl(
return response
}
override suspend fun getProfile(credentials: String, url: String): Profile? {
return coroutineApi.getProfile(credentials, url).ocs?.data
}
override suspend fun markConversationAsSensitive(
credentials: String,
baseUrl: String,
roomToken: String
): GenericOverall {
val url = ApiUtils.getUrlForSensitiveConversation(baseUrl, roomToken)
return coroutineApi.markConversationAsSensitive(credentials, url)
}
override suspend fun markConversationAsInsensitive(
credentials: String,
baseUrl: String,
roomToken: String
): GenericOverall {
val url = ApiUtils.getUrlForSensitiveConversation(baseUrl, roomToken)
return coroutineApi.markConversationAsInsensitive(credentials, url)
}
override suspend fun markConversationAsImportant(
credentials: String,
baseUrl: String,
roomToken: String
): GenericOverall {
val url = ApiUtils.getUrlForImportantConversation(baseUrl, roomToken)
return coroutineApi.markConversationAsImportant(credentials, url)
}
override suspend fun markConversationAsUnImportant(
credentials: String,
baseUrl: String,
roomToken: String
): GenericOverall {
val url = ApiUtils.getUrlForImportantConversation(baseUrl, roomToken)
return coroutineApi.markConversationAsUnimportant(credentials, url)
}
override suspend fun banActor(
credentials: String,
url: String,

View File

@ -8,6 +8,7 @@
package com.nextcloud.talk.ui
import android.content.Context
import android.content.ContextWrapper
import android.util.Log
import android.view.View.TEXT_ALIGNMENT_VIEW_START
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
@ -23,6 +24,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
@ -46,6 +48,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -68,15 +71,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.graphics.ColorUtils
import androidx.emoji2.widget.EmojiTextView
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
import autodagger.AutoInjector
@ -84,6 +90,8 @@ import coil.compose.AsyncImage
import com.elyeproj.loaderviewlibrary.LoaderImageView
import com.elyeproj.loaderviewlibrary.LoaderTextView
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder.Companion.KEY_MIMETYPE
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.chat.data.model.ChatMessage
@ -99,7 +107,9 @@ import com.nextcloud.talk.models.json.opengraph.Reference
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.DateUtils
import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preview.ComposePreviewUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import org.osmdroid.config.Configuration
@ -116,41 +126,54 @@ import kotlin.random.Random
@Suppress("FunctionNaming", "TooManyFunctions", "LongMethod", "StaticFieldLeak", "LargeClass")
class ComposeChatAdapter(
private var messagesJson: List<ChatMessageJson>? = null,
private var messageId: String? = null
private var messageId: String? = null,
private val utils: ComposePreviewUtils? = null
) {
interface PreviewAble {
val viewThemeUtils: ViewThemeUtils
val messageUtils: MessageUtils
val contactsViewModel: ContactsViewModel
val chatViewModel: ChatViewModel
val context: Context
val userManager: UserManager
}
@AutoInjector(NextcloudTalkApplication::class)
inner class ComposeChatAdapterViewModel : ViewModel() {
inner class ComposeChatAdapterViewModel : ViewModel(), PreviewAble {
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
override lateinit var viewThemeUtils: ViewThemeUtils
@Inject
lateinit var messageUtils: MessageUtils
override lateinit var messageUtils: MessageUtils
@Inject
lateinit var contactsViewModel: ContactsViewModel
override lateinit var contactsViewModel: ContactsViewModel
@Inject
lateinit var chatViewModel: ChatViewModel
override lateinit var chatViewModel: ChatViewModel
@Inject
lateinit var context: Context
override lateinit var context: Context
@Inject
lateinit var userManager: UserManager
val items = mutableStateListOf<ChatMessage>()
override lateinit var userManager: UserManager
init {
sharedApplication!!.componentApplication.inject(this)
sharedApplication?.componentApplication?.inject(this)
}
val currentUser: User = userManager.currentUser.blockingGet()
val colorScheme = viewThemeUtils.getColorScheme(context)
val highEmphasisColorInt = context.resources.getColor(R.color.high_emphasis_text, null)
}
inner class ComposeChatAdapterPreviewViewModel(
override val viewThemeUtils: ViewThemeUtils,
override val messageUtils: MessageUtils,
override val contactsViewModel: ContactsViewModel,
override val chatViewModel: ChatViewModel,
override val context: Context,
override val userManager: UserManager
) : ViewModel(), PreviewAble
companion object {
val TAG: String = ComposeChatAdapter::class.java.simpleName
private val REGULAR_TEXT_SIZE = 16.sp
@ -173,21 +196,48 @@ class ComposeChatAdapter(
private var incomingShape: RoundedCornerShape = RoundedCornerShape(2.dp, 20.dp, 20.dp, 20.dp)
private var outgoingShape: RoundedCornerShape = RoundedCornerShape(20.dp, 2.dp, 20.dp, 20.dp)
private val viewModel = ComposeChatAdapterViewModel()
val viewModel: PreviewAble =
if (utils != null) {
ComposeChatAdapterPreviewViewModel(
utils.viewThemeUtils,
utils.messageUtils,
utils.contactsViewModel,
utils.chatViewModel,
utils.context,
utils.userManager
)
} else {
ComposeChatAdapterViewModel()
}
val items = mutableStateListOf<ChatMessage>()
val currentUser: User = viewModel.userManager.currentUser.blockingGet()
val colorScheme = viewModel.viewThemeUtils.getColorScheme(viewModel.context)
val highEmphasisColorInt = viewModel.context.resources.getColor(R.color.high_emphasis_text, null)
fun Context.findMainActivityOrNull(): MainActivity? {
var context = this
while (context is ContextWrapper) {
if (context is MainActivity) return context
context = context.baseContext
}
return null
}
fun addMessages(messages: MutableList<ChatMessage>, append: Boolean) {
if (messages.isEmpty()) return
val processedMessages = messages.toMutableList()
if (viewModel.items.isNotEmpty()) {
if (items.isNotEmpty()) {
if (append) {
processedMessages.add(viewModel.items.first())
processedMessages.add(items.first())
} else {
processedMessages.add(viewModel.items.last())
processedMessages.add(items.last())
}
}
if (append) viewModel.items.addAll(processedMessages) else viewModel.items.addAll(0, processedMessages)
if (append) items.addAll(processedMessages) else items.addAll(0, processedMessages)
}
@OptIn(ExperimentalFoundationApi::class)
@ -202,7 +252,7 @@ class ComposeChatAdapter(
modifier = Modifier.padding(16.dp)
) {
stickyHeader {
if (viewModel.items.size == 0) {
if (items.size == 0) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
@ -211,11 +261,11 @@ class ComposeChatAdapter(
ShimmerGroup()
}
} else {
val timestamp = viewModel.items[listState.firstVisibleItemIndex].timestamp
val timestamp = items[listState.firstVisibleItemIndex].timestamp
val dateString = formatTime(timestamp * LONG_1000)
val color = Color(viewModel.highEmphasisColorInt)
val color = Color(highEmphasisColorInt)
val backgroundColor =
viewModel.context.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
Row(
horizontalArrangement = Arrangement.Absolute.Center,
verticalAlignment = Alignment.CenterVertically
@ -229,8 +279,8 @@ class ComposeChatAdapter(
.padding(8.dp)
.shadow(
16.dp,
spotColor = viewModel.colorScheme.primary,
ambientColor = viewModel.colorScheme.primary
spotColor = colorScheme.primary,
ambientColor = colorScheme.primary
)
.background(color = Color(backgroundColor), shape = RoundedCornerShape(8.dp))
.padding(8.dp)
@ -240,8 +290,8 @@ class ComposeChatAdapter(
}
}
items(viewModel.items) { message ->
message.activeUser = viewModel.currentUser
items(items) { message ->
message.activeUser = currentUser
when (val type = message.getCalculateMessageType()) {
ChatMessage.MessageType.SYSTEM_MESSAGE -> {
if (!message.shouldFilter()) {
@ -284,7 +334,7 @@ class ComposeChatAdapter(
}
}
if (messageId != null && viewModel.items.size > 0) {
if (messageId != null && items.size > 0) {
LaunchedEffect(Dispatchers.Main) {
delay(SCROLL_DELAY)
val pos = searchMessages(messageId!!)
@ -326,7 +376,7 @@ class ComposeChatAdapter(
}
private fun searchMessages(searchId: String): Int {
viewModel.items.forEachIndexed { index, message ->
items.forEachIndexed { index, message ->
if (message.id == searchId) return index
}
return -1
@ -380,18 +430,18 @@ class ComposeChatAdapter(
@Composable
(RowScope.() -> Unit)
) {
val incoming = message.actorId != viewModel.currentUser.userId
val incoming = message.actorId != currentUser.userId
val color = if (incoming) {
if (message.isDeleted) {
viewModel.context.resources.getColor(R.color.bg_message_list_incoming_bubble_deleted, null)
LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble_deleted, null)
} else {
viewModel.context.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
LocalContext.current.resources.getColor(R.color.bg_message_list_incoming_bubble, null)
}
} else {
if (message.isDeleted) {
ColorUtils.setAlphaComponent(viewModel.colorScheme.surfaceVariant.toArgb(), HALF_OPACITY)
ColorUtils.setAlphaComponent(colorScheme.surfaceVariant.toArgb(), HALF_OPACITY)
} else {
viewModel.colorScheme.surfaceVariant.toArgb()
colorScheme.surfaceVariant.toArgb()
}
}
val shape = if (incoming) incomingShape else outgoingShape
@ -405,7 +455,7 @@ class ComposeChatAdapter(
if (incoming) {
val imageUri = message.actorId?.let { viewModel.contactsViewModel.getImageUri(it, true) }
val errorPlaceholderImage: Int = R.drawable.account_circle_96dp
val loadedImage = loadImage(imageUri, viewModel.context, errorPlaceholderImage)
val loadedImage = loadImage(imageUri, LocalContext.current, errorPlaceholderImage)
AsyncImage(
model = loadedImage,
contentDescription = stringResource(R.string.user_avatar),
@ -427,13 +477,13 @@ class ComposeChatAdapter(
color = Color(color),
shape = shape
) {
val timeString = DateUtils(viewModel.context).getLocalTimeStringFromTimestamp(message.timestamp)
val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp)
val modifier = if (includePadding) Modifier.padding(8.dp, 4.dp, 8.dp, 4.dp) else Modifier
Column(modifier = modifier) {
if (message.parentMessageId != null && !message.isDeleted && messagesJson != null) {
messagesJson!!
.find { it.parentMessage?.id == message.parentMessageId }
?.parentMessage!!.asModel().let { CommonMessageQuote(viewModel.context, it) }
?.parentMessage!!.asModel().let { CommonMessageQuote(LocalContext.current, it) }
}
if (incoming) {
@ -470,8 +520,8 @@ class ComposeChatAdapter(
private fun Modifier.withCustomAnimation(incoming: Boolean): Modifier {
val infiniteTransition = rememberInfiniteTransition()
val borderColor by infiniteTransition.animateColor(
initialValue = viewModel.colorScheme.primary,
targetValue = viewModel.colorScheme.background,
initialValue = colorScheme.primary,
targetValue = colorScheme.background,
animationSpec = infiniteRepeatable(
animation = tween(ANIMATED_BLINK, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
@ -542,7 +592,7 @@ class ComposeChatAdapter(
LoaderTextView(ctx).apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
val color = if (outgoing) {
viewModel.colorScheme.primary.toArgb()
colorScheme.primary.toArgb()
} else {
resources.getColor(R.color.nc_shimmer_default_color, null)
}
@ -562,7 +612,7 @@ class ComposeChatAdapter(
@Composable
private fun EnrichedText(message: ChatMessage) {
AndroidView(factory = { ctx ->
val incoming = message.actorId != viewModel.currentUser.userId
val incoming = message.actorId != currentUser.userId
var processedMessageText = viewModel.messageUtils.enrichChatMessageText(
ctx,
message,
@ -574,7 +624,7 @@ class ComposeChatAdapter(
ctx, viewModel.viewThemeUtils, processedMessageText!!, message, null
)
androidx.emoji2.widget.EmojiTextView(ctx).apply {
EmojiTextView(ctx).apply {
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
setLineSpacing(0F, LINE_SPACING)
textAlignment = TEXT_ALIGNMENT_VIEW_START
@ -592,14 +642,14 @@ class ComposeChatAdapter(
}
@Composable
private fun SystemMessage(message: ChatMessage) {
fun SystemMessage(message: ChatMessage) {
val similarMessages = sharedApplication!!.resources.getQuantityString(
R.plurals.see_similar_system_messages,
message.expandableChildrenAmount,
message.expandableChildrenAmount
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val timeString = DateUtils(viewModel.context).getLocalTimeStringFromTimestamp(message.timestamp)
val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp)
Row(horizontalArrangement = Arrangement.Absolute.Center, verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.weight(1f))
Text(
@ -632,7 +682,7 @@ class ComposeChatAdapter(
Text(
text,
fontSize = AUTHOR_TEXT_SIZE,
color = Color(viewModel.highEmphasisColorInt)
color = Color(highEmphasisColorInt)
)
}
}
@ -640,14 +690,15 @@ class ComposeChatAdapter(
@Composable
private fun ImageMessage(message: ChatMessage, state: MutableState<Boolean>) {
val hasCaption = (message.message != "{file}")
val incoming = message.actorId != viewModel.currentUser.userId
val timeString = DateUtils(viewModel.context).getLocalTimeStringFromTimestamp(message.timestamp)
val incoming = message.actorId != currentUser.userId
val timeString = DateUtils(LocalContext.current).getLocalTimeStringFromTimestamp(message.timestamp)
CommonMessageBody(message, includePadding = false, playAnimation = state.value) {
Column {
message.activeUser = viewModel.currentUser
message.activeUser = currentUser
val imageUri = message.imageUrl
val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image
val loadedImage = load(imageUri, viewModel.context, errorPlaceholderImage)
val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE]
val drawableResourceId = getDrawableResourceIdForMimeType(mimetype)
val loadedImage = load(imageUri, LocalContext.current, drawableResourceId)
AsyncImage(
model = loadedImage,
@ -717,8 +768,8 @@ class ComposeChatAdapter(
WaveformSeekBar(ctx).apply {
setWaveData(FloatArray(DEFAULT_WAVE_SIZE) { Random.nextFloat() }) // READ ONLY for now
setColors(
viewModel.colorScheme.inversePrimary.toArgb(),
viewModel.colorScheme.onPrimaryContainer.toArgb()
colorScheme.inversePrimary.toArgb(),
colorScheme.onPrimaryContainer.toArgb()
)
}
},
@ -793,8 +844,8 @@ class ComposeChatAdapter(
private fun LinkMessage(message: ChatMessage, state: MutableState<Boolean>) {
val color = colorResource(R.color.high_emphasis_text)
viewModel.chatViewModel.getOpenGraph(
viewModel.currentUser.getCredentials(),
viewModel.currentUser.baseUrl!!,
currentUser.getCredentials(),
currentUser.baseUrl!!,
message.extractedUrlToPreview!!
)
CommonMessageBody(message, playAnimation = state.value) {
@ -828,7 +879,7 @@ class ComposeChatAdapter(
it.link?.let { Text(it, fontSize = TIME_TEXT_SIZE) }
it.thumb?.let {
val errorPlaceholderImage: Int = R.drawable.ic_mimetype_image
val loadedImage = loadImage(it, viewModel.context, errorPlaceholderImage)
val loadedImage = loadImage(it, LocalContext.current, errorPlaceholderImage)
AsyncImage(
model = loadedImage,
contentDescription = stringResource(R.string.nc_sent_an_image),
@ -882,7 +933,7 @@ class ComposeChatAdapter(
if (cardName?.isNotEmpty() == true) {
val cardDescription = String.format(
viewModel.context.resources.getString(R.string.deck_card_description),
LocalContext.current.resources.getString(R.string.deck_card_description),
stackName,
boardName
)
@ -899,3 +950,44 @@ class ComposeChatAdapter(
}
}
}
@Preview(showBackground = true, widthDp = 380, heightDp = 800)
@Composable
fun AllMessageTypesPreview() {
val previewUtils = ComposePreviewUtils.getInstance(LocalContext.current)
val adapter = remember { ComposeChatAdapter(messagesJson = null, messageId = null, previewUtils) }
val sampleMessages = remember {
listOf(
// Text Messages
ChatMessage().apply {
jsonMessageId = 1
actorId = "user1"
message = "I love Nextcloud"
timestamp = System.currentTimeMillis()
actorDisplayName = "User1"
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
},
ChatMessage().apply {
jsonMessageId = 2
actorId = "user1_id"
message = "I love Nextcloud"
timestamp = System.currentTimeMillis()
actorDisplayName = "User2"
messageType = ChatMessage.MessageType.REGULAR_TEXT_MESSAGE.name
}
)
}
LaunchedEffect(sampleMessages) { // Use LaunchedEffect or similar to update state once
if (adapter.items.isEmpty()) { // Prevent adding multiple times on recomposition
adapter.addMessages(sampleMessages.toMutableList(), append = false) // Add messages
}
}
MaterialTheme(colorScheme = adapter.colorScheme) { // Use the (potentially faked) color scheme
Box(modifier = Modifier.fillMaxSize()) { // Provide a container
adapter.GetView() // Call the main Composable
}
}
}

View File

@ -7,14 +7,16 @@
package com.nextcloud.talk.ui.dialog
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -42,9 +44,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -94,6 +94,15 @@ class ContextChatCompose(val bundle: Bundle) {
}
}
private fun Context.requireActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
throw IllegalStateException("No activity was present but it is required.")
}
@Composable
fun GetDialogView(
shouldDismiss: MutableState<Boolean>,
@ -101,9 +110,11 @@ class ContextChatCompose(val bundle: Bundle) {
contextViewModel: ContextChatComposeViewModel = ContextChatComposeViewModel()
) {
if (shouldDismiss.value) {
context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
return
}
context.requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
val colorScheme = contextViewModel.viewThemeUtils.getColorScheme(context)
MaterialTheme(colorScheme) {
Dialog(
@ -144,28 +155,28 @@ class ContextChatCompose(val bundle: Bundle) {
val name = bundle.getString(BundleKeys.KEY_CONVERSATION_NAME)!!
Text(name, fontSize = 24.sp)
}
Spacer(modifier = Modifier.weight(1f))
val cInt = context.resources.getColor(R.color.high_emphasis_text, null)
Icon(
painterResource(R.drawable.ic_call_black_24dp),
"",
tint = Color(cInt),
modifier = Modifier
.padding()
.padding(end = 16.dp)
.alpha(HALF_ALPHA)
)
Icon(
painterResource(R.drawable.ic_baseline_videocam_24),
"",
tint = Color(cInt),
modifier = Modifier
.padding()
.alpha(HALF_ALPHA)
)
ComposeChatMenu(colorScheme.background, false)
// Spacer(modifier = Modifier.weight(1f))
// val cInt = context.resources.getColor(R.color.high_emphasis_text, null)
// Icon(
// painterResource(R.drawable.ic_call_black_24dp),
// "",
// tint = Color(cInt),
// modifier = Modifier
// .padding()
// .padding(end = 16.dp)
// .alpha(HALF_ALPHA)
// )
//
// Icon(
// painterResource(R.drawable.ic_baseline_videocam_24),
// "",
// tint = Color(cInt),
// modifier = Modifier
// .padding()
// .alpha(HALF_ALPHA)
// )
//
// ComposeChatMenu(colorScheme.background, false)
}
if (shouldShow) {
Icon(

View File

@ -118,7 +118,6 @@ class ConversationsListBottomDialog(
currentUser.capabilities?.spreedCapability!!,
SpreedFeatures.FAVORITES
)
val canModerate = ConversationUtils.canModerate(conversation, currentUser.capabilities?.spreedCapability!!)
binding.conversationRemoveFromFavorites.visibility = setVisibleIf(
hasFavoritesCapability && conversation.favorite
@ -149,14 +148,11 @@ class ConversationsListBottomDialog(
)
binding.conversationOperationDelete.visibility = setVisibleIf(
canModerate
conversation.canDeleteConversation
)
binding.conversationOperationLeave.visibility = setVisibleIf(
conversation.canLeaveConversation &&
// leaving is by api not possible for the last user with moderator permissions.
// for now, hide this option for all moderators.
!ConversationUtils.canModerate(conversation, currentUser.capabilities!!.spreedCapability!!)
conversation.canLeaveConversation
)
}

View File

@ -18,6 +18,7 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.databinding.DialogTempMessageActionsBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils
@ -58,9 +59,10 @@ class TempMessageActionsDialog(
private fun initMenuItems() {
this.lifecycleScope.launch {
initResendMessage(message.sendingFailed && networkMonitor.isOnline.value)
initMenuEditMessage(message.sendingFailed || !networkMonitor.isOnline.value)
initMenuDeleteMessage(message.sendingFailed || !networkMonitor.isOnline.value)
val sendingFailed = message.sendStatus == SendStatus.FAILED
initResendMessage(sendingFailed && networkMonitor.isOnline.value)
initMenuEditMessage(sendingFailed || !networkMonitor.isOnline.value)
initMenuDeleteMessage(sendingFailed || !networkMonitor.isOnline.value)
initMenuItemCopy()
}
}

View File

@ -205,6 +205,10 @@ object ApiUtils {
return getUrlForParticipants(version, baseUrl, token) + "/active"
}
fun getUrlForImportantConversation(baseUrl: String, roomToken: String): String {
return "$baseUrl$OCS_API_VERSION/apps/spreed/api/v4/room/$roomToken/important"
}
@JvmStatic
fun getUrlForParticipantsSelf(version: Int, baseUrl: String?, token: String?): String {
return getUrlForParticipants(version, baseUrl, token) + "/self"
@ -449,6 +453,10 @@ object ApiUtils {
return "$baseUrl$OCS_API_VERSION/cloud/users/search/by-phone"
}
fun getUrlForUnbindingRoom(baseUrl: String, roomToken: String): String {
return "$baseUrl/ocs/v2.php/apps/spreed/api/v4/room/$roomToken/object"
}
fun getUrlForFileUpload(baseUrl: String, user: String, remotePath: String): String {
return "$baseUrl/remote.php/dav/files/$user$remotePath"
}
@ -473,6 +481,10 @@ object ApiUtils {
return "$baseUrl$OCS_API_VERSION/apps/spreed/temp-user-avatar"
}
fun getUrlForSensitiveConversation(baseUrl: String, roomToken: String): String {
return "$baseUrl$OCS_API_VERSION/apps/spreed/api/v4/room/$roomToken/sensitive"
}
fun getUrlForUserFields(baseUrl: String): String {
return "$baseUrl$OCS_API_VERSION/cloud/user/fields"
}
@ -625,4 +637,8 @@ object ApiUtils {
fun getUrlForChatMessageContext(baseUrl: String, token: String, messageId: String): String {
return "$baseUrl$OCS_API_VERSION$SPREED_API_VERSION/chat/$token/$messageId/context"
}
fun getUrlForProfile(baseUrl: String, userId: String): String {
return "$baseUrl$OCS_API_VERSION/profile/$userId"
}
}

View File

@ -57,7 +57,10 @@ enum class SpreedFeatures(val value: String) {
BAN_V1("ban-v1"),
EDIT_MESSAGES_NOTE_TO_SELF("edit-messages-note-to-self"),
ARCHIVE_CONVERSATIONS("archived-conversations-v2"),
CONVERSATION_CREATION_ALL("conversation-creation-all")
CONVERSATION_CREATION_ALL("conversation-creation-all"),
UNBIND_CONVERSATION("unbind-conversation"),
SENSITIVE_CONVERSATIONS("sensitive-conversations"),
IMPORTANT_CONVERSATIONS("important-conversations")
}
@Suppress("TooManyFunctions")
@ -140,6 +143,36 @@ object CapabilitiesUtil {
return false
}
fun retentionOfEventRooms(spreedCapabilities: SpreedCapability): Int {
if (spreedCapabilities.config?.containsKey("conversations") == true) {
val map = spreedCapabilities.config!!["conversations"]
if (map?.containsKey("retention-event") == true) {
return map["retention-event"].toString().toInt()
}
}
return 0
}
fun retentionOfSIPRoom(spreedCapabilities: SpreedCapability): Int {
if (spreedCapabilities.config?.containsKey("conversations") == true) {
val map = spreedCapabilities.config!!["conversations"]
if (map?.containsKey("retention-phone") == true) {
return map["retention-phone"].toString().toInt()
}
}
return 0
}
fun retentionOfInstantMeetingRoom(spreedCapabilities: SpreedCapability): Int {
if (spreedCapabilities.config?.containsKey("conversations") == true) {
val map = spreedCapabilities.config!!["conversations"]
if (map?.containsKey("retention-instant-meetings") == true) {
return map["retention-instant-meetings"].toString().toInt()
}
}
return 0
}
@JvmStatic
fun isCallRecordingAvailable(spreedCapabilities: SpreedCapability): Boolean {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.RECORDING_V1) &&

View File

@ -36,6 +36,7 @@ import com.nextcloud.talk.utils.Mimetype.AUDIO_MPEG
import com.nextcloud.talk.utils.Mimetype.AUDIO_OGG
import com.nextcloud.talk.utils.Mimetype.AUDIO_WAV
import com.nextcloud.talk.utils.Mimetype.IMAGE_GIF
import com.nextcloud.talk.utils.Mimetype.IMAGE_HEIC
import com.nextcloud.talk.utils.Mimetype.IMAGE_JPEG
import com.nextcloud.talk.utils.Mimetype.IMAGE_PNG
import com.nextcloud.talk.utils.Mimetype.TEXT_MARKDOWN
@ -155,7 +156,8 @@ class FileViewerUtils(private val context: Context, private val user: User) {
-> openMediaView(filename, mimetype)
IMAGE_PNG,
IMAGE_JPEG,
IMAGE_GIF
IMAGE_GIF,
IMAGE_HEIC
-> openImageView(filename, mimetype)
TEXT_MARKDOWN,
TEXT_PLAIN
@ -248,6 +250,7 @@ class FileViewerUtils(private val context: Context, private val user: User) {
return when (mimetype) {
IMAGE_PNG,
IMAGE_JPEG,
IMAGE_HEIC,
IMAGE_GIF,
AUDIO_MPEG,
AUDIO_WAV,

View File

@ -22,6 +22,7 @@ object Mimetype {
const val IMAGE_JPEG = "image/jpeg"
const val IMAGE_JPG = "image/jpg"
const val IMAGE_GIF = "image/gif"
const val IMAGE_HEIC = "image/heic"
const val VIDEO_MP4 = "video/mp4"
const val VIDEO_QUICKTIME = "video/quicktime"

View File

@ -81,4 +81,5 @@ object BundleKeys {
const val KEY_FIELD_MAP: String = "KEY_FIELD_MAP"
const val KEY_CHAT_URL: String = "KEY_CHAT_URL"
const val KEY_SCROLL_TO_NOTIFICATION_CATEGORY: String = "KEY_SCROLL_TO_NOTIFICATION_CATEGORY"
const val KEY_FOCUS_INPUT: String = "KEY_FOCUS_INPUT"
}

View File

@ -25,6 +25,7 @@ import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.core.MarkwonTheme
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.ext.tasklist.TaskListDrawable
import io.noties.markwon.ext.tasklist.TaskListPlugin
@ -195,6 +196,7 @@ class MessageUtils(val context: Context) {
}
})
.usePlugin(TaskListPlugin.create(drawable))
.usePlugin(TablePlugin.create { _ -> })
.usePlugin(StrikethroughPlugin.create()).build()
return markwon.toMarkdown(markdown)
}

View File

@ -0,0 +1,188 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.utils.preview
import android.content.Context
import com.github.aurae.retrofit2.LoganSquareConverterFactory
import com.nextcloud.android.common.ui.color.ColorUtil
import com.nextcloud.android.common.ui.theme.MaterialSchemes
import com.nextcloud.android.common.ui.theme.utils.AndroidViewThemeUtils
import com.nextcloud.android.common.ui.theme.utils.AndroidXViewThemeUtils
import com.nextcloud.android.common.ui.theme.utils.DialogViewThemeUtils
import com.nextcloud.android.common.ui.theme.utils.MaterialViewThemeUtils
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.api.NcApiCoroutines
import com.nextcloud.talk.chat.data.ChatMessageRepository
import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager
import com.nextcloud.talk.chat.data.io.MediaRecorderManager
import com.nextcloud.talk.chat.data.network.ChatNetworkDataSource
import com.nextcloud.talk.chat.data.network.OfflineFirstChatRepository
import com.nextcloud.talk.chat.data.network.RetrofitChatNetwork
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.contacts.ContactsRepository
import com.nextcloud.talk.contacts.ContactsRepositoryImpl
import com.nextcloud.talk.contacts.ContactsViewModel
import com.nextcloud.talk.conversationlist.data.OfflineConversationsRepository
import com.nextcloud.talk.conversationlist.data.network.ConversationsNetworkDataSource
import com.nextcloud.talk.conversationlist.data.network.OfflineFirstConversationsRepository
import com.nextcloud.talk.conversationlist.data.network.RetrofitConversationsNetwork
import com.nextcloud.talk.data.database.dao.ChatBlocksDao
import com.nextcloud.talk.data.database.dao.ChatMessagesDao
import com.nextcloud.talk.data.database.dao.ConversationsDao
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.network.NetworkMonitorImpl
import com.nextcloud.talk.data.user.UsersDao
import com.nextcloud.talk.data.user.UsersRepository
import com.nextcloud.talk.data.user.UsersRepositoryImpl
import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.repositories.reactions.ReactionsRepositoryImpl
import com.nextcloud.talk.ui.theme.MaterialSchemesProviderImpl
import com.nextcloud.talk.ui.theme.TalkSpecificViewThemeUtils
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.database.user.CurrentUserProviderImpl
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.nextcloud.talk.utils.preferences.AppPreferencesImpl
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
/**
* TODO - basically a reimplementation of common dependencies for use in Previewing Advanced Compose Views
* It's a hard coded Dependency Injector
*
*/
class ComposePreviewUtils private constructor(context: Context) {
private val mContext = context
companion object {
fun getInstance(context: Context) = ComposePreviewUtils(context)
val TAG: String = ComposePreviewUtils::class.java.simpleName
}
@OptIn(ExperimentalCoroutinesApi::class)
val appPreferences: AppPreferences
get() = AppPreferencesImpl(mContext)
val context: Context = mContext
val userRepository: UsersRepository
get() = UsersRepositoryImpl(usersDao)
val userManager: UserManager
get() = UserManager(userRepository)
val userProvider: CurrentUserProviderNew
get() = CurrentUserProviderImpl(userManager)
val colorUtil: ColorUtil
get() = ColorUtil(mContext)
val materialScheme: MaterialSchemes
get() = MaterialSchemesProviderImpl(userProvider, colorUtil).getMaterialSchemesForCurrentUser()
val viewThemeUtils: ViewThemeUtils
get() {
val android = AndroidViewThemeUtils(materialScheme, colorUtil)
val material = MaterialViewThemeUtils(materialScheme, colorUtil)
val androidx = AndroidXViewThemeUtils(materialScheme, android)
val talk = TalkSpecificViewThemeUtils(materialScheme, androidx)
val dialog = DialogViewThemeUtils(materialScheme)
return ViewThemeUtils(materialScheme, android, material, androidx, talk, dialog)
}
val messageUtils: MessageUtils
get() = MessageUtils(mContext)
val retrofit: Retrofit
get() {
val retrofitBuilder = Retrofit.Builder()
.client(OkHttpClient.Builder().build())
.baseUrl("https://nextcloud.com")
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.addConverterFactory(LoganSquareConverterFactory.create())
return retrofitBuilder.build()
}
val ncApi: NcApi
get() = retrofit.create(NcApi::class.java)
val ncApiCoroutines: NcApiCoroutines
get() = retrofit.create(NcApiCoroutines::class.java)
val chatNetworkDataSource: ChatNetworkDataSource
get() = RetrofitChatNetwork(ncApi, ncApiCoroutines)
val usersDao: UsersDao
get() = DummyUserDaoImpl()
val chatMessagesDao: ChatMessagesDao
get() = DummyChatMessagesDaoImpl()
val chatBlocksDao: ChatBlocksDao
get() = DummyChatBlocksDaoImpl()
val conversationsDao: ConversationsDao
get() = DummyConversationDaoImpl()
val networkMonitor: NetworkMonitor
get() = NetworkMonitorImpl(mContext)
val chatRepository: ChatMessageRepository
get() = OfflineFirstChatRepository(
chatMessagesDao,
chatBlocksDao,
chatNetworkDataSource,
networkMonitor,
userProvider
)
val conversationNetworkDataSource: ConversationsNetworkDataSource
get() = RetrofitConversationsNetwork(ncApi)
val conversationRepository: OfflineConversationsRepository
get() = OfflineFirstConversationsRepository(
conversationsDao,
conversationNetworkDataSource,
chatNetworkDataSource,
networkMonitor,
userProvider
)
val reactionsRepository: ReactionsRepository
get() = ReactionsRepositoryImpl(ncApi, userProvider, chatMessagesDao)
val mediaRecorderManager: MediaRecorderManager
get() = MediaRecorderManager()
val audioFocusRequestManager: AudioFocusRequestManager
get() = AudioFocusRequestManager(mContext)
val chatViewModel: ChatViewModel
get() = ChatViewModel(
appPreferences,
chatNetworkDataSource,
chatRepository,
conversationRepository,
reactionsRepository,
mediaRecorderManager,
audioFocusRequestManager,
userProvider
)
val contactsRepository: ContactsRepository
get() = ContactsRepositoryImpl(ncApiCoroutines, userProvider)
val contactsViewModel: ContactsViewModel
get() = ContactsViewModel(contactsRepository)
}

View File

@ -0,0 +1,213 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.utils.preview
import com.nextcloud.talk.data.database.dao.ChatBlocksDao
import com.nextcloud.talk.data.database.dao.ChatMessagesDao
import com.nextcloud.talk.data.database.dao.ConversationsDao
import com.nextcloud.talk.data.database.model.ChatBlockEntity
import com.nextcloud.talk.data.database.model.ChatMessageEntity
import com.nextcloud.talk.data.database.model.ConversationEntity
import com.nextcloud.talk.data.user.UsersDao
import com.nextcloud.talk.data.user.model.UserEntity
import com.nextcloud.talk.models.json.push.PushConfigurationState
import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.Single
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
class DummyChatMessagesDaoImpl : ChatMessagesDao {
override fun getNewestMessageId(internalConversationId: String): Long = 0L
override fun getMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> = flowOf()
override fun getTempMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> =
flowOf()
override fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> {
// nothing to return here as long this class is only used for the Search window
return flowOf()
}
override fun getTempMessageForConversation(
internalConversationId: String,
referenceId: String
): Flow<ChatMessageEntity> = flowOf()
override suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>) { /* */ }
override suspend fun upsertChatMessage(chatMessage: ChatMessageEntity) { /* */ }
override fun getChatMessageForConversation(
internalConversationId: String,
messageId: Long
): Flow<ChatMessageEntity> = flowOf()
override fun deleteChatMessages(internalIds: List<String>) { /* */ }
override fun deleteTempChatMessages(internalConversationId: String, referenceIds: List<String>) { /* */ }
override fun updateChatMessage(message: ChatMessageEntity) { /* */ }
override fun getMessagesFromIds(messageIds: List<Long>): Flow<List<ChatMessageEntity>> = flowOf()
override fun getMessagesForConversationSince(
internalConversationId: String,
messageId: Long
): Flow<List<ChatMessageEntity>> = flowOf()
override fun getMessagesForConversationBefore(
internalConversationId: String,
messageId: Long,
limit: Int
): Flow<List<ChatMessageEntity>> = flowOf()
override fun getMessagesForConversationBeforeAndEqual(
internalConversationId: String,
messageId: Long,
limit: Int
): Flow<List<ChatMessageEntity>> = flowOf()
override fun getCountBetweenMessageIds(
internalConversationId: String,
oldestMessageId: Long,
newestMessageId: Long
): Int = 0
override fun clearAllMessagesForUser(pattern: String) { /* */ }
override fun deleteMessagesOlderThan(internalConversationId: String, messageId: Long) { /* */ }
}
class DummyUserDaoImpl : UsersDao() {
private val dummyUsers = mutableListOf(
UserEntity(1L, "user1_id", "user1", "server1", "1"),
UserEntity(2L, "user2_id", "user2", "server1", "2"),
UserEntity(0L, "user3_id", "user3", "server2", "3")
)
private var activeUserId: Long? = 1L
override fun getActiveUser(): Maybe<UserEntity> {
return Maybe.fromCallable { dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } }
}
override fun getActiveUserObservable(): Observable<UserEntity> {
return Observable.fromCallable { dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion } }
}
override fun getActiveUserSynchronously(): UserEntity? {
return dummyUsers.find { it.id == activeUserId && !it.scheduledForDeletion }
}
override fun deleteUser(user: UserEntity): Int {
val initialSize = dummyUsers.size
dummyUsers.removeIf { it.id == user.id }
return initialSize - dummyUsers.size
}
override fun updateUser(user: UserEntity): Int {
val index = dummyUsers.indexOfFirst { it.id == user.id }
return if (index != -1) {
dummyUsers[index] = user
1
} else {
0
}
}
override fun saveUser(user: UserEntity): Long {
val newUser = user.copy(id = dummyUsers.size + 1L)
dummyUsers.add(newUser)
return newUser.id
}
override fun saveUsers(vararg users: UserEntity): List<Long> {
return users.map { saveUser(it) }
}
override fun getUsers(): Single<List<UserEntity>> {
return Single.just(dummyUsers.filter { !it.scheduledForDeletion })
}
override fun getUserWithId(id: Long): Maybe<UserEntity> {
return Maybe.fromCallable { dummyUsers.find { it.id == id } }
}
override fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe<UserEntity> {
return Maybe.fromCallable { dummyUsers.find { it.id == id && !it.scheduledForDeletion } }
}
override fun getUserWithUserId(userId: String): Maybe<UserEntity> {
return Maybe.fromCallable { dummyUsers.find { it.userId == userId } }
}
override fun getUsersScheduledForDeletion(): Single<List<UserEntity>> {
return Single.just(dummyUsers.filter { it.scheduledForDeletion })
}
override fun getUsersNotScheduledForDeletion(): Single<List<UserEntity>> {
return Single.just(dummyUsers.filter { !it.scheduledForDeletion })
}
override fun getUserWithUsernameAndServer(username: String, server: String): Maybe<UserEntity> {
return Maybe.fromCallable { dummyUsers.find { it.username == username } }
}
override fun setUserAsActiveWithId(id: Long): Int {
activeUserId = id
return 1
}
override fun updatePushState(id: Long, state: PushConfigurationState): Single<Int> {
val index = dummyUsers.indexOfFirst { it.id == id }
return if (index != -1) {
dummyUsers[index] = dummyUsers[index]
Single.just(1)
} else {
Single.just(0)
}
}
}
class DummyConversationDaoImpl : ConversationsDao {
override fun getConversationsForUser(accountId: Long): Flow<List<ConversationEntity>> = flowOf()
override fun getConversationForUser(accountId: Long, token: String): Flow<ConversationEntity?> = flowOf()
override fun upsertConversations(conversationEntities: List<ConversationEntity>) { /* */ }
override fun deleteConversations(conversationIds: List<String>) { /* */ }
override fun updateConversation(conversationEntity: ConversationEntity) { /* */ }
override fun clearAllConversationsForUser(accountId: Long) { /* */ }
}
class DummyChatBlocksDaoImpl : ChatBlocksDao {
override fun deleteChatBlocks(blocks: List<ChatBlockEntity>) { /* */ }
override fun getChatBlocks(internalConversationId: String): Flow<List<ChatBlockEntity>> = flowOf()
override fun getChatBlocksContainingMessageId(
internalConversationId: String,
messageId: Long
): Flow<List<ChatBlockEntity?>> = flowOf()
override fun getConnectedChatBlocks(
internalConversationId: String,
oldestMessageId: Long,
newestMessageId: Long
): Flow<List<ChatBlockEntity>> = flowOf()
override suspend fun upsertChatBlock(chatBlock: ChatBlockEntity) { /* */ }
override fun clearChatBlocksForUser(pattern: String) { /* */ }
override fun deleteChatBlocksOlderThan(internalConversationId: String, messageId: Long) { /* */ }
}

View File

@ -1,7 +1,7 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
~ SPDX-FileCopyrightText: 2021-2025 Google LLC
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"

View File

@ -139,6 +139,19 @@
<include layout="@layout/out_of_office_view" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/conversation_delete_notice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"
android:layout_margin="8dp"
app:cardCornerRadius="12dp">
<include layout="@layout/remainder_to_delete_conversation" />
</com.google.android.material.card.MaterialCardView>
<com.stfalcon.chatkit.messages.MessagesList
android:id="@+id/messagesListView"
android:layout_width="match_parent"

View File

@ -86,8 +86,49 @@
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/margin_between_elements"
android:textSize="@dimen/headline_text_size"
android:textStyle="bold"
tools:text="Jane Doe" />
<androidx.emoji2.widget.EmojiTextView
android:id="@+id/pronouns"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/avatar_image"
android:paddingBottom="4dp"
android:paddingTop="2dp"
android:layout_marginStart="@dimen/margin_between_elements"
android:layout_marginTop="@dimen/margin_between_elements"
android:layout_toEndOf="@id/display_name_text"
android:textSize="@dimen/supporting_text_text_size"
android:visibility="gone"
tools:visibility="visible"
tools:text="She/Her"
/>
<androidx.emoji2.widget.EmojiTextView
android:id="@+id/profession_company"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/display_name_text"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/margin_between_elements"
android:textSize="@dimen/headline_text_size"
android:visibility="gone"
tools:visibility="visible"
tools:text="Marketing Manager @ Nextcloud GmbH" />
<androidx.emoji2.widget.EmojiTextView
android:id="@+id/location_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/profession_company"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/margin_between_elements"
android:textSize="@dimen/headline_text_size"
android:visibility="gone"
tools:visibility="visible"
tools:text="10:03 PM · London" />
</RelativeLayout>
<LinearLayout
@ -269,34 +310,34 @@
android:textSize="@dimen/supporting_text_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/share_conversation_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_margin"
android:paddingTop="@dimen/standard_half_margin"
android:paddingEnd="@dimen/standard_margin"
android:paddingBottom="@dimen/standard_half_margin"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground">
<LinearLayout
android:id="@+id/share_conversation_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:paddingStart="@dimen/standard_margin"
android:paddingTop="@dimen/standard_half_margin"
android:paddingEnd="@dimen/standard_margin"
android:paddingBottom="@dimen/standard_half_margin">
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_share_variant"
app:tint="@color/grey_600" />
<ImageView
android:layout_width="24dp"
android:layout_height="40dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@null"
android:src="@drawable/ic_share_variant"
app:tint="@color/grey_600" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/nc_guest_access_share_link"
android:textSize="@dimen/headline_text_size" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/nc_guest_access_share_link"
android:textSize="@dimen/headline_text_size" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
@ -304,18 +345,18 @@
android:id="@+id/lock_conversation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_margin"
android:paddingEnd="@dimen/standard_margin"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground">
android:paddingStart="@dimen/standard_margin"
android:paddingEnd="@dimen/standard_margin">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="@dimen/standard_half_margin"
android:paddingBottom="@dimen/standard_half_margin"
android:layout_weight="1"
android:orientation="horizontal">
android:orientation="horizontal"
android:paddingTop="@dimen/standard_half_margin"
android:paddingBottom="@dimen/standard_half_margin">
<ImageView
android:layout_width="24dp"
@ -431,12 +472,12 @@
android:id="@+id/list_bans_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal"
android:paddingStart="@dimen/standard_margin"
android:paddingTop="@dimen/standard_half_margin"
android:paddingEnd="@dimen/standard_margin"
android:paddingBottom="@dimen/standard_half_margin"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground">
android:paddingBottom="@dimen/standard_half_margin">
<ImageView
android:layout_width="24dp"

View File

@ -195,37 +195,6 @@
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/conversation_archive"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_padding"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/outline_archive_24"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/conversation_archive_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="40dp"
android:paddingEnd="@dimen/zero"
android:text="@string/archive_conversation"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/conversation_operation_rename"
android:layout_width="match_parent"
@ -257,7 +226,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/conversation_operation_delete"
android:id="@+id/conversation_archive"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
@ -271,16 +240,17 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_delete_grey600_24dp"
android:src="@drawable/outline_archive_24"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/conversation_archive_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="40dp"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_delete_call"
android:text="@string/archive_conversation"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
@ -316,6 +286,36 @@
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/conversation_operation_delete"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_padding"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_delete_grey600_24dp"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="40dp"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_delete_call"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -12,7 +12,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/popup_menu_color">
android:background="@color/bg_bottom_sheet">
<TextView
android:id="@+id/event_scheduled"
@ -26,7 +26,6 @@
android:id="@+id/meetingTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp"
tools:text="Meeting at 8:00 pm"/>
@ -46,6 +45,7 @@
android:layout_height="wrap_content"
android:text="@string/archive_conversation"
android:visibility = "gone"
android:textSize="18sp"
android:paddingTop="24dp"/>

View File

@ -62,7 +62,8 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/standard_margin"
android:clickable="false" />
android:checked="false"
android:clickable="false"/>
</LinearLayout>
@ -111,6 +112,47 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:checked="true"
android:clickable="false" />
android:clickable="false"/>
</LinearLayout>
<LinearLayout
android:id="@+id/notification_settings_sensitive_conversation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="@dimen/standard_half_padding"
android:paddingBottom="@dimen/standard_half_padding"
android:paddingStart="@dimen/standard_margin"
android:paddingEnd="@dimen/standard_margin"
android:background="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nc_sensitive_conversation"
android:textSize="@dimen/headline_text_size" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/nc_sensitive_conversation_hint"
android:textSize="@dimen/supporting_text_text_size"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/sensitive_conversation_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:checked="false"
android:clickable="false" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kch@gmail.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/deletion_warning_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="12dp">
<TextView
android:id="@+id/deletion_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:lineSpacingExtra="4dp"
android:gravity="center"
android:layout_marginBottom="12dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_now_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nc_delete_now"
android:textColor="@color/white"
app:icon="@drawable/ic_delete"
app:iconPadding="8dp"
app:iconTint="@color/white"
app:iconGravity="textStart"
android:backgroundTint="@color/nc_darkRed"
android:layout_marginEnd="16dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/keep_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nc_keep"
app:icon="@drawable/ic_check"
app:iconPadding="8dp"
app:iconGravity="textStart" />
</LinearLayout>
</LinearLayout>

View File

@ -64,6 +64,7 @@
android:layout_height="wrap_content"
android:layout_below="@id/dialogName"
android:layout_marginTop="6dp"
android:id="@+id/relativeLayout"
android:layout_toEndOf="@id/dialogAvatarFrameLayout">
<androidx.emoji2.widget.EmojiTextView
@ -121,8 +122,8 @@
android:layout_height="wrap_content"
android:layout_alignTop="@id/dialogAvatarFrameLayout"
android:layout_marginTop="2dp"
android:layout_toStartOf="@id/dialogDate"
android:layout_toEndOf="@id/dialogAvatarFrameLayout"
android:layout_toStartOf="@id/dialogDate"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">تحرير</string>
<string name="add_participants">إضافة </string>
<string name="add_to_notes">إضافة إلى الملاحظات</string>
<string name="added_to_favorites">إضافة المحادثة %1$sإلى المُفضّلة</string>
<string name="appbar_search_in">بحث في %s</string>
@ -274,9 +275,9 @@
<string name="nc_ignore_battery_optimization_dialog_text">تحسين البطارية لا يتم تجاهله. يجب تغيير هذا للتأكد من أن الإشعارات تعمل في الخلفية! الرجاء النقر فوق \"موافق OK\" ثم تحديد \"جميع التطبيقات All apps\" -> %1$s -> \"لا تقم بالتحسين Do not optimize\"</string>
<string name="nc_ignore_battery_optimization_dialog_title">تجاهل توفير البطارية</string>
<string name="nc_important_conversation">محادثة مهمة</string>
<string name="nc_important_conversation_desc">الاشعارات في هذه المحادثة ستتجاوز إعداد الحالة \"يُرجى عدم الإزعاج\"</string>
<string name="nc_invitations">دعوات</string>
<string name="nc_join_open_conversations">الانضمام إلى محادثات جارية</string>
<string name="nc_keep">حفظ</string>
<string name="nc_last_moderator_leaving_room_warning">تحتاج إلى إنشاء ميسر جديد قبل أن تتمكن من مغادرة المحادثة</string>
<string name="nc_last_modified">%1$s| آخر تعديل: %2$s</string>
<string name="nc_leave">غادر المحادثة</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Editar</string>
<string name="add_participants">Amestar</string>
<string name="add_to_notes">Amestar a Notes</string>
<string name="added_to_favorites">La conversación «%1$s» metióse en Favoritos</string>
<string name="appbar_search_in">Buscar en: %s</string>
@ -162,6 +163,7 @@
<string name="nc_guest_access_share_link">Compartir l\'enllaz de la converación</string>
<string name="nc_important_conversation">Converación importante</string>
<string name="nc_invitations">Invitaciones</string>
<string name="nc_keep">Caltener</string>
<string name="nc_leave">Colar de la conversación</string>
<string name="nc_leaving_call">Colando de la llamada…</string>
<string name="nc_license_title">Llicencia</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Edit</string>
<string name="add_participants">Add</string>
<string name="add_to_notes">Add to Notes</string>
<string name="added_to_favorites">Added conversation %1$s to favourites</string>
<string name="appbar_search_in">Search in %s</string>
@ -71,6 +72,7 @@
<string name="leave_call">Leave call</string>
<string name="left_conversation">You left the conversation %1$s</string>
<string name="load_more_results">Load more results</string>
<string name="local_time">Local time: %1$s</string>
<string name="lock_conversation">Lock conversation</string>
<string name="lock_symbol">Lock symbol</string>
<string name="lower_hand">Lower hand</string>
@ -177,6 +179,7 @@
<string name="nc_delete_conversation_more">If you delete the conversation, it will also be deleted for all other participants.</string>
<string name="nc_delete_message">Delete</string>
<string name="nc_delete_message_leaked_to_matterbridge">Message deleted successfully, but it might have been leaked to other services</string>
<string name="nc_delete_now">Delete now</string>
<string name="nc_deleted_user">User %1$s was removed</string>
<string name="nc_demote">Demote from moderator</string>
<string name="nc_description_record_voice">Record voice message</string>
@ -282,10 +285,11 @@
<string name="nc_ignore_battery_optimization_dialog_text">Battery optimization is not ignored. This should be changed to make sure that notifications work in the background! Please click OK and select \"All apps\" -> %1$s -> Do not optimize</string>
<string name="nc_ignore_battery_optimization_dialog_title">Ignore battery optimization</string>
<string name="nc_important_conversation">Important conversation</string>
<string name="nc_important_conversation_desc">Notifications in this conversation will override Do Not Disturb settings</string>
<string name="nc_important_conversation_desc">\"Do not disturb\" user status is ignored for important conversations</string>
<string name="nc_invalid_time">Invalid time</string>
<string name="nc_invitations">Invitations</string>
<string name="nc_join_open_conversations">Join open conversations</string>
<string name="nc_keep">Keep</string>
<string name="nc_last_moderator_leaving_room_warning">You need to promote a new moderator before you can leave the conversation</string>
<string name="nc_last_modified">%1$s | Last modified: %2$s</string>
<string name="nc_leave">Leave conversation</string>
@ -305,6 +309,10 @@
<string name="nc_manual">Not set</string>
<string name="nc_mark_as_read">Mark as read</string>
<string name="nc_mark_as_unread">Mark as unread</string>
<string name="nc_mark_conversation_as_important">Conversation marked as important</string>
<string name="nc_mark_conversation_as_insensitive">Conversation unmarked as sensitive</string>
<string name="nc_mark_conversation_as_sensitive">Conversation marked as sensitive</string>
<string name="nc_mark_conversation_as_unimportant">Conversation unmarked as important</string>
<string name="nc_meeting_ended">Meeting ended</string>
<string name="nc_message_added_to_notes">Message added to notes</string>
<string name="nc_message_failed">Failed</string>
@ -383,6 +391,7 @@
<string name="nc_rename_confirm">Rename</string>
<string name="nc_reply">Reply</string>
<string name="nc_reply_privately">Reply privately</string>
<string name="nc_room_retention">Room is retained successfully</string>
<string name="nc_save_message">Save</string>
<string name="nc_save_success">Saved successfully</string>
<string name="nc_screen_lock_timeout_30">30 seconds</string>
@ -397,6 +406,8 @@
<string name="nc_search">Search</string>
<string name="nc_select_an_account">Select an account</string>
<string name="nc_send_edit_message">Update message</string>
<string name="nc_sensitive_conversation">Sensitive conversation</string>
<string name="nc_sensitive_conversation_hint">Message preview will be disabled in conversation list and notifications</string>
<string name="nc_sent_a_gif" formatted="true">%1$s sent a GIF.</string>
<string name="nc_sent_a_gif_you">You sent a GIF.</string>
<string name="nc_sent_a_video" formatted="true">%1$s sent a video.</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Редактиране</string>
<string name="add_participants">Добавяне</string>
<string name="appbar_search_in">Търсене в %s</string>
<string name="archived">Архивирано</string>
<string name="audio_output_bluetooth">Bluetooth /Блутут/</string>
@ -16,7 +17,7 @@
<string name="call_without_notification">Обаждане без известие</string>
<string name="camera_permission_granted">Дадено е право на камера. Моля, изберете камера отново. </string>
<string name="choose_avatar_from_cloud">Избор на аватар от облака</string>
<string name="clear_status_message">Изчистване на съобщението за състоянието</string>
<string name="clear_status_message">Изчисти състоянието</string>
<string name="clear_status_message_after">Изчистване на съобщение за състоянието след</string>
<string name="close">Затваряне</string>
<string name="connection_established">Осъществена е връзка</string>
@ -37,7 +38,7 @@
<string name="file_list_folder">папка</string>
<string name="file_list_loading">Зареждане …</string>
<string name="filename_progress">%1$s (%2$d)</string>
<string name="fourHours">4 часа</string>
<string name="fourHours">4 чàса</string>
<string name="invisible">Невидим</string>
<string name="leave_call">Напускане на обаждането</string>
<string name="load_more_results">Зареждане на още резултати</string>
@ -64,7 +65,7 @@
<string name="nc_action_open_main_menu">Отворяне на главното меню</string>
<string name="nc_add_attachment">Добавяне на прикачен файл</string>
<string name="nc_add_emojis">Добавяне на емотикони</string>
<string name="nc_add_file">Добавяне към разговор</string>
<string name="nc_add_file">Добави към разговор</string>
<string name="nc_add_participants">Добавяне на участници</string>
<string name="nc_add_to_favorites">Добави към любимите</string>
<string name="nc_all_ok_operation">Добре, всичко е готово!</string>
@ -112,6 +113,7 @@
<string name="nc_common_disabled">Изключено</string>
<string name="nc_common_dismiss">Отхвърляне</string>
<string name="nc_common_error_sorry">Съжалявам нещо се обърка!</string>
<string name="nc_common_more_options">Още опции</string>
<string name="nc_common_set">Настройка</string>
<string name="nc_common_skip">Пропусни</string>
<string name="nc_common_unknown">Неизвестен</string>
@ -196,7 +198,7 @@
<string name="nc_guest_access_share_link">Споделяне на връзка за разговор</string>
<string name="nc_hint_enter_a_message">Въвеждане на съобщение …</string>
<string name="nc_important_conversation">Важен разговор</string>
<string name="nc_important_conversation_desc">Известията в този разговор ще отменят настройките „Не безпокойте“</string>
<string name="nc_keep">Запази</string>
<string name="nc_last_moderator_leaving_room_warning">Трябва да повишите нов модератор, преди да можете да напуснете разговора</string>
<string name="nc_last_modified">%1$s Последна промяна: %2$s</string>
<string name="nc_leave">Напускане на разговор</string>
@ -224,6 +226,7 @@
<string name="nc_missed_call">Пропуснахте обаждане от %s</string>
<string name="nc_moderator">Модератор</string>
<string name="nc_new_conversation">Нов разговор</string>
<string name="nc_new_conversation_visibility">Видимост</string>
<string name="nc_new_mention">Непрочетени споменавания</string>
<string name="nc_new_messages">Непрочетени съобщения.</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s не е наличен (не е инсталиран или е ограничен от администратора)</string>
@ -392,7 +395,7 @@
<string name="no_phone_book_integration_due_to_permissions">Няма интеграция на телефонен номер поради липсващи права</string>
<string name="oneHour">1 час</string>
<string name="online">На линия</string>
<string name="online_status">Състояние на линия</string>
<string name="online_status">Състояние</string>
<string name="openConversations">Отворени разговори</string>
<string name="open_in_files_app">Отворяне в приложението Файлове</string>
<string name="play_pause_voice_message">Възпроизвеждане/пауза на гласово съобщение</string>
@ -428,10 +431,10 @@
<string name="save">Записване</string>
<string name="scope_federated_description">Синхронизиране само с доверени сървъри</string>
<string name="scope_federated_title">Федериран</string>
<string name="scope_local_description">Видим само за хора от този случай и гости</string>
<string name="scope_local_description">Видимо за потребители на тази инстанция на сървъра, както и гости.</string>
<string name="scope_local_title">Локално</string>
<string name="scope_private_description">Видим само за хора, съчетани чрез интегриране на телефонен номер чрез Talk на мобилен телефон</string>
<string name="scope_private_title">Частен</string>
<string name="scope_private_description">Видим само за хора, открити по телефонен номер, който е зададен в \"Talk\".</string>
<string name="scope_private_title">Лично</string>
<string name="scope_published_description">Синхронизиране с доверени сървъри и с глобалната и публичната адресна книга</string>
<string name="scope_published_title">Публикувано</string>
<string name="scope_toggle">Превключване на обхват</string>
@ -447,7 +450,7 @@
<string name="set">Да се зададе</string>
<string name="set_avatar_from_camera">Задаване на аватар от камерата</string>
<string name="set_status">Задаване на състояние</string>
<string name="set_status_message">Задаване на съобщение за състояние</string>
<string name="set_status_message">Задай състояние</string>
<string name="share">Споделяне</string>
<string name="shared_items_audio">Аудио</string>
<string name="shared_items_file">Файл</string>
@ -495,7 +498,7 @@
<string name="userinfo_no_info_headline">Няма зададена лична информация</string>
<string name="userinfo_no_info_text">Добавяне на име, снимка и подробности за контакт към страницата на вашия профил.</string>
<string name="video_call">Видео разговор</string>
<string name="whats_your_status">Какъв е вашият статус?</string>
<string name="whats_your_status">Какво е вашето състояние?</string>
<plurals name="polls_amount_voters">
<item quantity="one">%dгласувания </item>
<item quantity="other">%d гласувания</item>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Edició</string>
<string name="add_participants">Afegeix</string>
<string name="add_to_notes">Afegeix-ho a les notes</string>
<string name="added_to_favorites">S\'ha afegit la conversa %1$s als preferits</string>
<string name="appbar_search_in">Cerca a %s</string>
@ -271,9 +272,9 @@
<string name="nc_ignore_battery_optimization_dialog_text">L\'optimització de la bateria no s\'ignora. Això hauria de canviar-se per a garantir que les notificacions funcionin en segon pla. Feu clic a D\'acord i seleccioneu \"Totes les aplicacions\" -> %1$s -> No optimitzis</string>
<string name="nc_ignore_battery_optimization_dialog_title">Ignora l\'optimització de la bateria</string>
<string name="nc_important_conversation">Conversa important</string>
<string name="nc_important_conversation_desc">Les notificacions d\'aquesta conversa anul·laran els paràmetres de no destorbar.</string>
<string name="nc_invitations">Invitacions</string>
<string name="nc_join_open_conversations">Uneix-te a converses obertes</string>
<string name="nc_keep">Mantén</string>
<string name="nc_last_moderator_leaving_room_warning">Heu de promocionar un nou moderador abans de deixar la conversa</string>
<string name="nc_last_modified">%1$s | Darrera modificació: %2$s</string>
<string name="nc_leave">Surt de la conversa</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Upravit</string>
<string name="add_participants">Přidat</string>
<string name="add_to_notes">Přidat do Poznámek</string>
<string name="added_to_favorites">Konverzace %1$s přidána do oblíbených</string>
<string name="appbar_search_in">Hledat v %s</string>
@ -71,6 +72,7 @@
<string name="leave_call">Opustit hovor</string>
<string name="left_conversation">Opustili jste konverzaci %1$s</string>
<string name="load_more_results">Načíst další výsledky</string>
<string name="local_time">Místní čas: %1$s</string>
<string name="lock_conversation">Uzamknout konverzaci</string>
<string name="lock_symbol">Symbol zámku</string>
<string name="lower_hand">Přestat se hlásit</string>
@ -177,6 +179,7 @@
<string name="nc_delete_conversation_more">Pokud konverzaci smažete, bude smazána také pro všechny její ostatní účastníky.</string>
<string name="nc_delete_message">Smazat</string>
<string name="nc_delete_message_leaked_to_matterbridge">Zpráva úspěšně smazána, ale možná unikla do jiných služeb</string>
<string name="nc_delete_now">Smazat nyní</string>
<string name="nc_deleted_user">Uživatel %1$s byl odebrán</string>
<string name="nc_demote">Odebrat oprávnění moderátora</string>
<string name="nc_description_record_voice">Nahrát hlasovou zprávu</string>
@ -282,10 +285,11 @@
<string name="nc_ignore_battery_optimization_dialog_text">Úspora energie z akumulátoru není ignorována. Pokud chcete, aby upozorňování na pozadí fungovalo správně, mělo by toto být změněno! Klikněte na OK a vyberte „Všechny aplikace“ -> %1$s -> neoptimalizovat</string>
<string name="nc_ignore_battery_optimization_dialog_title">Ignorovat úsporu energie z akumulátoru</string>
<string name="nc_important_conversation">Důležitá konverzace</string>
<string name="nc_important_conversation_desc">Upozornění v této konverzaci budou ignorovat režim Nerušit</string>
<string name="nc_important_conversation_desc">Stav uživatele „Nevyrušovat“ bude v případě důležitých konverzací ignorován</string>
<string name="nc_invalid_time">Neplatný čas</string>
<string name="nc_invitations">Pozvání</string>
<string name="nc_join_open_conversations">Přidat se do všem přístupných konverzací</string>
<string name="nc_keep">Ponechat</string>
<string name="nc_last_moderator_leaving_room_warning">Než budete moci konverzaci opustit, je třeba předat někomu roli moderátora</string>
<string name="nc_last_modified">%1$s | Naposledy upraveno: %2$s</string>
<string name="nc_leave">Opustit konverzaci</string>
@ -305,7 +309,12 @@
<string name="nc_manual">Nenastaveno</string>
<string name="nc_mark_as_read">Označit jako přečtené</string>
<string name="nc_mark_as_unread">Označit jako nepřečtené</string>
<string name="nc_mark_conversation_as_important">Konverzace označena jako důležitá</string>
<string name="nc_mark_conversation_as_insensitive">Zrušeno značení konverzace coby citlivé</string>
<string name="nc_mark_conversation_as_sensitive">Konverzace označena jako citlivá</string>
<string name="nc_mark_conversation_as_unimportant">Zrušeno značení konverzace coby důležité</string>
<string name="nc_meeting_ended">Schůzka skončila</string>
<string name="nc_message_added_to_notes">Zpráva přidána do poznámek</string>
<string name="nc_message_failed">Nezdařilo se</string>
<string name="nc_message_failed_to_send">Odeslání zprávy se nezdařilo:</string>
<string name="nc_message_offline">Bez připojení</string>
@ -382,6 +391,7 @@
<string name="nc_rename_confirm">Přejmenovat</string>
<string name="nc_reply">Odpovědět</string>
<string name="nc_reply_privately">Odpovědět soukromě</string>
<string name="nc_room_retention">Místnost je úspěšně ponechána</string>
<string name="nc_save_message">Uložit</string>
<string name="nc_save_success">Úspěšně uloženo</string>
<string name="nc_screen_lock_timeout_30">30 sekund</string>
@ -396,6 +406,8 @@
<string name="nc_search">Hledat</string>
<string name="nc_select_an_account">Vyberte účet</string>
<string name="nc_send_edit_message">Aktualizovat zprávu</string>
<string name="nc_sensitive_conversation">Citlivá konverzace</string>
<string name="nc_sensitive_conversation_hint">Náhled zprávy bude vypnut pro seznam konverzací a notifikace</string>
<string name="nc_sent_a_gif" formatted="true">%1$s poslal(a) GIF animaci.</string>
<string name="nc_sent_a_gif_you">Odeslali jste GIF animaci.</string>
<string name="nc_sent_a_video" formatted="true">%1$s poslal(a) video.</string>
@ -531,6 +543,7 @@
<string name="online_status">Stav online</string>
<string name="openConversations">Otevřít konverzace</string>
<string name="open_in_files_app">Otevřít v aplikaci Soubory</string>
<string name="open_notes">Otevřít poznámky</string>
<string name="play_pause_voice_message">Přehrát/pozastavit hlasovou zprávu</string>
<string name="playback_speed_control">Ovládání rychlosti přehrávání</string>
<string name="polls_add_option">Přidat volbu</string>
@ -670,6 +683,12 @@
<item quantity="many">Viz %d podobných zpráv</item>
<item quantity="other">Viz %d podobné zprávy</item>
</plurals>
<plurals name="nc_conversation_auto_delete_info">
<item quantity="one">Tato konverzace bude automaticky smazána pro kohokoli po %1$d dni bez jakékoli aktivity.</item>
<item quantity="few">Tato konverzace bude automaticky smazána pro kohokoli po %1$d dnech bez jakékoli aktivity.</item>
<item quantity="many">Tato konverzace bude automaticky smazána pro kohokoli po %1$d dnech bez jakékoli aktivity.</item>
<item quantity="other">Tato konverzace bude automaticky smazána pro kohokoli po %1$d dnech bez jakékoli aktivity.</item>
</plurals>
<plurals name="polls_amount_voters">
<item quantity="one">%d hlas</item>
<item quantity="few">%d hlasy</item>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Rediger</string>
<string name="add_participants">Tilføj</string>
<string name="add_to_notes">Tilføj til Noter</string>
<string name="added_to_favorites">Tilføjede samtalen %1$s til favoritter</string>
<string name="appbar_search_in">Søg i %s</string>
@ -269,7 +270,6 @@
<string name="nc_ignore_battery_optimization_dialog_text">Batterioptimering bliver ikke ignoreret. Dette bør ændres for at være sikker på at notifikationer virker i baggrunden! Klik venligst på OK og vælg \"Alle apps\" -> %1$s -> Optimer ikke</string>
<string name="nc_ignore_battery_optimization_dialog_title">Ignorer batterioptimering</string>
<string name="nc_important_conversation">Vigtig samtale</string>
<string name="nc_important_conversation_desc">notifikationer under denne samtale vil ignorere Forstyr ikke indstillinger</string>
<string name="nc_invitations">Invitationer</string>
<string name="nc_join_open_conversations">Deltag i åbne samtaler</string>
<string name="nc_last_moderator_leaving_room_warning">Du skal udnævne en ny moderator inden du kan forlade samtalen.</string>
@ -390,9 +390,9 @@
<string name="nc_server_failed_to_import_account">Den valgte konto kunne ikke importeres</string>
<string name="nc_server_helper_text">Linket til dit %1$s web interface når du åbner den i browseren.</string>
<string name="nc_server_import_account">Importer konto fra %1$s appen</string>
<string name="nc_server_import_account_plain">Importer konto</string>
<string name="nc_server_import_account_plain">Importér konto</string>
<string name="nc_server_import_accounts">Importer konti fra %1$s appen</string>
<string name="nc_server_import_accounts_plain">Importer konti</string>
<string name="nc_server_import_accounts_plain">Importér konti</string>
<string name="nc_server_maintenance">Få venligst %1$s ud fra vedligeholdelse </string>
<string name="nc_server_not_installed">Afslut venligst din %1$s installation</string>
<string name="nc_server_testing_connection">Tester forbindelsen</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Bearbeiten</string>
<string name="add_participants">Hinzufügen</string>
<string name="add_to_notes">Zu Notizen hinzufügen</string>
<string name="added_to_favorites">Unterhaltung %1$s zu Favoriten hinzugefügt</string>
<string name="appbar_search_in">Suche in %s</string>
@ -71,6 +72,7 @@
<string name="leave_call">Anruf verlassen</string>
<string name="left_conversation">Sie haben die Unterhaltung %1$s verlassen</string>
<string name="load_more_results">Weitere Ergebnisse laden</string>
<string name="local_time">Ortszeit: %1$s</string>
<string name="lock_conversation">Unterhaltung sperren</string>
<string name="lock_symbol">Schloss-Symbol</string>
<string name="lower_hand">Hand herunternehmen</string>
@ -177,6 +179,7 @@
<string name="nc_delete_conversation_more">Wenn Sie diese Unterhaltung löschen, dann wird diese auch für alle anderen Teilnehmer gelöscht.</string>
<string name="nc_delete_message">Löschen</string>
<string name="nc_delete_message_leaked_to_matterbridge">Nachricht gelöscht, sie wurde aber möglicherweise an andere Dienste weitergegeben.</string>
<string name="nc_delete_now">Jetzt löschen</string>
<string name="nc_deleted_user">Benutzer %1$s wurde entfernt</string>
<string name="nc_demote">Moderator absetzen</string>
<string name="nc_description_record_voice">Sprachnachricht aufnehmen</string>
@ -282,10 +285,11 @@
<string name="nc_ignore_battery_optimization_dialog_text">Die Batterieoptimierung ist aktiviert. Dies sollte geändert werden, um sicherzustellen, dass Benachrichtigungen im Hintergrund funktionieren! Bitte klicken Sie auf OK und wählen Sie \"Alle Apps\" -> %1$s -> Nicht optimieren</string>
<string name="nc_ignore_battery_optimization_dialog_title">Batterieoptimierung ignorieren</string>
<string name="nc_important_conversation">Wichtige Unterhaltung</string>
<string name="nc_important_conversation_desc">Benachrichtigungen in dieser Unterhaltung überschreiben Nicht-Stören-Einstellungen</string>
<string name="nc_important_conversation_desc">\"Nicht stören\"-Benutzerstatus wird für wichtige Unterhaltungen ignoriert</string>
<string name="nc_invalid_time">Ungültige Zeit</string>
<string name="nc_invitations">Einladungen</string>
<string name="nc_join_open_conversations">Offenen Unterhaltungen beitreten</string>
<string name="nc_keep">Behalten</string>
<string name="nc_last_moderator_leaving_room_warning">Sie müssen einen neuen Moderator bestimmen, bevor Sie die Unterhaltung verlassen können.</string>
<string name="nc_last_modified">%1$s | Zuletzt geändert: %2$s</string>
<string name="nc_leave">Unterhaltung verlassen</string>
@ -305,6 +309,10 @@
<string name="nc_manual">Nicht eingestellt</string>
<string name="nc_mark_as_read">Als gelesen markieren</string>
<string name="nc_mark_as_unread">Als ungelesen markieren</string>
<string name="nc_mark_conversation_as_important">Unterhaltung als wichtig markiert</string>
<string name="nc_mark_conversation_as_insensitive">Unterhaltung nicht als sensibel markiert</string>
<string name="nc_mark_conversation_as_sensitive">Unterhaltung als sensibel markiert</string>
<string name="nc_mark_conversation_as_unimportant">Unterhaltung nicht als wichtig markiert</string>
<string name="nc_meeting_ended">Meeting beendet</string>
<string name="nc_message_added_to_notes">Nachricht zu den Notizen hinzugefügt</string>
<string name="nc_message_failed">Fehlgeschlagen</string>
@ -383,6 +391,7 @@
<string name="nc_rename_confirm">Umbenennen</string>
<string name="nc_reply">Antworten</string>
<string name="nc_reply_privately">Privat antworten</string>
<string name="nc_room_retention">Raum wird beibehalten</string>
<string name="nc_save_message">Speichern</string>
<string name="nc_save_success">Gespeichert</string>
<string name="nc_screen_lock_timeout_30">30 Sekunden</string>
@ -397,6 +406,8 @@
<string name="nc_search">Suchen</string>
<string name="nc_select_an_account">Konto auswählen</string>
<string name="nc_send_edit_message">Nachricht aktualisieren</string>
<string name="nc_sensitive_conversation">Sensible Unterhaltung</string>
<string name="nc_sensitive_conversation_hint">Die Nachrichtenvorschau wird in der Unterhaltungsliste und den Benachrichtigungen deaktiviert</string>
<string name="nc_sent_a_gif" formatted="true">%1$s hat ein GIF gesendet.</string>
<string name="nc_sent_a_gif_you">Sie haben ein GIF gesendet.</string>
<string name="nc_sent_a_video" formatted="true">%1$s hat ein Video gesendet.</string>
@ -494,7 +505,7 @@
<string name="nc_shared_location">Geteilter Ort</string>
<string name="nc_show_notification_warning_description">Wenn Benachrichtigungen nicht korrekt konfiguriert sind, zeige regelmäßig eine Warnung</string>
<string name="nc_show_notification_warning_title">Zeige regelmäßige Benachrichtigungswarnung</string>
<string name="nc_sort_by">Sortiere nach</string>
<string name="nc_sort_by">Sortieren nach</string>
<string name="nc_start_group_chat">Gruppenchat starten</string>
<string name="nc_start_time">Startzeit</string>
<string name="nc_switch_account">Konto wechseln</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Επεξεργασία</string>
<string name="add_participants">Προσθήκη</string>
<string name="appbar_search_in">Αναζήτηση στο %s</string>
<string name="archived">Αρχειοθετήθηκε</string>
<string name="audio_output_dialog_headline">Έξοδος ήχου</string>
@ -106,6 +107,7 @@
<string name="nc_conversations_empty">Συμμετέχετε σε συνομιλία ή ξεκινήστε μια νέα</string>
<string name="nc_conversations_empty_details">Πείτε γειά σε φίλους και συνεργάτες!</string>
<string name="nc_copy_message">Αντιγραφή</string>
<string name="nc_create_new_conversation">Δημιουργία νέας συνομιλίας</string>
<string name="nc_create_poll">Δημιουργία ψηφοφορίας</string>
<string name="nc_date_header_today">Σήμερα</string>
<string name="nc_date_header_yesterday">Χθές</string>
@ -165,8 +167,8 @@
<string name="nc_guest_access_share_link">Κοινή χρήση συνδέσμου συνομιλίας</string>
<string name="nc_hint_enter_a_message">Εισάγετε ένα μήνυμα ...</string>
<string name="nc_important_conversation">Σημαντική συνομιλία</string>
<string name="nc_important_conversation_desc">Οι Ειδοποιήσεις σε αυτή την συνομιλία θα παρακάμψουν τις ρυθμίσεις Μην ενοχλείτε</string>
<string name="nc_invitations">Προσκλήσεις</string>
<string name="nc_join_open_conversations">Δημιουργία νέας συνομιλίας</string>
<string name="nc_last_modified">%1$s Τελευταία τροποποίηση %2$s</string>
<string name="nc_leave">Εγκατάλειψη συνομιλίας</string>
<string name="nc_leaving_call">Αποχώρηση από την κλήση ...</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Editar</string>
<string name="add_participants">Guardar</string>
<string name="appbar_search_in">Compartir en %s</string>
<string name="archived">Archivado</string>
<string name="audio_output_bluetooth">Bluetooth</string>
@ -200,7 +201,7 @@
<string name="nc_guest_access_share_link">Compartir enlace de la conversación</string>
<string name="nc_hint_enter_a_message">Escribe un mensaje...</string>
<string name="nc_important_conversation">Conversación importante</string>
<string name="nc_important_conversation_desc">Las notificaciones en esta conversación anularán la configuración de No molestar</string>
<string name="nc_keep">Mantén</string>
<string name="nc_last_moderator_leaving_room_warning">Necesitas promover un nuevo moderador antes de poder abandonar la conversación</string>
<string name="nc_last_modified">%1$s | Última modificación: %2$s</string>
<string name="nc_leave">Dejar la conversación</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Editar</string>
<string name="add_participants">Añadir</string>
<string name="add_to_notes">Añadir a Notas</string>
<string name="added_to_favorites">Se añadió la conversación %1$s a los favoritos</string>
<string name="appbar_search_in">Buscar en %s</string>
@ -275,7 +276,6 @@
<string name="nc_ignore_battery_optimization_dialog_text">La optimización de batería no se está ignorando. ¡Esto debería ser cambiado para garantizar que las notificaciones funcionen en segundo plano!. Por favor, haga clic en OK y seleccione \"Todas las apps\" -> %1$s -> No optimizar</string>
<string name="nc_ignore_battery_optimization_dialog_title">Ignorar la optimización de batería</string>
<string name="nc_important_conversation">Conversación importante</string>
<string name="nc_important_conversation_desc">Las notificaciones de esta conversación anularán el ajuste de No Molestar</string>
<string name="nc_invitations">Invitaciones</string>
<string name="nc_join_open_conversations">Unirse a conversaciones abiertas</string>
<string name="nc_last_moderator_leaving_room_warning">Debe escoger a un nuevo moderador antes de que pueda abandonar la conversación</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Muuda</string>
<string name="add_participants">Lisa</string>
<string name="add_to_notes">Lisa Märkmetesse</string>
<string name="added_to_favorites">„%1$s“ vestlus on märgitud lemmikuks</string>
<string name="appbar_search_in">Otsi siin: %s</string>
@ -27,15 +28,15 @@
<string name="call_without_notification">Kõne ilma teavituseta</string>
<string name="camera_permission_granted">Õigused kaamera kasutamiseks on nüüd olemas. Palun vali kaamera uuesti.</string>
<string name="choose_avatar_from_cloud">Vali tunnuspilt pilvest</string>
<string name="clear_status_message">Tühjenda staatuseteade</string>
<string name="clear_status_message_after">Tühjenda staatuseteade pärast</string>
<string name="clear_status_message">Eemalda olekuteade</string>
<string name="clear_status_message_after">Eemalda olekuteade pärast</string>
<string name="close">Sulge</string>
<string name="close_icon">Sulgemise ikoon</string>
<string name="connection_established">Saadi ühendus</string>
<string name="connection_lost">Puudub ühendus serveriga</string>
<string name="connection_lost_sent_messages_are_queued">Ühendus on katkenud - saadetud sõnumid on edasisaatmise ootejärjekorras</string>
<string name="continuous_voice_message_recording">Häälsõnumi pidevaks salvestamiseks lukusta salvestamine</string>
<string name="conversation_archived">Vestlust on arhiveeritud</string>
<string name="conversation_archived">Vestlus on arhiveeritud</string>
<string name="conversation_is_read_only">Vestlus on vaid loetav</string>
<string name="conversation_read_only_failed">Ei õnnestunud muuta vestlust ainult loetavaks</string>
<string name="conversations">Vestlused</string>
@ -71,6 +72,7 @@
<string name="leave_call">Lahku kõnest</string>
<string name="left_conversation">Sa lahkusid %1$s vestlusest</string>
<string name="load_more_results">Laadi veel tulemusi</string>
<string name="local_time">Kohalik aeg: %1$s</string>
<string name="lock_conversation">Lukusta vestlus</string>
<string name="lock_symbol">Lukuikoon</string>
<string name="lower_hand">Lase käsi alla</string>
@ -177,6 +179,7 @@
<string name="nc_delete_conversation_more">Kui kustutad selle vestluse, siis kustub see ka kõikide osalejate jaoks.</string>
<string name="nc_delete_message">Kustuta</string>
<string name="nc_delete_message_leaked_to_matterbridge">Sõnumi kustutamine õnnestus, kuid võis juhtuda, et ta oli juba teistesse sõnumiteenustesse edastatud</string>
<string name="nc_delete_now">Kustuta kohe</string>
<string name="nc_deleted_user">Kasutaja „%1$s“ on eemaldatud</string>
<string name="nc_demote">Võta ära moderaatori õigused</string>
<string name="nc_description_record_voice">Salvesta häälsõnum</string>
@ -282,10 +285,11 @@
<string name="nc_ignore_battery_optimization_dialog_text">Akukasutuse optimeerimine pole eiratud. Et teavituste saatmine toimiks taustal, siis peaksid seda muutma. Palun klõpsi „Sobib“ ja vali „Kõik rakendused“ → „%1$s“ → „Ära optimeeri“</string>
<string name="nc_ignore_battery_optimization_dialog_title">Eira akukasutuse optimeerimist</string>
<string name="nc_important_conversation">Oluline vestlus</string>
<string name="nc_important_conversation_desc">Selle vestluse teavituste seadistused on ülemuslikud „Ära sega“ seadistuste suhtes</string>
<string name="nc_important_conversation_desc">„Ära sega“ olek on oluliste vestluste puhul eiratud</string>
<string name="nc_invalid_time">Vigane aeg</string>
<string name="nc_invitations">Kutsed</string>
<string name="nc_join_open_conversations">Liitu avalike vestlustega</string>
<string name="nc_keep">Hoia alles</string>
<string name="nc_last_moderator_leaving_room_warning">Pead määrtama uue moderaatori enne, kui saad siis vestlusest lahkuda</string>
<string name="nc_last_modified">%1$s | Viimati muudetud: %2$s</string>
<string name="nc_leave">Lahku vestlusest</string>
@ -305,13 +309,17 @@
<string name="nc_manual">Pole määratud</string>
<string name="nc_mark_as_read">Märgi loetuks</string>
<string name="nc_mark_as_unread">Märgi mitteloetuks</string>
<string name="nc_mark_conversation_as_important">Vestlus on märgitud oluliseks</string>
<string name="nc_mark_conversation_as_insensitive">Vestlus on märgitud mittedelikaatseks</string>
<string name="nc_mark_conversation_as_sensitive">Vestlus on märgitud delikaatseks</string>
<string name="nc_mark_conversation_as_unimportant">Vestlus on märgitud mitteoluliseks</string>
<string name="nc_meeting_ended">Kohtumine on lõppenud</string>
<string name="nc_message_added_to_notes">Teade on lisatud märkmetesse</string>
<string name="nc_message_failed">Ebaõnnestus</string>
<string name="nc_message_failed_to_send">Sõnumi saatmine ei õnnestunud:</string>
<string name="nc_message_offline">Pole võrgus</string>
<string name="nc_message_quote_cancel_reply">Katkesta vastamine</string>
<string name="nc_message_read">Sõnim on loetud</string>
<string name="nc_message_read">Sõnum on loetud</string>
<string name="nc_message_sending">Saatmisel</string>
<string name="nc_message_sent">Sõnum on saadetud</string>
<string name="nc_microphone_permission_hint">Et saaksid suhtlemisel kasutada heliedastust, palun luba rakendusel kasutada mikrofoni.</string>
@ -383,6 +391,7 @@
<string name="nc_rename_confirm">Muuda nime</string>
<string name="nc_reply">Vasta</string>
<string name="nc_reply_privately">Vasta privaatselt</string>
<string name="nc_room_retention">Jututoa allesjätmine õnnestus</string>
<string name="nc_save_message">Salvesta</string>
<string name="nc_save_success">Salvestamine õnnestus</string>
<string name="nc_screen_lock_timeout_30">30 sekundit</string>
@ -397,6 +406,8 @@
<string name="nc_search">Otsi</string>
<string name="nc_select_an_account">Otsi kasutajakontot</string>
<string name="nc_send_edit_message">Uuenda sõnumit</string>
<string name="nc_sensitive_conversation">Vestlus tundlikul teemal</string>
<string name="nc_sensitive_conversation_hint">Sõnumite eelvaated saava vestluste loendis ja teavitustes olema peidetud</string>
<string name="nc_sent_a_gif" formatted="true">%1$s saatis gif-faili.</string>
<string name="nc_sent_a_gif_you">Sina saatsid gif-faili.</string>
<string name="nc_sent_a_video" formatted="true">%1$s saatis video.</string>
@ -532,6 +543,7 @@
<string name="online_status">Võrgus staatus</string>
<string name="openConversations">Ava vestlused</string>
<string name="open_in_files_app">Ava failirakenduses</string>
<string name="open_notes">Ava märkmik</string>
<string name="play_pause_voice_message">Esita häälsõnumit või peata esitus</string>
<string name="playback_speed_control">Taasesituse kiiruse juhtimine</string>
<string name="polls_add_option">Lisa valik</string>
@ -669,6 +681,10 @@
<item quantity="one">Vaata %d sarnast sõnumit</item>
<item quantity="other">Vaata %d sarnast sõnumit</item>
</plurals>
<plurals name="nc_conversation_auto_delete_info">
<item quantity="one">See vestlus kustub automaatselt kõigi osalejate jaoks, kui siin pole olnud tegevust %1$d päeva jooksul.</item>
<item quantity="other">See vestlus kustub automaatselt kõigi osalejate jaoks, kui siin pole olnud tegevust %1$d päeva jooksul.</item>
</plurals>
<plurals name="polls_amount_voters">
<item quantity="one">%d hääl</item>
<item quantity="other">%d häält</item>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Aldatu</string>
<string name="add_participants">Gehitu</string>
<string name="add_to_notes">Gehitu oharretara</string>
<string name="added_to_favorites">%1$s elkarrizketa gogokoetara gehitu da</string>
<string name="appbar_search_in">Bilatu %s(e)n</string>
@ -235,7 +236,6 @@
<string name="nc_guest_access_share_link">Partekatu elkarrizketa esteka</string>
<string name="nc_hint_enter_a_message">Idatzi mezu bat …</string>
<string name="nc_important_conversation">Elkarrizketa garrantzitsua</string>
<string name="nc_important_conversation_desc">Ez Molestatu ezarpenek sahiestuko dituzte elkarrizketa honen jakinarazpenak </string>
<string name="nc_invitations">Gonbidapenak</string>
<string name="nc_join_open_conversations">Sartu elkarrizketa irekietara</string>
<string name="nc_last_moderator_leaving_room_warning">Moderatzaile berri bat sustatu behar duzu elkarrizketatik irten aurretik</string>

View File

@ -43,6 +43,7 @@
<string name="later_today">تا آخر وقت همین امروز</string>
<string name="leave_call">Leave call</string>
<string name="load_more_results">بار کردن نتیحه‌های بیش‌تر</string>
<string name="local_time">زمان محلی: %1$s</string>
<string name="lock_conversation">Lock conversation</string>
<string name="lock_symbol">نماد قفل</string>
<string name="mentioned">Mentioned</string>
@ -175,6 +176,7 @@
<string name="nc_important_conversation">گفتگوی مهم</string>
<string name="nc_invitations">دعوت‌ها</string>
<string name="nc_join_open_conversations">پیوستن به گفتگوهای باز</string>
<string name="nc_keep">نگاه داشتن</string>
<string name="nc_last_moderator_leaving_room_warning">You need to promote a new moderator before you can leave the conversation</string>
<string name="nc_last_modified">تغییرات %1$s | آخرین تغییرات: %2$s</string>
<string name="nc_leave">ترک گفتگو</string>
@ -194,9 +196,11 @@
<string name="nc_mark_as_unread">علامت به عنوان خوانده‌نشده</string>
<string name="nc_message_failed">Failed</string>
<string name="nc_message_failed_to_send">ناتوانی در فرستادن پیام:</string>
<string name="nc_message_offline">آفلاین</string>
<string name="nc_message_quote_cancel_reply">لغو پاسخ</string>
<string name="nc_message_read">پیام، خوانده شد</string>
<string name="nc_message_sent">پیام، ارسال شد</string>
<string name="nc_missed_call">شما یک تماس از %s را از دست دادید</string>
<string name="nc_moderator">مدیر</string>
<string name="nc_new_conversation">گفتگوی جدید</string>
<string name="nc_new_conversation_visibility">Visibility</string>
@ -398,6 +402,7 @@
<string name="translation_copy_translated_text">رونوشت متن ترجمه</string>
<string name="translation_detect_language">تشخیص زبان</string>
<string name="translation_device_settings">تنظیمات افزاره</string>
<string name="translation_error_message">امکان تشخیص زبان وجود ندارد.</string>
<string name="translation_from">از</string>
<string name="translation_to">به</string>
<string name="user_avatar">آواتار کاربر</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Muokkaa</string>
<string name="add_participants">Lisää</string>
<string name="appbar_search_in">Etsi kohteesta %s</string>
<string name="archived">Arkistoitu</string>
<string name="audio_output_bluetooth">Bluetooth</string>
@ -167,7 +168,6 @@
<string name="nc_guest_access_share_link">Jaa keskustelulinkki</string>
<string name="nc_hint_enter_a_message">Kirjoita viesti…</string>
<string name="nc_important_conversation">Tärkeä keskustelu</string>
<string name="nc_important_conversation_desc">Tämän keskustelun ilmoitukset eivät noudata Älä häiritse -asetuksia</string>
<string name="nc_invitations">Kutsut</string>
<string name="nc_join_open_conversations">Liity avoimiin keskusteluihin</string>
<string name="nc_last_modified">%1$s | Viimeksi muokattu: %2$s</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Modifier</string>
<string name="add_participants">Ajouter</string>
<string name="add_to_notes">Ajouter à Notes</string>
<string name="added_to_favorites">La conversation %1$s a été ajoutée aux favoris</string>
<string name="appbar_search_in">Rechercher dans %s</string>
@ -71,6 +72,7 @@
<string name="leave_call">Quitter l\'appel</string>
<string name="left_conversation">Vous avez quitté la conversation %1$s</string>
<string name="load_more_results">Charger plus de résultats</string>
<string name="local_time">Heure locale : %1$s</string>
<string name="lock_conversation">Verrouiller la conversation</string>
<string name="lock_symbol">Symbole de verrouillage</string>
<string name="lower_hand">Baisser la main</string>
@ -177,6 +179,7 @@
<string name="nc_delete_conversation_more">Si vous supprimez la conversation, elle sera supprimée pour tous les participants.</string>
<string name="nc_delete_message">Supprimer</string>
<string name="nc_delete_message_leaked_to_matterbridge">Message supprimé avec succès, mais il pourrait avoir été divulgué à dautres services</string>
<string name="nc_delete_now">Supprimer maintenant</string>
<string name="nc_deleted_user">L\'utilisateur %1$s a été supprimé</string>
<string name="nc_demote">Destituer de modérateur</string>
<string name="nc_description_record_voice">Enregistrer un message vocal</string>
@ -282,10 +285,11 @@
<string name="nc_ignore_battery_optimization_dialog_text">L\'optimisation de la batterie n\'est pas ignorée. Ceci devrait être modifié pour vous assurer que les notifications fonctionnent en arrière-plan. Merci de cliquer OK et sélectionner \"Toutes les applications\" -> %1$s -> Ne pas optimiser</string>
<string name="nc_ignore_battery_optimization_dialog_title">Ignorer l\'optimisation de batterie</string>
<string name="nc_important_conversation">Conversation importante</string>
<string name="nc_important_conversation_desc">Les notifications dans cette conversation l\'emportent sur les paramètres « Ne Pas Déranger ».</string>
<string name="nc_important_conversation_desc">Le statut utilisateur \"Ne pas déranger\" est ignoré pour les conversations importantes.</string>
<string name="nc_invalid_time">Heure invalide</string>
<string name="nc_invitations">Invitations</string>
<string name="nc_join_open_conversations">Rejoindre des conversations ouvertes</string>
<string name="nc_keep">Conserver</string>
<string name="nc_last_moderator_leaving_room_warning">Vous devez promouvoir un nouveau modérateur avant de pouvoir quitter la conversation.</string>
<string name="nc_last_modified">%1$s | Dernière modification : %2$s</string>
<string name="nc_leave">Quitter la conversation</string>
@ -397,6 +401,8 @@
<string name="nc_search">Recherche</string>
<string name="nc_select_an_account">Choisissez un compte</string>
<string name="nc_send_edit_message">Message de mise à jour</string>
<string name="nc_sensitive_conversation">Conversation sensible</string>
<string name="nc_sensitive_conversation_hint">La prévisualisation des messages sera désactivée dans la liste des conversations et les notifiactions</string>
<string name="nc_sent_a_gif" formatted="true">%1$s a envoyé un GIF.</string>
<string name="nc_sent_a_gif_you">Vous avez envoyé un GIF.</string>
<string name="nc_sent_a_video" formatted="true">%1$s a envoyé une vidéo.</string>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name= "nc_edit">Cuir in eagar</string>
<string name="add_participants">Cuir</string>
<string name="add_to_notes">Cuir le Nótaí</string>
<string name="added_to_favorites">Cuireadh comhrá %1$s le ceanáin</string>
<string name="appbar_search_in">Cuardaigh i %s</string>
@ -71,6 +72,7 @@
<string name="leave_call">Fág glaoch</string>
<string name="left_conversation">D\'fhág tú an comhrá %1$s</string>
<string name="load_more_results">Íoslódáil níos mó torthaí</string>
<string name="local_time">Am áitiúil: %1$s</string>
<string name="lock_conversation">Cuir glas ar an gcomhrá</string>
<string name="lock_symbol">Siombail ghlais</string>
<string name="lower_hand">Lámh íochtair</string>
@ -177,6 +179,7 @@
<string name="nc_delete_conversation_more">Má scriosann tú an comhrá, scriosfar é do gach rannpháirtí eile freisin.</string>
<string name="nc_delete_message">Scrios</string>
<string name="nc_delete_message_leaked_to_matterbridge">D\'éirigh leis an teachtaireacht a scriosadh, ach seans gur sceitheadh chuig seirbhísí eile í</string>
<string name="nc_delete_now">Scrios anois</string>
<string name="nc_deleted_user">Baineadh úsáideoir %1$s</string>
<string name="nc_demote">Léim as an modhnóir</string>
<string name="nc_description_record_voice">Taifead teachtaireacht gutha</string>
@ -282,10 +285,11 @@
<string name="nc_ignore_battery_optimization_dialog_text">Ní thugtar aird ar bharrfheabhsú ceallraí. Ba cheart é seo a athrú chun a chinntiú go n-oibríonn fógraí sa chúlra! Cliceáil OK agus roghnaigh \"Gach aip\" -> %1$s -> Ná optamaigh le do thoil</string>
<string name="nc_ignore_battery_optimization_dialog_title">Déan neamhaird de bharrfheabhsú ceallraí</string>
<string name="nc_important_conversation">Comhrá tábhachtach</string>
<string name="nc_important_conversation_desc">Sáróidh fógraí sa chomhrá seo na socruithe Ná Cuir Isteach</string>
<string name="nc_important_conversation_desc">Déantar neamhaird ar stádas an úsáideora \"Ná cuir isteach\" i gcás comhráite tábhachtacha</string>
<string name="nc_invalid_time">Am neamhbhailí</string>
<string name="nc_invitations">cuirí</string>
<string name="nc_join_open_conversations">Glac páirt i gcomhráite oscailte</string>
<string name="nc_keep">Coinnigh</string>
<string name="nc_last_moderator_leaving_room_warning">Ní mór duit modhnóir nua a chur chun cinn sula bhféadfaidh tú an comhrá a fhágáil</string>
<string name="nc_last_modified">%1$s | Athraithe is déanaí: %2$s</string>
<string name="nc_leave">Fág an comhrá</string>
@ -305,6 +309,10 @@
<string name="nc_manual">Gan socraithe</string>
<string name="nc_mark_as_read">Marcáil mar léite</string>
<string name="nc_mark_as_unread">Marcáil mar neamhléite</string>
<string name="nc_mark_conversation_as_important">Comhrá marcáilte mar thábhachtach</string>
<string name="nc_mark_conversation_as_insensitive">Dímharcáladh an comhrá mar chomhrá íogair</string>
<string name="nc_mark_conversation_as_sensitive">Comhrá marcáilte mar íogair</string>
<string name="nc_mark_conversation_as_unimportant">Dímharcáladh an comhrá mar chomhrá tábhachtach</string>
<string name="nc_meeting_ended">Deireadh leis an gcruinniú</string>
<string name="nc_message_added_to_notes">Teachtaireacht curtha leis na nótaí</string>
<string name="nc_message_failed">Theip</string>
@ -383,6 +391,7 @@
<string name="nc_rename_confirm">Athainmnigh</string>
<string name="nc_reply">Freagra</string>
<string name="nc_reply_privately">Freagair go príobháideach</string>
<string name="nc_room_retention">Coinníodh an seomra go rathúil</string>
<string name="nc_save_message">Sábháil</string>
<string name="nc_save_success">Sábháilte go rathúil</string>
<string name="nc_screen_lock_timeout_30">30 soicind</string>
@ -397,6 +406,8 @@
<string name="nc_search">Cuardach</string>
<string name="nc_select_an_account">Roghnaigh cuntas</string>
<string name="nc_send_edit_message">Nuashonraigh teachtaireacht</string>
<string name="nc_sensitive_conversation">Comhrá íogair</string>
<string name="nc_sensitive_conversation_hint">Díchumasófar réamhamharc teachtaireachta sa liosta comhráite agus sna fógraí</string>
<string name="nc_sent_a_gif" formatted="true">Sheol %1$s GIF.</string>
<string name="nc_sent_a_gif_you">Sheol tú GIF.</string>
<string name="nc_sent_a_video" formatted="true">Sheol %1$s físeán.</string>

Some files were not shown because too many files have changed in this diff Show More