Compare commits

...

248 Commits

Author SHA1 Message Date
Nextcloud bot
e2a6728942
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-07-10 02:55:47 +00:00
Andy Scherzinger
e3a7298ca5
Merge pull request from nextcloud/renovate/org.jlleitschuh.gradle-ktlint-gradle-13.x
Update dependency org.jlleitschuh.gradle:ktlint-gradle to v13
2025-07-10 00:16:54 +02:00
Andy Scherzinger
8c74a3c41e
style(ktlint): Update formatting
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-07-10 00:02:19 +02:00
Andy Scherzinger
2d2a2ce601
ci(chksm): Add meta data
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-07-09 23:52:30 +02:00
Andy Scherzinger
42b5473535
ci(lint): Bump score
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-07-09 23:15:39 +02:00
Andy Scherzinger
c7676438a1
chore(deps): Update dependency android-common to v0.27.0
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-07-09 23:15:12 +02:00
Andy Scherzinger
669a528b14
style(ktlint): Fix formatting for ktlint check to succeed
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-07-09 23:15:12 +02:00
renovate[bot]
2e5bcc174c
Update dependency org.jlleitschuh.gradle:ktlint-gradle to v13
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 23:15:12 +02:00
Andy Scherzinger
e1ea3d81ae
style(searchbar): Update searchbar to m3 theming
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-07-09 23:14:05 +02:00
Andy Scherzinger
02121a4009
Merge pull request from nextcloud/renovate/com.github.nextcloud.android-common-ui-0.x
fix(deps): update dependency com.github.nextcloud.android-common:ui to v0.27.0
2025-07-09 21:54:56 +02:00
renovate[bot]
729e934c4f
fix(deps): update dependency com.github.nextcloud.android-common:ui to v0.27.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 18:48:41 +00:00
Nextcloud bot
42e976cf18
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-07-09 02:55:45 +00:00
Sowjanya Kota
e982864e55
Merge pull request from nextcloud/fix_autocomplete
Fix mentions of phone conversation
2025-07-08 13:16:17 +02:00
Sowjanya Kota
5704be67fd
Merge pull request from nextcloud/bug_fix
Scale DatePicker for smaller screens
2025-07-08 12:39:36 +02:00
Sowjanya Kota
c5942fe575
Merge pull request from nextcloud/improve_accessibility
Improve accessibility
2025-07-08 12:30:19 +02:00
sowjanyakch
2b5973e763
scale datePicker
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 12:18:07 +02:00
sowjanyakch
3ad7291cc4
maximum width
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 12:18:07 +02:00
sowjanyakch
61c0490038
fix header
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 11:16:59 +02:00
sowjanyakch
fac30aa3f5
use scaled icon + fix headers
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 11:14:22 +02:00
sowjanyakch
8787bb557b
create phone scaling icon
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 11:14:21 +02:00
sowjanyakch
e9158b291f
extract common code to PhoneUtils
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 11:14:21 +02:00
sowjanyakch
d780d4b261
use displayName instead of source to display avatars
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 11:14:21 +02:00
sowjanyakch
c2deca00ee
fix mentions
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 11:14:20 +02:00
sowjanyakch
a5a42aae61
fix mention autocomplete item
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 11:14:20 +02:00
sowjanyakch
c16a6c74fa
add circular phone icons
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 11:14:19 +02:00
Sowjanya Kota
4667fef98c
Merge pull request from nextcloud/edit_messages
Edit mentions
2025-07-08 10:57:40 +02:00
sowjanyakch
26a383e4de
add replaceMentionChipSpans function
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:26 +02:00
sowjanyakch
317984919f
avoid crash if messageParameters is null
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:25 +02:00
sowjanyakch
8b02b7797c
hide mentionAutocomplete in edit mode
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:25 +02:00
sowjanyakch
00ce1281ec
ktlint
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:25 +02:00
sowjanyakch
022077be62
refactor
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:25 +02:00
sowjanyakch
1488a33e9a
check if message is null
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:24 +02:00
sowjanyakch
3e747b34fc
remove nested block depth warning
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:24 +02:00
sowjanyakch
bbd921a031
suppress nested block depth warning
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:24 +02:00
sowjanyakch
62458fa887
format code
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:23 +02:00
sowjanyakch
85d996622d
use mention chips in edit text
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:23 +02:00
sowjanyakch
c4b3555d7f
remove unused imports
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:23 +02:00
sowjanyakch
e33e7fd0fb
editing mentions work
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:23 +02:00
sowjanyakch
15f3240f98
move edit button on click listener to setEditUI
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:22 +02:00
sowjanyakch
09905e61fe
remove unused import
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:22 +02:00
sowjanyakch
895ad61d3d
use mention name in edit text field
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:21 +02:00
sowjanyakch
f14fa59de7
also get mentions from the input textfield
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:21 +02:00
sowjanyakch
50173694ef
include mentions in edited message
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-08 10:41:21 +02:00
Marcel Hibbe
36c8f421c2
Merge pull request from nextcloud/bugfix/5126/fixContactNotClickable
fix that contact in search is clickable
2025-07-08 09:10:23 +02:00
Marcel Hibbe
41e12ec19d
Merge pull request from nextcloud/crash_fix
Crash fix
2025-07-07 12:25:47 +02:00
Marcel Hibbe
02476b10ae
fix that contact in search is clickable
A contact (so not a conversation yet!) had the VIEW_TYPE 2131558677, but 2131558687 was expected to identify the type, see FlexibleViewType.kt

The checks in onItemClick are replaced to directly change the type via kotlin "is" method.

best solution for future: FlexibleAdapter should be removed!-> replace with Compose!

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-07-07 10:45:24 +02:00
sowjanyakch
b3e89633fa
avoid null
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-07 09:26:11 +02:00
sowjanyakch
b9667e45c0
fix crash
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-07 09:23:53 +02:00
Nextcloud Android Bot
5606254a4a Weekly 22.0.0 Alpha 09 2025-07-07 03:11:38 +00:00
Andy Scherzinger
9cc950e30a
Merge pull request from nextcloud/renovate/junit-framework-monorepo
fix(deps): update dependency org.junit.vintage:junit-vintage-engine to v5.13.3
2025-07-04 20:12:49 +02:00
renovate[bot]
4f5e2ce273 fix(deps): update dependency org.junit.vintage:junit-vintage-engine to v5.13.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 19:57:47 +02:00
Andy Scherzinger
6b63a4f793
Merge pull request from nextcloud/renovate/gradle-8.x
chore(deps): update dependency gradle to v8.14.3
2025-07-04 19:56:54 +02:00
Andy Scherzinger
3af5981841
ci(lint): Update lint score
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-07-04 19:17:32 +02:00
Andy Scherzinger
d9be063891
ci(chksm): Add meta-data
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-07-04 19:06:30 +02:00
renovate[bot]
b5ec3cd2af
chore(deps): update dependency gradle to v8.14.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 18:57:43 +02:00
github-actions[bot]
fc65a7a8d0
Merge pull request from nextcloud/renovate/workversion
fix(deps): update workversion to v2.10.2
2025-07-04 16:39:11 +00:00
Andy Scherzinger
6a026c1fc7
fix(worker): Update to worker v2.10.x API
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-07-04 18:25:32 +02:00
renovate[bot]
c9f699d0d3
fix(deps): update workversion to v2.10.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 18:19:45 +02:00
Marcel Hibbe
913907a7e2
Merge pull request from nextcloud/bumpJacksonCore
bump jackson core version
2025-07-03 12:30:07 +02:00
Andy Scherzinger
7f6f077680
Merge pull request from nextcloud/style/noid/outlineIcons
🎨 Migrate to outlined icon style
2025-07-03 11:27:53 +02:00
Marcel Hibbe
632e8f85a9
remove jackson from renovate rule to ignore it
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-07-03 11:18:55 +02:00
Marcel Hibbe
42bd8d2235
bump jackson core version
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-07-03 11:04:20 +02:00
Andy Scherzinger
33ad578ef5
style(icons): Migrate from filled to outline icon style
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-07-03 10:44:05 +02:00
Nextcloud bot
b7ae981c3d
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-07-03 02:58:35 +00:00
Andy Scherzinger
81cf077f6f
Merge pull request from nextcloud/renovate/ubuntu-noble
chore(deps): update ubuntu:noble docker digest to 440dcf6
2025-07-02 19:05:20 +02:00
sowjanyakch
c208b3d0b1
use proper wording recording instead of message
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-02 18:45:48 +02:00
sowjanyakch
df2a820e88
add content description to buttons
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-02 18:45:48 +02:00
sowjanyakch
8c0432efbf
add content description to buttons
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-02 18:45:47 +02:00
sowjanyakch
9ca6acc933
add content description to buttons
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-02 18:45:47 +02:00
renovate[bot]
1b7b11ff76
chore(deps): update ubuntu:noble docker digest to 440dcf6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-02 16:09:51 +00:00
Andy Scherzinger
c8e4de6665
Merge pull request from nextcloud/renovate/ubuntu-noble
chore(deps): update ubuntu:noble docker digest to 89ef6e4
2025-07-02 12:47:59 +02:00
renovate[bot]
78cb62a3bd
chore(deps): update ubuntu:noble docker digest to 89ef6e4
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-02 10:37:59 +00:00
Marcel Hibbe
59fd9f2319
Merge pull request from nextcloud/bugfix/5106/fixCrashWhenSendingMessage
fix crash when sending message
2025-07-01 13:12:05 +02:00
Marcel Hibbe
74a1c5aeb0
fix crash when sending message
-> fix to handle null for getTempMessageForConversation

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-07-01 12:21:24 +02:00
Sowjanya Kota
a3a5272dbb
Merge pull request from nextcloud/fix_delete_all_messages
Show delete all messages in 1:1 only when canDeleteConversation is true
2025-07-01 11:36:38 +02:00
sowjanyakch
dd77ce7c4c
ktlint
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-01 11:02:38 +02:00
sowjanyakch
94db4e6892
show delete all messages option in 1:1 conversation when canDeleteConversation is true
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-01 11:02:37 +02:00
Andy Scherzinger
99d5fc7d19
Merge pull request from nextcloud/renovate/nextcloud-pr-feedback-action-digest
chore(deps): update nextcloud/pr-feedback-action digest to e397f3c
2025-07-01 10:48:15 +02:00
renovate[bot]
ba0d37020b
chore(deps): update nextcloud/pr-feedback-action digest to e397f3c
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 07:44:58 +00:00
Sowjanya Kota
c19cba3c0d
Merge pull request from nextcloud/test_push_notification
document about test push button in diagnosis screen
2025-07-01 09:43:37 +02:00
Nextcloud bot
2d0f0d9f73
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-07-01 02:54:26 +00:00
Andy Scherzinger
cdb1c4b0f8
Merge pull request from nextcloud/renovate/org.jetbrains.kotlinx-kotlinx-serialization-json-1.x
fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.9.0
2025-06-30 21:59:21 +02:00
Andy Scherzinger
ea0588cb99
Merge pull request from nextcloud/renovate/com.google.firebase-firebase-messaging-24.x
fix(deps): update dependency com.google.firebase:firebase-messaging to v24.1.2
2025-06-30 21:47:31 +02:00
renovate[bot]
ea19f60ef3
fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.9.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 19:26:34 +00:00
renovate[bot]
3096a73585
fix(deps): update dependency com.google.firebase:firebase-messaging to v24.1.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 19:21:04 +00:00
sowjanyakch
dbf5c84050
test push button in diagnosis screen
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-06-30 19:12:11 +02:00
Andy Scherzinger
4c7d6f90ec
Merge pull request from nextcloud/renovate/github-codeql-action-3.x
chore(deps): update github/codeql-action action to v3.29.2
2025-06-30 16:37:33 +02:00
renovate[bot]
3930d4b740
chore(deps): update github/codeql-action action to v3.29.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 13:15:54 +00:00
Marcel Hibbe
9b3f508652
Merge pull request from nextcloud/renovate/ksp-monorepo
chore(deps): update plugin com.google.devtools.ksp to v2.2.0-2.0.2
2025-06-30 10:34:27 +00:00
renovate[bot]
28556e12cd
chore(deps): update plugin com.google.devtools.ksp to v2.2.0-2.0.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 12:20:26 +02:00
Marcel Hibbe
bd8ad9e672
Merge pull request from nextcloud/updateVerificationMetadata
update verification metadata
2025-06-30 09:39:38 +00:00
Marcel Hibbe
0a44067a58
update verification metadata
..especially to fix failed verification of
android-common:material-color-utilities

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-30 11:13:38 +02:00
Nextcloud bot
4d24716a4d
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-30 03:43:14 +00:00
Nextcloud Android Bot
1648c61b06 Weekly 22.0.0 Alpha 08 2025-06-30 03:12:18 +00:00
Nextcloud bot
bd050f0bc0
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-27 16:31:13 +00:00
Andy Scherzinger
f7997d339b
Merge pull request from nextcloud/renovate/github-codeql-action-3.x
chore(deps): update github/codeql-action action to v3.29.1
2025-06-27 16:38:47 +02:00
renovate[bot]
28cacf2a90
chore(deps): update github/codeql-action action to v3.29.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 12:01:34 +00:00
Andy Scherzinger
890a7febc2
Merge pull request from nextcloud/feat/workflow-auto-update-reuse.yml
ci: update reuse.yml workflow from template
2025-06-27 14:00:11 +02:00
Nextcloud bot
48e8caac0a ci: update reuse.yml workflow from template
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-27 10:21:08 +00:00
Tobias Kaminsky
a6fbac92c2
Merge pull request from nextcloud/repo-sync/android-config/master
🔄 synced file(s) with nextcloud/android-config
2025-06-27 10:59:25 +02:00
nextcloud-android-bot
439912754b 🔄 synced local '.github/workflows/' with remote 'config/workflows/'
Signed-off-by: nextcloud-android-bot <android@nextcloud.com>
2025-06-27 08:18:05 +00:00
Nextcloud bot
8792ac1dbd
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-26 03:09:14 +00:00
github-actions[bot]
adc0be1d1d
Merge pull request from nextcloud/renovate/major-retrofit-monorepo
fix(deps): update retrofit monorepo to v3 (major)
2025-06-25 19:31:02 +00:00
renovate[bot]
5fccf47353 fix(deps): update retrofit monorepo to v3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 21:16:53 +02:00
github-actions[bot]
324e02a807
Merge pull request from nextcloud/renovate/media3_version
fix(deps): update media3_version to v1.7.1
2025-06-25 18:57:11 +00:00
github-actions[bot]
94894b17a6
Merge pull request from nextcloud/renovate/androidx.compose.runtime-runtime-1.x
fix(deps): update dependency androidx.compose.runtime:runtime to v1.8.3
2025-06-25 18:55:40 +00:00
renovate[bot]
f1a0d8c70a fix(deps): update media3_version to v1.7.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 20:21:28 +02:00
renovate[bot]
0edc4ea337 fix(deps): update dependency androidx.compose.runtime:runtime to v1.8.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 20:21:07 +02:00
github-actions[bot]
2fe295ef63
Merge pull request from nextcloud/renovate/androidx.activity-activity-compose-1.x
fix(deps): update dependency androidx.activity:activity-compose to v1.10.1
2025-06-25 20:04:34 +02:00
github-actions[bot]
9723d298ed
Merge pull request from nextcloud/renovate/lifecycleversion
fix(deps): update lifecycleversion to v2.9.1
2025-06-25 20:04:01 +02:00
github-actions[bot]
44f201800f
Merge pull request from nextcloud/renovate/org.junit.vintage-junit-vintage-engine-5.x
fix(deps): update dependency org.junit.vintage:junit-vintage-engine to v5.13.2
2025-06-25 17:52:55 +00:00
renovate[bot]
375d7316d8 fix(deps): update dependency androidx.activity:activity-compose to v1.10.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 19:45:18 +02:00
renovate[bot]
d89c243904 fix(deps): update lifecycleversion to v2.9.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 19:44:36 +02:00
github-actions[bot]
22a780d7c8
Merge pull request from nextcloud/renovate/roomversion
fix(deps): update roomversion to v2.7.2
2025-06-25 13:47:13 +00:00
github-actions[bot]
0b95237991
Merge pull request from nextcloud/renovate/org.jetbrains.kotlin.plugin.compose-2.x
chore(deps): update plugin org.jetbrains.kotlin.plugin.compose to v2.2.0
2025-06-25 12:54:16 +00:00
github-actions[bot]
351173bb6b
Merge pull request from nextcloud/renovate/androidx.compose.ui-ui-test-junit4-1.x
fix(deps): update dependency androidx.compose.ui:ui-test-junit4 to v1.8.3
2025-06-25 10:51:18 +00:00
renovate[bot]
1aa2d2aaaa
fix(deps): update dependency androidx.compose.ui:ui-test-junit4 to v1.8.3
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 10:38:48 +00:00
github-actions[bot]
06df0084cf
Merge pull request from nextcloud/renovate/com.android.tools.build-gradle-8.x
fix(deps): update dependency com.android.tools.build:gradle to v8.11.0
2025-06-25 10:26:47 +00:00
github-actions[bot]
ca73e93aeb
Merge pull request from nextcloud/renovate/kotlin-monorepo
fix(deps): update kotlin monorepo to v2.2.0
2025-06-25 12:17:25 +02:00
Andy Scherzinger
c305816f6d
ci(chksm): Add meta-data
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-06-25 12:08:05 +02:00
renovate[bot]
8bc31cdbd4
fix(deps): update dependency com.android.tools.build:gradle to v8.11.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 11:59:08 +02:00
github-actions[bot]
0d9402f496
Merge pull request 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.1
2025-06-25 11:54:41 +02:00
renovate[bot]
9867ed08ac
fix(deps): update kotlin monorepo to v2.2.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 09:47:23 +00:00
renovate[bot]
ef5f6b67f6
chore(deps): update plugin org.jetbrains.kotlin.plugin.compose to v2.2.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 09:41:34 +00:00
renovate[bot]
5cd84f633c
fix(deps): update dependency org.junit.vintage:junit-vintage-engine to v5.13.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 09:40:01 +00:00
renovate[bot]
d7d139c861
fix(deps): update dependency com.github.spotbugs.snom:spotbugs-gradle-plugin to v6.2.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 09:37:40 +00:00
Marcel Hibbe
5612cfa19d
Merge pull request from nextcloud/bugfix/5073/fixInfinityMaximumHeight
fix crash in call view
2025-06-25 07:59:39 +00:00
Nextcloud bot
b4d5af8a4c
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-25 03:09:28 +00:00
renovate[bot]
5ac7e8357b
fix(deps): update roomversion to v2.7.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 11:35:40 +00:00
github-actions[bot]
156d142ba0
Merge pull request from nextcloud/renovate/com.mebigfatguy.fb-contrib-fb-contrib-7.x
fix(deps): update dependency com.mebigfatguy.fb-contrib:fb-contrib to v7.6.11
2025-06-24 11:28:21 +00:00
renovate[bot]
207408a606
fix(deps): update dependency com.mebigfatguy.fb-contrib:fb-contrib to v7.6.11
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 11:15:30 +00:00
Marcel Hibbe
5a7f4924ab
fix IllegalStateException for LazyVerticalGrid
LazyVerticalGrid was measured with infinite height, which Compose does not allow.
By applying the availableHeight explicitly, this should fix the Exception:

Exception java.lang.IllegalStateException: Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()). If you want to add a header before the list of items please add a header as a separate item() before the main items() inside the LazyColumn scope. There are could be other reasons for this to happen: your ComposeView was added into a LinearLayout with some weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a custom layout. Please try to remove the source of infinite constraints in the hierarchy above the scrolling container.
  at androidx.compose.foundation.CheckScrollableContainerConstraintsKt.checkScrollableContainerConstraints-K40F9xA (CheckScrollableContainerConstraints.kt:35)
  at androidx.compose.foundation.lazy.grid.LazyGridKt$rememberLazyGridMeasurePolicy$1$1.invoke-0kLqBqw (LazyGrid.kt:174)
  at androidx.compose.foundation.lazy.grid.LazyGridKt$rememberLazyGridMeasurePolicy$1$1.invoke (LazyGrid.kt:172)
  at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$3$2$1.invoke-0kLqBqw (LazyLayout.kt:119)
  at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$3$2$1.invoke (LazyLayout.kt:112)
  at androidx.compose.ui.layout.LayoutNodeSubcompositionsState$createMeasurePolicy$1.measure-3p2s80s (SubcomposeLayout.kt:725)
  at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0 (InnerNodeCoordinator.kt:135)
  at androidx.compose.ui.graphics.SimpleGraphicsLayerModifier.measure-3p2s80s (GraphicsLayerModifier.kt:646)
  at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0 (LayoutModifierNodeCoordinator.kt:188)
  at androidx.compose.foundation.layout.FillNode.measure-3p2s80s (Size.kt:699)
  at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0 (LayoutModifierNodeCoordinator.kt:188)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke (LayoutNodeLayoutDelegate.kt:316)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke (LayoutNodeLayoutDelegate.kt:315)
  at androidx.compose.runtime.snapshots.Snapshot$Companion.observe (Snapshot.kt:503)
  at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe (SnapshotStateObserver.kt:502)
  at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads (SnapshotStateObserver.kt:258)
  at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release (OwnerSnapshotObserver.kt:133)
  at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release (OwnerSnapshotObserver.kt:113)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0 (LayoutNodeLayoutDelegate.kt:1782)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0 (LayoutNodeLayoutDelegate.kt:40)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0 (LayoutNodeLayoutDelegate.kt:696)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.measure-BRTryo0 (LayoutNodeLayoutDelegate.kt:672)
  at androidx.compose.foundation.layout.BoxMeasurePolicy.measure-3p2s80s (Box.kt:151)
  at androidx.compose.foundation.layout.BoxWithConstraintsKt$BoxWithConstraints$1$1.invoke-0kLqBqw (BoxWithConstraints.kt:70)
  at androidx.compose.foundation.layout.BoxWithConstraintsKt$BoxWithConstraints$1$1.invoke (BoxWithConstraints.kt:67)
  at androidx.compose.ui.layout.LayoutNodeSubcompositionsState$createMeasurePolicy$1.measure-3p2s80s (SubcomposeLayout.kt:725)
  at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0 (InnerNodeCoordinator.kt:135)
  at androidx.compose.foundation.layout.FillNode.measure-3p2s80s (Size.kt:699)
  at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0 (LayoutModifierNodeCoordinator.kt:188)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke (LayoutNodeLayoutDelegate.kt:316)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke (LayoutNodeLayoutDelegate.kt:315)
  at androidx.compose.runtime.snapshots.Snapshot$Companion.observe (Snapshot.kt:503)
  at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe (SnapshotStateObserver.kt:502)
  at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads (SnapshotStateObserver.kt:258)
  at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release (OwnerSnapshotObserver.kt:133)
  at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release (OwnerSnapshotObserver.kt:113)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0 (LayoutNodeLayoutDelegate.kt:1782)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0 (LayoutNodeLayoutDelegate.kt:40)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0 (LayoutNodeLayoutDelegate.kt:696)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.measure-BRTryo0 (LayoutNodeLayoutDelegate.kt:672)
  at androidx.compose.ui.layout.RootMeasurePolicy.measure-3p2s80s (RootMeasurePolicy.kt:38)
  at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0 (InnerNodeCoordinator.kt:135)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke (LayoutNodeLayoutDelegate.kt:316)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke (LayoutNodeLayoutDelegate.kt:315)
  at androidx.compose.runtime.snapshots.Snapshot$Companion.observe (Snapshot.kt:2441)
  at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe (SnapshotStateObserver.kt:502)
  at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads (SnapshotStateObserver.kt:258)
  at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release (OwnerSnapshotObserver.kt:133)
  at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release (OwnerSnapshotObserver.kt:113)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0 (LayoutNodeLayoutDelegate.kt:1782)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0 (LayoutNodeLayoutDelegate.kt:40)
  at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0 (LayoutNodeLayoutDelegate.kt:696)
  at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release (LayoutNode.kt:1222)
  at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-sdFAvZA (MeasureAndLayoutDelegate.kt:367)
  at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureOnly (MeasureAndLayoutDelegate.kt:622)
  at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureOnly (MeasureAndLayoutDelegate.kt:420)
  at androidx.compose.ui.platform.AndroidComposeView.onMeasure (AndroidComposeView.android.kt:1370)
  at android.view.View.measure (View.java:27557)
  at androidx.compose.ui.platform.AbstractComposeView.internalOnMeasure$ui_release (ComposeView.android.kt:309)
  at androidx.compose.ui.platform.AbstractComposeView.onMeasure (ComposeView.android.kt:296)
  at android.view.View.measure (View.java:27557)
  at android.widget.RelativeLayout.measureChild (RelativeLayout.java:696)
  at android.widget.RelativeLayout.onMeasure (RelativeLayout.java:499)
  at android.view.View.measure (View.java:27557)
  at android.widget.LinearLayout.measureVertical (LinearLayout.java:1031)
  at android.widget.LinearLayout.onMeasure (LinearLayout.java:721)
  at android.view.View.measure (View.java:27557)
  at android.widget.RelativeLayout.measureChildHorizontal (RelativeLayout.java:735)
  at android.widget.RelativeLayout.onMeasure (RelativeLayout.java:481)
  at android.view.View.measure (View.java:27557)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:7123)
  at android.widget.FrameLayout.onMeasure (FrameLayout.java:194)
  at androidx.appcompat.widget.ContentFrameLayout.onMeasure (ContentFrameLayout.java:141)
  at android.view.View.measure (View.java:27557)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:7123)
  at android.widget.LinearLayout.measureChildBeforeLayout (LinearLayout.java:1608)
  at android.widget.LinearLayout.measureVertical (LinearLayout.java:878)
  at android.widget.LinearLayout.onMeasure (LinearLayout.java:721)
  at android.view.View.measure (View.java:27557)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:7123)
  at android.widget.FrameLayout.onMeasure (FrameLayout.java:194)
  at android.view.View.measure (View.java:27557)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:7123)
  at android.widget.LinearLayout.measureChildBeforeLayout (LinearLayout.java:1608)
  at android.widget.LinearLayout.measureVertical (LinearLayout.java:878)
  at android.widget.LinearLayout.onMeasure (LinearLayout.java:721)
  at android.view.View.measure (View.java:27557)
  at android.view.ViewGroup.measureChildWithMargins (ViewGroup.java:7123)
  at android.widget.FrameLayout.onMeasure (FrameLayout.java:194)
  at com.android.internal.policy.DecorView.onMeasure (DecorView.java:824)
  at android.view.View.measure (View.java:27557)
  at android.view.ViewRootImpl.performMeasure (ViewRootImpl.java:4682)
  at android.view.ViewRootImpl.measureHierarchy (ViewRootImpl.java:3098)
  at android.view.ViewRootImpl.performTraversals (ViewRootImpl.java:3461)
  at android.view.ViewRootImpl.doTraversal (ViewRootImpl.java:2765)
  at android.view.ViewRootImpl$TraversalRunnable.run (ViewRootImpl.java:10219)
  at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1544)
  at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1553)
  at android.view.Choreographer.doCallbacks (Choreographer.java:1109)
  at android.view.Choreographer.doFrame (Choreographer.java:994)
  at android.view.Choreographer$FrameDisplayEventReceiver.run (Choreographer.java:1527)
  at android.os.Handler.handleCallback (Handler.java:958)
  at android.os.Handler.dispatchMessage (Handler.java:99)
  at android.os.Looper.loopOnce (Looper.java:257)
  at android.os.Looper.loop (Looper.java:368)
  at android.app.ActivityThread.main (ActivityThread.java:8839)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:572)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1049)

Also:
Suppress UnusedBoxWithConstraintsScope as it seems that the maxHeight and maxWidth variables are not recognized as being used when they are "only" used in calculations.

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-24 13:04:12 +02:00
github-actions[bot]
f53b90cdf6
Merge pull request from nextcloud/renovate/androidx.core-core-ktx-1.x
fix(deps): update dependency androidx.core:core-ktx to v1.16.0
2025-06-24 10:59:45 +00:00
renovate[bot]
8980bd0f64
fix(deps): update dependency androidx.core:core-ktx to v1.16.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 10:44:43 +00:00
github-actions[bot]
6b0142c6c3
Merge pull request from nextcloud/renovate/androidx.activity-activity-ktx-1.x
fix(deps): update dependency androidx.activity:activity-ktx to v1.10.1
2025-06-24 12:36:53 +02:00
renovate[bot]
011ea9dc82
fix(deps): update dependency androidx.activity:activity-ktx to v1.10.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 10:21:17 +00:00
github-actions[bot]
c95bed4be5
Merge pull request from nextcloud/renovate/androidx.compose-compose-bom-2025.x
fix(deps): update dependency androidx.compose:compose-bom to v2025.06.01
2025-06-24 12:14:42 +02:00
renovate[bot]
1a6e18b003
fix(deps): update dependency androidx.compose:compose-bom to v2025.06.01
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 09:58:00 +00:00
Nextcloud bot
87d75102d4
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-24 03:07:36 +00:00
Marcel Hibbe
c110ecdc2b
Merge pull request from nextcloud/targetAndroid15
Android 15 support
2025-06-23 11:00:09 +00:00
Nextcloud Android Bot
fa8a72bc74 Weekly 22.0.0 Alpha 07 2025-06-23 03:10:03 +00:00
Nextcloud bot
a8dd25a285
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-21 03:10:13 +00:00
Marcel Hibbe
486d8d1035
use full space for search text field
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 12:07:47 +02:00
Marcel Hibbe
84d09d0e58
Add clear search button
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 12:02:34 +02:00
Marcel Hibbe
cc270848b5
add extra space at bottom of conversations list
by adding a SpacerItem (legacy code...-> migrate to Compose!)

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:16 +02:00
Marcel Hibbe
8ac9427ba6
simplify initSystemBars
.. by not executing duplicated checks and not using the common lib for now (needs to be aligned if possible)

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:16 +02:00
Marcel Hibbe
6905c78796
add paddings to static layouts
... to improve edge to edge design.

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:16 +02:00
Marcel Hibbe
4a4f4fc8e1
Make navigationBar fully transparent for Android 15
= Change navigationBarStyle to SystemBarStyle.light

This is done to align with the XML screens, see comment.

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:16 +02:00
Marcel Hibbe
02eab6766a
Replace method initStatusBar (use from common lib)
add comment on adjustUIForAPILevel35

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:16 +02:00
Marcel Hibbe
6effeef2ee
solve detekt warnings
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:15 +02:00
Marcel Hibbe
d3d3776a8f
solve detekt warnings
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:15 +02:00
Marcel Hibbe
d97de1bdd2
rename method
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:15 +02:00
Marcel Hibbe
e94e2b63ae
fix lint warnings
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:15 +02:00
Marcel Hibbe
5c3cc89084
add paddings
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:14 +02:00
Marcel Hibbe
c2d3340ddb
add transparent navigationBar for all composables
color statusbar for composable screens by adding a colored Box where the statusBar is (or fallback to coloring via window.statusBarColor)

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:14 +02:00
Marcel Hibbe
cc9ca58a69
add edge to edge support (while handling special cases for xml vs compose and keyboard handling)
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:14 +02:00
Marcel Hibbe
21932df918
fix top padding for chat + use android-common 0.26
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:13 +02:00
Marcel Hibbe
f29d174a61
rename setupSystemColors to initSystemBars
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:13 +02:00
Marcel Hibbe
8e3909efbc
handle edge to edge support for android 15
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:13 +02:00
Marcel Hibbe
9d3ad47e20
WIP targetSdkVersion 35
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-20 10:57:13 +02:00
Nextcloud bot
f7fcc74c68
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-20 03:07:18 +00:00
Nextcloud bot
0eebf8b2d0
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-19 03:07:37 +00:00
Sowjanya Kota
95f8b08a19
Merge pull request 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
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
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
402 changed files with 8054 additions and 2603 deletions
.devcontainer
.drone.yml
.github/workflows
CHANGELOG.md
app
build.gradle
schemas/com.nextcloud.talk.data.source.local.TalkDatabase
src
androidTest/java/com/nextcloud/talk
gplay/java/com/nextcloud/talk/utils
main/java/com/nextcloud/talk
PhoneUtils.kt
account
activities
adapters
application
arbitrarystorage
bottomsheet/items
call
chat
components
contacts
conversation
conversationcreation
conversationinfo
conversationinfoedit
conversationlist
dagger/modules
data

View File

@ -1,4 +1,4 @@
FROM ubuntu:noble@sha256:6015f66923d7afbc53558d7ccffd325d43b4e249f41a6e93eef074c9505d2233
FROM ubuntu:noble@sha256:440dcf6a5640b2ae5c77724e68787a906afb8ddee98bf86db94eea8528c2c076
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@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
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@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
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@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2

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@e397f3c7e655092b746e3610d121545530c6a90e # main
with:
feedback-message: |
Hello there,

View File

@ -29,8 +29,6 @@ jobs:
permissions:
# for hmarr/auto-approve-action to approve PRs
pull-requests: write
# for alexwilson/enable-github-automerge-action to approve PRs
contents: write
steps:
- name: Disabled on forks
@ -46,13 +44,18 @@ jobs:
# GitHub actions bot approve
- uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363 # v4.0.0
if: startsWith(steps.branchname.outputs.branch, 'renovate/')
if: github.actor == 'renovate[bot]'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# Enable GitHub auto merge
- name: Auto merge
uses: alexwilson/enable-github-automerge-action@56e3117d1ae1540309dc8f7a9f2825bc3c5f06ff # v2.0.0
if: startsWith(steps.branchname.outputs.branch, 'renovate/')
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.head_ref }}
# Enable GitHub auto merge
- name: Enable Pull Request Automerge
if: github.actor == 'renovate[bot]'
run: gh pr merge --merge --auto
env:
GH_TOKEN: ${{ secrets.AUTOMERGE }}

View File

@ -12,11 +12,11 @@ name: REUSE Compliance Check
on: [pull_request]
permissions:
contents: read
contents: read
jobs:
reuse-compliance-check:
runs-on: ubuntu-latest
runs-on: ubuntu-latest-low
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

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@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
with:
sarif_file: results.sarif

View File

@ -33,7 +33,7 @@ jobs:
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
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

@ -13,9 +13,9 @@ import com.github.spotbugs.snom.Effort
import com.github.spotbugs.snom.SpotBugsTask
plugins {
id "org.jetbrains.kotlin.plugin.compose" version "2.1.21"
id "org.jetbrains.kotlin.plugin.compose" version "2.2.0"
id "org.jetbrains.kotlin.kapt"
id 'com.google.devtools.ksp' version '2.1.21-2.0.1'
id 'com.google.devtools.ksp' version '2.2.0-2.0.2'
}
apply plugin: 'com.android.application'
@ -28,19 +28,19 @@ apply plugin: "org.jlleitschuh.gradle.ktlint"
apply plugin: 'kotlinx-serialization'
android {
compileSdk 34
compileSdk 35
namespace 'com.nextcloud.talk'
defaultConfig {
minSdkVersion 26
targetSdkVersion 34
targetSdkVersion 35
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable)
// xx .xxx .xx .xx
versionCode 220000003
versionName "22.0.0 Alpha 03"
versionCode 220000009
versionName "22.0.0 Alpha 09"
flavorDimensions "default"
renderscriptTargetApi 19
@ -155,18 +155,18 @@ ext {
daggerVersion = "2.56.2"
emojiVersion = "1.5.0"
fidoVersion = "4.1.0-patch2"
lifecycleVersion = '2.8.7'
lifecycleVersion = '2.9.1'
okhttpVersion = "4.12.0"
markwonVersion = "4.6.2"
materialDialogsVersion = "3.3.0"
parcelerVersion = "1.1.13"
prismVersion = "2.0.0"
retrofit2Version = "2.11.0"
roomVersion = "2.7.1"
workVersion = "2.9.1"
retrofit2Version = "3.0.0"
roomVersion = "2.7.2"
workVersion = "2.10.2"
espressoVersion = "3.6.1"
androidxTestVersion = "1.5.0"
media3_version = "1.4.1"
media3_version = "1.7.1"
coroutines_version = "1.10.2"
mockitoKotlinVersion = "5.4.0"
}
@ -180,10 +180,10 @@ 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.11'
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.8")
implementation("androidx.compose.runtime:runtime:1.7.8")
implementation("androidx.compose.runtime:runtime:1.8.3")
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.datastore:datastore-core:1.1.7'
implementation 'androidx.datastore:datastore-preferences:1.1.7'
@ -191,9 +191,9 @@ dependencies {
implementation fileTree(include: ['*'], dir: 'libs')
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
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"
@ -236,7 +236,7 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}"
implementation 'com.bluelinelabs:logansquare:1.3.7'
implementation 'com.fasterxml.jackson.core:jackson-core:2.14.3'
implementation 'com.fasterxml.jackson.core:jackson-core:2.19.1'
kapt 'com.bluelinelabs:logansquare-compiler:1.3.7'
implementation "com.squareup.retrofit2:retrofit:${retrofit2Version}"
@ -301,24 +301,24 @@ dependencies {
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
})
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.9.3'
implementation 'com.github.nextcloud.android-common:ui:0.23.2'
implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.activity:activity-ktx:1.10.1'
implementation 'com.github.nextcloud.android-common:ui:0.27.0'
implementation 'com.github.nextcloud-deps:android-talk-webrtc:132.6834.0'
gplayImplementation 'com.google.android.gms:play-services-base:18.6.0'
gplayImplementation "com.google.firebase:firebase-messaging:24.1.1"
gplayImplementation "com.google.firebase:firebase-messaging:24.1.2"
//compose
implementation(platform("androidx.compose:compose-bom:2025.04.00"))
implementation(platform("androidx.compose:compose-bom:2025.06.01"))
implementation("androidx.compose.ui:ui")
implementation 'androidx.compose.material3:material3:1.3.2'
implementation("androidx.compose.ui:ui-tooling-preview")
implementation 'androidx.activity:activity-compose:1.9.3'
implementation 'androidx.activity:activity-compose:1.10.1'
debugImplementation("androidx.compose.ui:ui-tooling")
//tests
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.7.8")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.8.3")
debugImplementation("androidx.compose.ui:ui-test-manifest")
testImplementation 'junit:junit:4.13.2'
@ -343,11 +343,11 @@ dependencies {
androidTestImplementation('com.android.support.test.espresso:espresso-intents:3.0.2')
androidTestImplementation(platform("androidx.compose:compose-bom:2025.04.00"))
androidTestImplementation(platform("androidx.compose:compose-bom:2025.06.01"))
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.3'
}
tasks.register('installGitHooks', Copy) {

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

@ -180,8 +180,8 @@ class ChatBlocksDaoTest {
scheduledForDeletion = java.lang.Boolean.FALSE
)
private fun createConversationEntity(accountId: Long, token: String, roomName: String): ConversationEntity {
return ConversationEntity(
private fun createConversationEntity(accountId: Long, token: String, roomName: String) =
ConversationEntity(
internalId = "$accountId@$token",
accountId = accountId,
token = token,
@ -229,5 +229,4 @@ class ChatBlocksDaoTest {
participantType = Participant.ParticipantType.DUMMY,
recordingConsentRequired = 1
)
}
}

View File

@ -23,8 +23,8 @@ class ShareUtilsIT {
assertEquals(TEST_DATE_IN_MILLIS, HttpUtils.parseDate("Mon, 09 Apr 2008 23:55:38 GMT")?.time)
}
private fun parseDate2(dateStr: String): Date {
return DateUtils.parseDate(
private fun parseDate2(dateStr: String): Date =
DateUtils.parseDate(
dateStr, Locale.US,
HttpUtils.httpDateFormatStr,
// RFC 822, updated by RFC 1123 with any TZ
@ -48,7 +48,6 @@ class ShareUtilsIT {
// RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com
"EEE MMM d yyyy HH:mm:ss z"
)
}
companion object {
private const val TEST_DATE_IN_MILLIS = 1207778138000

View File

@ -24,7 +24,9 @@ import com.nextcloud.talk.jobs.GetFirebasePushTokenWorker
import java.util.concurrent.TimeUnit
@AutoInjector(NextcloudTalkApplication::class)
class ClosedInterfaceImpl : ClosedInterface, ProviderInstaller.ProviderInstallListener {
class ClosedInterfaceImpl :
ClosedInterface,
ProviderInstaller.ProviderInstallListener {
override val isGooglePlayServicesAvailable: Boolean = isGPlayServicesAvailable()

View File

@ -0,0 +1,12 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk
object PhoneUtils {
fun isPhoneNumber(input: String?): Boolean = input?.matches(Regex("^\\+?\\d+$")) == true
}

View File

@ -91,7 +91,7 @@ class AccountVerificationActivity : BaseActivity() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
setContentView(binding.root)
actionBar?.hide()
setupSystemColors()
initSystemBars()
handleIntent()
}
@ -490,9 +490,9 @@ class AccountVerificationActivity : BaseActivity() {
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
.observeForever { workInfo: WorkInfo ->
.observeForever { workInfo: WorkInfo? ->
when (workInfo.state) {
when (workInfo?.state) {
WorkInfo.State.SUCCEEDED -> {
val intent = Intent(this, ServerSelectionActivity::class.java)
startActivity(intent)

View File

@ -78,7 +78,7 @@ class ServerSelectionActivity : BaseActivity() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
setContentView(binding.root)
actionBar?.hide()
setupSystemColors()
initSystemBars()
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
@ -154,10 +154,9 @@ class ServerSelectionActivity : BaseActivity() {
)
}
private fun isAbleToShowProviderLink(): Boolean {
return !resources!!.getBoolean(R.bool.hide_provider) &&
private fun isAbleToShowProviderLink(): Boolean =
!resources!!.getBoolean(R.bool.hide_provider) &&
!TextUtils.isEmpty(resources!!.getString(R.string.nc_providers_url))
}
private fun showImportAccountsInfo(availableAccounts: List<Account>) {
if (!TextUtils.isEmpty(
@ -204,9 +203,8 @@ class ServerSelectionActivity : BaseActivity() {
}
}
private fun isImportAccountNameSet(): Boolean {
return !TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type))
}
private fun isImportAccountNameSet(): Boolean =
!TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type))
@SuppressLint("LongLogTag")
@Suppress("Detekt.TooGenericExceptionCaught")
@ -362,9 +360,8 @@ class ServerSelectionActivity : BaseActivity() {
})
}
private fun isServerStatusQueryable(status: Status): Boolean {
return status.installed && !status.maintenance && !status.needsUpgrade
}
private fun isServerStatusQueryable(status: Status): Boolean =
status.installed && !status.maintenance && !status.needsUpgrade
private fun setErrorText(text: String?) {
binding.errorWrapper.visibility = View.VISIBLE

View File

@ -86,7 +86,7 @@ class SwitchAccountActivity : BaseActivity() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
setContentView(binding.root)
setupActionBar()
setupSystemColors()
initSystemBars()
Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context))

View File

@ -114,7 +114,7 @@ class WebViewLoginActivity : BaseActivity() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
setContentView(binding.root)
actionBar?.hide()
setupSystemColors()
initSystemBars()
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
handleIntent()
@ -155,7 +155,9 @@ class WebViewLoginActivity : BaseActivity() {
val webauthnOptionsBuilder = WebauthnDialogOptions.builder().setShowSdkLogo(true).setAllowSkipPin(true)
webViewWebauthnBridge = WebViewWebauthnBridge.createInstanceForWebView(
this, binding.webview, webauthnOptionsBuilder
this,
binding.webview,
webauthnOptionsBuilder
)
CookieSyncManager.createInstance(this)
@ -286,6 +288,7 @@ class WebViewLoginActivity : BaseActivity() {
}
}
@SuppressLint("DiscouragedPrivateApi")
@Suppress("Detekt.TooGenericExceptionCaught")
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
try {
@ -388,9 +391,9 @@ class WebViewLoginActivity : BaseActivity() {
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
.observeForever { workInfo: WorkInfo ->
.observeForever { workInfo: WorkInfo? ->
when (workInfo.state) {
when (workInfo?.state) {
WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> {
restartApp()
}
@ -429,7 +432,8 @@ class WebViewLoginActivity : BaseActivity() {
return null
}
}
return if (!TextUtils.isEmpty(loginData.serverUrl) && !TextUtils.isEmpty(loginData.username) &&
return if (!TextUtils.isEmpty(loginData.serverUrl) &&
!TextUtils.isEmpty(loginData.username) &&
!TextUtils.isEmpty(loginData.token)
) {
loginData

View File

@ -11,11 +11,13 @@ package com.nextcloud.talk.activities
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.webkit.SslErrorHandler
@ -37,6 +39,7 @@ import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.FileViewerUtils
import com.nextcloud.talk.utils.UriUtils
import com.nextcloud.talk.utils.adjustUIForAPILevel35
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.preferences.AppPreferences
@ -81,6 +84,7 @@ open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
adjustUIForAPILevel35()
super.onCreate(savedInstanceState)
cleanTempCertPreference()
@ -111,9 +115,22 @@ open class BaseActivity : AppCompatActivity() {
eventBus.unregister(this)
}
fun setupSystemColors() {
colorizeStatusBar()
colorizeNavigationBar()
/*
* May be aligned with android-common lib in the future: .../ui/util/extensions/AppCompatActivityExtensions.kt
*/
fun initSystemBars() {
window.decorView.setOnApplyWindowInsetsListener { view, insets ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
val statusBarHeight = insets.getInsets(WindowInsets.Type.statusBars()).top
view.setPadding(0, statusBarHeight, 0, 0)
val color = ResourcesCompat.getColor(resources, R.color.bg_default, context.theme)
view.setBackgroundColor(color)
} else {
colorizeStatusBar()
colorizeNavigationBar()
}
insets
}
}
open fun colorizeStatusBar() {

View File

@ -2233,7 +2233,8 @@ class CallActivity : CallBaseActivity() {
}
if (!isSelfInCall &&
currentCallStatus !== CallStatus.LEAVING && ApplicationWideCurrentRoomHolder.getInstance().isInCall
currentCallStatus !== CallStatus.LEAVING &&
ApplicationWideCurrentRoomHolder.getInstance().isInCall
) {
Log.d(TAG, "Most probably a moderator ended the call for all.")
hangup(shutDownView = true, endCallForAll = false)

View File

@ -49,7 +49,10 @@ import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class MainActivity : BaseActivity(), ActionBarProvider {
class MainActivity :
BaseActivity(),
ActionBarProvider {
lateinit var binding: ActivityMainBinding
@Inject

View File

@ -49,13 +49,9 @@ class GeocodingAdapter(private val context: Context, private var dataSource: Lis
}
}
override fun getItemCount(): Int {
return dataSource.size
}
override fun getItemCount(): Int = dataSource.size
fun getItem(position: Int): Any {
return dataSource[position]
}
fun getItem(position: Int): Any = dataSource[position]
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val nameView: TextView = itemView.findViewById(R.id.name)

View File

@ -153,8 +153,8 @@ class ParticipantDisplayItem(
participantDisplayItemNotifier.notifyChange()
}
private fun buildUiState(): ParticipantUiState {
return ParticipantUiState(
private fun buildUiState(): ParticipantUiState =
ParticipantUiState(
sessionKey = sessionKey,
nick = nick ?: "Guest",
isConnected = isConnected,
@ -164,7 +164,6 @@ class ParticipantDisplayItem(
avatarUrl = urlForAvatar,
mediaStream = mediaStream
)
}
private fun updateUrlForAvatar() {
if (actorType == ActorType.FEDERATED) {
@ -192,8 +191,8 @@ class ParticipantDisplayItem(
participantDisplayItemNotifier.removeObserver(observer)
}
override fun toString(): String {
return "ParticipantSession{" +
override fun toString(): String =
"ParticipantSession{" +
"userId='" + userId + '\'' +
", actorType='" + actorType + '\'' +
", actorId='" + actorId + '\'' +
@ -206,7 +205,6 @@ class ParticipantDisplayItem(
", rootEglBase=" + rootEglBase +
", raisedHand=" + raisedHand +
'}'
}
companion object {
/**

View File

@ -30,7 +30,5 @@ class PredefinedStatusListAdapter(
holder.bind(list[position], clickListener, context, isBackupStatusAvailable)
}
override fun getItemCount(): Int {
return list.size
}
override fun getItemCount(): Int = list.size
}

View File

@ -8,7 +8,4 @@ package com.nextcloud.talk.adapters
import com.nextcloud.talk.models.json.reactions.ReactionVoter
data class ReactionItem(
val reactionVoter: ReactionVoter,
val reaction: String?
)
data class ReactionItem(val reactionVoter: ReactionVoter, val reaction: String?)

View File

@ -12,10 +12,8 @@ import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ReactionItemBinding
class ReactionsAdapter(
private val clickListener: ReactionItemClickListener,
private val user: User?
) : RecyclerView.Adapter<ReactionsViewHolder>() {
class ReactionsAdapter(private val clickListener: ReactionItemClickListener, private val user: User?) :
RecyclerView.Adapter<ReactionsViewHolder>() {
internal var list: MutableList<ReactionItem> = ArrayList<ReactionItem>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReactionsViewHolder {
@ -27,7 +25,5 @@ class ReactionsAdapter(
holder.bind(list[position], clickListener)
}
override fun getItemCount(): Int {
return list.size
}
override fun getItemCount(): Int = list.size
}

View File

@ -16,10 +16,8 @@ import com.nextcloud.talk.extensions.loadGuestAvatar
import com.nextcloud.talk.extensions.loadUserAvatar
import com.nextcloud.talk.models.json.reactions.ReactionVoter
class ReactionsViewHolder(
private val binding: ReactionItemBinding,
private val user: User?
) : RecyclerView.ViewHolder(binding.root) {
class ReactionsViewHolder(private val binding: ReactionItemBinding, private val user: User?) :
RecyclerView.ViewHolder(binding.root) {
fun bind(reactionItem: ReactionItem, clickListener: ReactionItemClickListener) {
binding.root.setOnClickListener { clickListener.onClick(reactionItem) }

View File

@ -35,29 +35,24 @@ class AdvancedUserItem(
val account: Account?,
private val viewThemeUtils: ViewThemeUtils,
private val actionRequiredCount: Int
) : AbstractFlexibleItem<UserItemViewHolder>(), IFilterable<String?> {
override fun equals(other: Any?): Boolean {
return if (other is AdvancedUserItem) {
) : AbstractFlexibleItem<UserItemViewHolder>(),
IFilterable<String?> {
override fun equals(other: Any?): Boolean =
if (other is AdvancedUserItem) {
model == other.model
} else {
false
}
}
override fun hashCode(): Int {
return model.hashCode()
}
override fun hashCode(): Int = model.hashCode()
override fun getLayoutRes(): Int {
return R.layout.account_item
}
override fun getLayoutRes(): Int = R.layout.account_item
override fun createViewHolder(
view: View?,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
): UserItemViewHolder {
return UserItemViewHolder(view, adapter)
}
): UserItemViewHolder = UserItemViewHolder(view, adapter)
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
@ -94,13 +89,12 @@ class AdvancedUserItem(
}
}
override fun filter(constraint: String?): Boolean {
return model.displayName != null &&
override fun filter(constraint: String?): Boolean =
model.displayName != null &&
Pattern
.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
.matcher(model.displayName!!.trim())
.find()
}
class UserItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) {
var binding: AccountItemBinding

View File

@ -49,12 +49,10 @@ class ContactItem(
}
return false
}
override fun hashCode(): Int {
return model.hashCode()
}
override fun hashCode(): Int = model.hashCode()
override fun filter(constraint: String?): Boolean {
return model.displayName != null &&
override fun filter(constraint: String?): Boolean =
model.displayName != null &&
(
Pattern.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
.matcher(model.displayName!!.trim())
@ -63,18 +61,13 @@ class ContactItem(
.matcher(model.calculatedActorId!!.trim())
.find()
)
}
override fun getLayoutRes(): Int {
return R.layout.rv_item_contact
}
override fun getLayoutRes(): Int = R.layout.rv_item_contact
override fun createViewHolder(
view: View?,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
): ContactItemViewHolder {
return ContactItemViewHolder(view, adapter)
}
): ContactItemViewHolder = ContactItemViewHolder(view, adapter)
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?,
@ -143,7 +136,8 @@ class ContactItem(
} else if (model.calculatedActorType == Participant.ActorType.EMAILS) {
setGenericAvatar(holder!!, R.drawable.ic_avatar_mail)
} else if (model.calculatedActorType == Participant.ActorType.GUESTS ||
model.type == Participant.ParticipantType.GUEST || model.type == Participant.ParticipantType.GUEST_MODERATOR
model.type == Participant.ParticipantType.GUEST ||
model.type == Participant.ParticipantType.GUEST_MODERATOR
) {
var displayName: String?
@ -180,9 +174,7 @@ class ContactItem(
holder.binding.avatarView.loadUserAvatar(avatar)
}
override fun getHeader(): GenericTextHeaderItem? {
return header
}
override fun getHeader(): GenericTextHeaderItem? = header
override fun setHeader(p0: GenericTextHeaderItem?) {
this.header = header
@ -194,7 +186,6 @@ class ContactItem(
}
companion object {
const val VIEW_TYPE = FlexibleItemViewType.CONTACT_ITEM
private const val FULLY_OPAQUE: Float = 1.0f
private const val SEMI_TRANSPARENT: Float = 0.38f
}

View File

@ -83,17 +83,12 @@ class ConversationItem(
return result
}
override fun getLayoutRes(): Int {
return R.layout.rv_item_conversation_with_last_message
}
override fun getLayoutRes(): Int = R.layout.rv_item_conversation_with_last_message
override fun getItemViewType(): Int {
return VIEW_TYPE
}
override fun getItemViewType(): Int = VIEW_TYPE
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>?>?): ConversationItemViewHolder {
return ConversationItemViewHolder(view, adapter)
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>?>?): ConversationItemViewHolder =
ConversationItemViewHolder(view, adapter)
@SuppressLint("SetTextI18n")
override fun bindViewHolder(
@ -219,8 +214,8 @@ class ConversationItem(
}
}
private fun shouldLoadAvatar(holder: ConversationItemViewHolder): Boolean {
return when (model.objectType) {
private fun shouldLoadAvatar(holder: ConversationItemViewHolder): Boolean =
when (model.objectType) {
ConversationEnums.ObjectType.SHARE_PASSWORD -> {
holder.binding.dialogAvatar.setImageDrawable(
ContextCompat.getDrawable(
@ -243,7 +238,6 @@ class ConversationItem(
else -> true
}
}
private fun setLastMessage(holder: ConversationItemViewHolder, appContext: Context) {
if (chatMessage != null) {
@ -277,8 +271,8 @@ class ConversationItem(
}
}
private fun calculateRegularLastMessageText(appContext: Context): CharSequence {
return if (chatMessage?.actorId == user.userId) {
private fun calculateRegularLastMessageText(appContext: Context): CharSequence =
if (chatMessage?.actorId == user.userId) {
String.format(
appContext.getString(R.string.nc_formatted_message_you),
lastMessageDisplayText
@ -301,7 +295,6 @@ class ConversationItem(
lastMessageDisplayText
)
}
}
private fun showUnreadMessages(holder: ConversationItemViewHolder) {
holder.binding.dialogName.setTypeface(holder.binding.dialogName.typeface, Typeface.BOLD)
@ -343,17 +336,14 @@ class ConversationItem(
}
}
override fun filter(constraint: String?): Boolean {
return model.displayName != null &&
override fun filter(constraint: String?): Boolean =
model.displayName != null &&
Pattern
.compile(constraint!!, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
.matcher(model.displayName.trim())
.find()
}
override fun getHeader(): GenericTextHeaderItem? {
return header
}
override fun getHeader(): GenericTextHeaderItem? = header
override fun setHeader(header: GenericTextHeaderItem?) {
this.header = header
@ -431,9 +421,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

@ -14,5 +14,4 @@ object FlexibleItemViewType {
const val POLL_RESULT_HEADER_ITEM: Int = 1120391234
const val POLL_RESULT_VOTER_ITEM: Int = 1120391235
const val POLL_RESULT_VOTERS_OVERVIEW_ITEM: Int = 1120391236
const val CONTACT_ITEM: Int = 2131558687
}

View File

@ -38,20 +38,14 @@ open class GenericTextHeaderItem(title: String, viewThemeUtils: ViewThemeUtils)
return false
}
override fun hashCode(): Int {
return Objects.hash(model)
}
override fun hashCode(): Int = Objects.hash(model)
override fun getLayoutRes(): Int {
return R.layout.rv_item_title_header
}
override fun getLayoutRes(): Int = R.layout.rv_item_title_header
override fun createViewHolder(
view: View?,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
): HeaderViewHolder {
return HeaderViewHolder(view, adapter)
}
): HeaderViewHolder = HeaderViewHolder(view, adapter)
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<*>?>?,

View File

@ -23,13 +23,8 @@ object LoadMoreResultsItem :
// layout is used as view type for uniqueness
const val VIEW_TYPE = FlexibleItemViewType.LOAD_MORE_RESULTS_ITEM
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) {
var binding: RvItemLoadMoreBinding
init {
binding = RvItemLoadMoreBinding.bind(view)
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
var binding: RvItemLoadMoreBinding = RvItemLoadMoreBinding.bind(view)
}
override fun getLayoutRes(): Int = R.layout.rv_item_load_more
@ -50,15 +45,9 @@ object LoadMoreResultsItem :
override fun filter(constraint: String?): Boolean = true
override fun getItemViewType(): Int {
return VIEW_TYPE
}
override fun getItemViewType(): Int = VIEW_TYPE
override fun equals(other: Any?): Boolean {
return other is LoadMoreResultsItem
}
override fun equals(other: Any?): Boolean = other is LoadMoreResultsItem
override fun hashCode(): Int {
return 0
}
override fun hashCode(): Int = 0
}

View File

@ -15,6 +15,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.ResourcesCompat
import coil.Coil
import coil.request.ImageRequest
import com.nextcloud.talk.PhoneUtils.isPhoneNumber
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.items.ParticipantItem.ParticipantItemViewHolder
import com.nextcloud.talk.data.user.model.User
@ -40,7 +41,8 @@ class MentionAutocompleteItem(
private val context: Context,
@JvmField val roomToken: String,
private val viewThemeUtils: ViewThemeUtils
) : AbstractFlexibleItem<ParticipantItemViewHolder>(), IFilterable<String?> {
) : AbstractFlexibleItem<ParticipantItemViewHolder>(),
IFilterable<String?> {
@JvmField
var source: String?
@ -74,25 +76,19 @@ class MentionAutocompleteItem(
statusMessage = mention.statusMessage
}
override fun equals(o: Any?): Boolean {
return if (o is MentionAutocompleteItem) {
override fun equals(o: Any?): Boolean =
if (o is MentionAutocompleteItem) {
objectId == o.objectId && displayName == o.displayName
} else {
false
}
}
override fun hashCode(): Int {
return Objects.hash(objectId, displayName)
}
override fun hashCode(): Int = Objects.hash(objectId, displayName)
override fun getLayoutRes(): Int {
return R.layout.rv_item_conversation_info_participant
}
override fun getLayoutRes(): Int = R.layout.rv_item_conversation_info_participant
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>?>?): ParticipantItemViewHolder {
return ParticipantItemViewHolder(view, adapter)
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>?>?): ParticipantItemViewHolder =
ParticipantItemViewHolder(view, adapter)
@SuppressLint("SetTextI18n")
override fun bindViewHolder(
@ -129,14 +125,22 @@ class MentionAutocompleteItem(
private fun setAvatar(holder: ParticipantItemViewHolder, objectId: String?) {
when (source) {
SOURCE_CALLS -> {
run {}
run {
holder.binding.avatarView.loadUserAvatar(
viewThemeUtils.talk.themePlaceholderAvatar(
holder.binding.avatarView,
R.drawable.ic_avatar_group
if (isPhoneNumber(displayName)) {
holder.binding.avatarView.loadUserAvatar(
viewThemeUtils.talk.themePlaceholderAvatar(
holder.binding.avatarView,
R.drawable.ic_phone_small
)
)
)
} else {
holder.binding.avatarView.loadUserAvatar(
viewThemeUtils.talk.themePlaceholderAvatar(
holder.binding.avatarView,
R.drawable.ic_avatar_group
)
)
}
}
}
@ -234,8 +238,8 @@ class MentionAutocompleteItem(
holder.binding.nameText.setLayoutParams(layoutParams)
}
override fun filter(constraint: String?): Boolean {
return objectId != null &&
override fun filter(constraint: String?): Boolean =
objectId != null &&
Pattern
.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
.matcher(objectId)
@ -245,7 +249,6 @@ class MentionAutocompleteItem(
.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
.matcher(displayName)
.find()
}
companion object {
private const val STATUS_SIZE_IN_DP = 9f

View File

@ -31,13 +31,11 @@ data class MessageResultItem(
val messageEntry: SearchMessageEntry,
var showHeader: Boolean = false,
private val viewThemeUtils: ViewThemeUtils
) :
AbstractFlexibleItem<MessageResultItem.ViewHolder>(),
) : AbstractFlexibleItem<MessageResultItem.ViewHolder>(),
IFilterable<String>,
ISectionable<MessageResultItem.ViewHolder, GenericTextHeaderItem> {
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) {
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
var binding: RvItemSearchMessageBinding
init {
@ -73,9 +71,7 @@ data class MessageResultItem(
override fun filter(constraint: String?): Boolean = true
override fun getItemViewType(): Int {
return VIEW_TYPE
}
override fun getItemViewType(): Int = VIEW_TYPE
companion object {
const val VIEW_TYPE = FlexibleItemViewType.MESSAGE_RESULT_ITEM

View File

@ -49,31 +49,25 @@ class ParticipantItem(
private val user: User,
private val viewThemeUtils: ViewThemeUtils,
private val conversation: ConversationModel
) : AbstractFlexibleItem<ParticipantItemViewHolder>(), IFilterable<String?> {
) : AbstractFlexibleItem<ParticipantItemViewHolder>(),
IFilterable<String?> {
var isOnline = true
override fun equals(o: Any?): Boolean {
return if (o is ParticipantItem) {
override fun equals(o: Any?): Boolean =
if (o is ParticipantItem) {
model.calculatedActorType == o.model.calculatedActorType &&
model.calculatedActorId == o.model.calculatedActorId
} else {
false
}
}
override fun hashCode(): Int {
return model.hashCode()
}
override fun hashCode(): Int = model.hashCode()
override fun getLayoutRes(): Int {
return R.layout.rv_item_conversation_info_participant
}
override fun getLayoutRes(): Int = R.layout.rv_item_conversation_info_participant
override fun createViewHolder(
view: View?,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?
): ParticipantItemViewHolder {
return ParticipantItemViewHolder(view, adapter)
}
): ParticipantItemViewHolder = ParticipantItemViewHolder(view, adapter)
@SuppressLint("SetTextI18n")
override fun bindViewHolder(
@ -106,7 +100,8 @@ class ParticipantItem(
@SuppressLint("SetTextI18n")
private fun setParticipantInfo(holder: ParticipantItemViewHolder) {
if (TextUtils.isEmpty(model.displayName) && (
if (TextUtils.isEmpty(model.displayName) &&
(
model.type == Participant.ParticipantType.GUEST ||
model.type == Participant.ParticipantType.USER_FOLLOWING_LINK
)
@ -289,14 +284,14 @@ class ParticipantItem(
holder.binding.nameText.setLayoutParams(layoutParams)
}
override fun filter(constraint: String?): Boolean {
return model.displayName != null && (
Pattern.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
.matcher(model.displayName!!.trim()).find() ||
override fun filter(constraint: String?): Boolean =
model.displayName != null &&
(
Pattern.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
.matcher(model.calculatedActorId!!.trim()).find()
)
}
.matcher(model.displayName!!.trim()).find() ||
Pattern.compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL)
.matcher(model.calculatedActorId!!.trim()).find()
)
class ParticipantItemViewHolder internal constructor(view: View?, adapter: FlexibleAdapter<*>?) :
FlexibleViewHolder(view, adapter) {

View File

@ -0,0 +1,37 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.adapters.items
import android.view.View
import com.nextcloud.talk.R
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
class SpacerItem(private val height: Int) : AbstractFlexibleItem<SpacerItem.ViewHolder>() {
override fun getLayoutRes(): Int = R.layout.item_spacer
override fun createViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>?>?): ViewHolder =
ViewHolder(view!!, adapter!!)
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<*>?>?,
holder: ViewHolder,
position: Int,
payloads: MutableList<Any>?
) {
holder.itemView.layoutParams.height = height
}
override fun equals(other: Any?) = other is SpacerItem
override fun hashCode(): Int = 0
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter)
}

View File

@ -40,8 +40,9 @@ import kotlinx.coroutines.withContext
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class IncomingDeckCardViewHolder(incomingView: View, payload: Any) : MessageHolders
.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
class IncomingDeckCardViewHolder(incomingView: View, payload: Any) :
MessageHolders
.IncomingTextMessageViewHolder<ChatMessage>(incomingView, payload) {
private val binding: ItemCustomIncomingDeckCardMessageBinding =
ItemCustomIncomingDeckCardMessageBinding.bind(itemView)

View File

@ -220,15 +220,13 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) :
binding.webview.webViewClient = object : WebViewClient() {
@Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)")
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
return if (url != null && UriUtils.hasHttpProtocolPrefixed(url)
) {
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean =
if (url != null && UriUtils.hasHttpProtocolPrefixed(url)) {
view?.context?.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
true
} else {
false
}
}
}
val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html")
@ -268,9 +266,7 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) :
}
}
private fun addMarkerToGeoLink(locationGeoLink: String): String {
return locationGeoLink.replace("geo:", "geo:0,0?q=")
}
private fun addMarkerToGeoLink(locationGeoLink: String): String = locationGeoLink.replace("geo:", "geo:0,0?q=")
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
this.commonMessageInterface = commonMessageInterface

View File

@ -20,6 +20,7 @@ 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
@ -157,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)
@ -202,7 +203,10 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
val firstPart = message.toString().substringBefore("\n- [")
messageTextView.text = messageUtils.enrichChatMessageText(
binding.messageText.context, firstPart, true, viewThemeUtils
binding.messageText.context,
firstPart,
true,
viewThemeUtils
)
val checkboxList = mutableListOf<CheckBox>()
@ -218,7 +222,8 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
this.isEnabled = (
chatMessage.actorType == "bots" ||
chatActivity.userAllowedByPrivilages(chatMessage)
) && messageIsEditable
) &&
messageIsEditable
setTextColor(ContextCompat.getColor(context, R.color.no_emphasis_text))

View File

@ -42,9 +42,8 @@ import kotlinx.coroutines.withContext
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class OutcomingDeckCardViewHolder(
outcomingView: View
) : MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView),
class OutcomingDeckCardViewHolder(outcomingView: View) :
MessageHolders.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView),
AdjustableMessageHolderInterface {
override val binding: ItemCustomOutcomingDeckCardMessageBinding = ItemCustomOutcomingDeckCardMessageBinding.bind(

View File

@ -157,15 +157,13 @@ class OutcomingLocationMessageViewHolder(incomingView: View) :
binding.webview.webViewClient = object : WebViewClient() {
@Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)")
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
return if (url != null && UriUtils.hasHttpProtocolPrefixed(url)
) {
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean =
if (url != null && UriUtils.hasHttpProtocolPrefixed(url)) {
view?.context?.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
true
} else {
false
}
}
}
val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html")
@ -268,9 +266,7 @@ class OutcomingLocationMessageViewHolder(incomingView: View) :
}
}
private fun addMarkerToGeoLink(locationGeoLink: String): String {
return locationGeoLink.replace("geo:", "geo:0,0?q=")
}
private fun addMarkerToGeoLink(locationGeoLink: String): String = locationGeoLink.replace("geo:", "geo:0,0?q=")
fun assignCommonMessageInterface(commonMessageInterface: CommonMessageInterface) {
this.commonMessageInterface = commonMessageInterface

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
@ -172,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) {
@ -185,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))
@ -231,13 +231,15 @@ class OutcomingTextMessageViewHolder(itemView: View) :
val messageIsEditable = hasSpreedFeatureCapability(
user.capabilities?.spreedCapability!!,
SpreedFeatures.EDIT_MESSAGES
) && !isOlderThanTwentyFourHours
) &&
!isOlderThanTwentyFourHours
val isNoTimeLimitOnNoteToSelf = hasSpreedFeatureCapability(
user.capabilities?.spreedCapability!!,
SpreedFeatures
.EDIT_MESSAGES_NOTE_TO_SELF
) && chatActivity.currentConversation?.type == ConversationEnums.ConversationType.NOTE_TO_SELF
) &&
chatActivity.currentConversation?.type == ConversationEnums.ConversationType.NOTE_TO_SELF
checkBoxContainer.removeAllViews()
val regex = """(- \[(X|x| )])\s*(.+)""".toRegex(RegexOption.MULTILINE)
@ -247,7 +249,10 @@ class OutcomingTextMessageViewHolder(itemView: View) :
val firstPart = message.toString().substringBefore("\n- [")
messageTextView.text = messageUtils.enrichChatMessageText(
binding.messageText.context, firstPart, true, viewThemeUtils
binding.messageText.context,
firstPart,
true,
viewThemeUtils
)
val checkboxList = mutableListOf<CheckBox>()

View File

@ -316,9 +316,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) :
this.previewMessageInterface = previewMessageInterface
}
fun hasBubbleBackground(message: ChatMessage): Boolean {
return !message.isVoiceMessage && message.message != "{file}"
}
fun hasBubbleBackground(message: ChatMessage): Boolean = !message.isVoiceMessage && message.message != "{file}"
abstract val messageText: EmojiTextView
abstract val messageCaption: EmojiTextView

View File

@ -33,8 +33,9 @@ import com.stfalcon.chatkit.messages.MessageHolders
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class SystemMessageViewHolder(itemView: View) : MessageHolders
.IncomingTextMessageViewHolder<ChatMessage>(itemView) {
class SystemMessageViewHolder(itemView: View) :
MessageHolders
.IncomingTextMessageViewHolder<ChatMessage>(itemView) {
private val binding: ItemSystemMessageBinding = ItemSystemMessageBinding.bind(itemView)

View File

@ -86,7 +86,9 @@ import javax.inject.Singleton
)
@Singleton
@AutoInjector(NextcloudTalkApplication::class)
class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
class NextcloudTalkApplication :
MultiDexApplication(),
LifecycleObserver {
//region Fields (components)
lateinit var componentApplication: NextcloudTalkApplicationComponent
private set

View File

@ -15,11 +15,9 @@ class ArbitraryStorageManager(private val arbitraryStoragesRepository: Arbitrary
arbitraryStoragesRepository.saveArbitraryStorage(ArbitraryStorage(accountIdentifier, key, objectString, value))
}
fun getStorageSetting(accountIdentifier: Long, key: String, objectString: String): Maybe<ArbitraryStorage> {
return arbitraryStoragesRepository.getStorageSetting(accountIdentifier, key, objectString)
}
fun getStorageSetting(accountIdentifier: Long, key: String, objectString: String): Maybe<ArbitraryStorage> =
arbitraryStoragesRepository.getStorageSetting(accountIdentifier, key, objectString)
fun deleteAllEntriesForAccountIdentifier(accountIdentifier: Long): Int {
return arbitraryStoragesRepository.deleteArbitraryStorage(accountIdentifier)
}
fun deleteAllEntriesForAccountIdentifier(accountIdentifier: Long): Int =
arbitraryStoragesRepository.deleteArbitraryStorage(accountIdentifier)
}

View File

@ -14,10 +14,7 @@ interface ListItemWithImage {
fun populateIcon(imageView: ImageView)
}
data class BasicListItemWithImage(
@DrawableRes val iconRes: Int,
override val title: String
) : ListItemWithImage {
data class BasicListItemWithImage(@DrawableRes val iconRes: Int, override val title: String) : ListItemWithImage {
override fun populateIcon(imageView: ImageView) {
imageView.setImageResource(iconRes)

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
@ -22,10 +23,9 @@ import com.nextcloud.talk.R
private const val KEY_ACTIVATED_INDEX = "activated_index"
internal class ListItemViewHolder(
itemView: View,
private val adapter: ListIconDialogAdapter<*>
) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
internal class ListItemViewHolder(itemView: View, private val adapter: ListIconDialogAdapter<*>) :
RecyclerView.ViewHolder(itemView),
View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
@ -42,7 +42,8 @@ internal class ListIconDialogAdapter<IT : ListItemWithImage>(
disabledItems: IntArray?,
private var waitForPositiveButton: Boolean,
private var selection: ListItemListener<IT>
) : RecyclerView.Adapter<ListItemViewHolder>(), DialogAdapter<IT, ListItemListener<IT>> {
) : RecyclerView.Adapter<ListItemViewHolder>(),
DialogAdapter<IT, ListItemListener<IT>> {
private var disabledIndices: IntArray = disabledItems ?: IntArray(0)
@ -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

@ -163,7 +163,4 @@ class ReactionAnimator(
private const val BOTTOM_MARGIN: Int = 5
}
}
data class CallReaction(
var emoji: String,
var userName: String
)
data class CallReaction(var emoji: String, var userName: String)

View File

@ -9,6 +9,7 @@
package com.nextcloud.talk.call.components
import android.annotation.SuppressLint
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -29,6 +30,7 @@ import com.nextcloud.talk.adapters.ParticipantUiState
import org.webrtc.EglBase
import kotlin.math.ceil
@SuppressLint("UnusedBoxWithConstraintsScope")
@Suppress("LongParameterList")
@Composable
fun ParticipantGrid(
@ -78,7 +80,9 @@ fun ParticipantGrid(
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxWidth()
.height(availableHeight),
verticalArrangement = Arrangement.spacedBy(itemSpacing),
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
contentPadding = PaddingValues(vertical = edgePadding, horizontal = edgePadding)

View File

@ -7,6 +7,7 @@
package com.nextcloud.talk.call.components
import android.annotation.SuppressLint
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
@ -40,6 +41,7 @@ const val NICK_OFFSET = 4f
const val NICK_BLUR_RADIUS = 4f
const val AVATAR_SIZE_FACTOR = 0.6f
@SuppressLint("UnusedBoxWithConstraintsScope")
@Suppress("Detekt.LongMethod")
@Composable
fun ParticipantTile(

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
@ -67,6 +66,8 @@ import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.core.text.bold
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.emoji2.text.EmojiCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.commit
@ -276,6 +277,8 @@ class ChatActivity :
lateinit var conversationInfoViewModel: ConversationInfoViewModel
lateinit var messageInputViewModel: MessageInputViewModel
private var chatMenu: Menu? = null
private val startSelectContactForResult = registerForActivityResult(
ActivityResultContracts
.StartActivityForResult()
@ -307,12 +310,7 @@ class ChatActivity :
runBlocking {
val id = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
id?.let {
val isSaved = chatViewModel.isMessageSaved(id.toLong())
if (isSaved) {
onMessageSearchResult(intent)
} else {
startContextChatWindowForMessage(id)
}
startContextChatWindowForMessage(id)
}
}
}
@ -366,6 +364,7 @@ class ChatActivity :
var startCallFromRoomSwitch: Boolean = false
var voiceOnly: Boolean = true
var focusInput: Boolean = false
private lateinit var path: String
var myFirstMessage: CharSequence? = null
@ -461,7 +460,28 @@ class ChatActivity :
binding = ActivityChatBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
setupSystemColors()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.chat_container)) { view, insets ->
val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())
val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
val isKeyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val bottomPadding = if (isKeyboardVisible) imeInsets.bottom else navBarInsets.bottom
view.setPadding(
view.paddingLeft,
statusBarInsets.top,
view.paddingRight,
bottomPadding
)
WindowInsetsCompat.CONSUMED
}
} else {
colorizeStatusBar()
colorizeNavigationBar()
}
conversationUser = currentUserProvider.currentUser.blockingGet()
handleIntent(intent)
@ -489,7 +509,7 @@ class ChatActivity :
initObservers()
pickMultipleMedia = registerForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(5)
ActivityResultContracts.PickMultipleVisualMedia(MAX_AMOUNT_MEDIA_FILE_PICKER)
) { uris ->
if (uris.isNotEmpty()) {
onChooseFileResult(uris)
@ -549,6 +569,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() {
@ -640,12 +662,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() }
}
@ -680,7 +707,7 @@ class ChatActivity :
?.split("#")
?.getOrNull(1)
?.toLongOrNull()
val currentTimeStamp = (System.currentTimeMillis() / 1000).toLong()
val currentTimeStamp = (System.currentTimeMillis() / ONE_SECOND_IN_MILLIS).toLong()
val retentionPeriod = retentionOfEventRooms(spreedCapabilities)
val isPastEvent = eventEndTimeStamp?.let { it < currentTimeStamp }
if (isPastEvent == true && retentionPeriod != 0) {
@ -696,7 +723,8 @@ class ChatActivity :
) {
val retentionPeriod = retentionOfSIPRoom(spreedCapabilities)
val systemMessage = currentConversation?.lastMessage?.systemMessageType
if (retentionPeriod != 0 && (
if (retentionPeriod != 0 &&
(
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED ||
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE
)
@ -713,7 +741,8 @@ class ChatActivity :
) {
val retentionPeriod = retentionOfInstantMeetingRoom(spreedCapabilities)
val systemMessage = currentConversation?.lastMessage?.systemMessageType
if (retentionPeriod != 0 && (
if (retentionPeriod != 0 &&
(
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED ||
systemMessage == ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE
)
@ -1102,6 +1131,8 @@ class ChatActivity :
context.getString(R.string.nc_room_retention),
Snackbar.LENGTH_LONG
).show()
chatMenu?.removeItem(R.id.conversation_event)
}
is ChatViewModel.UnbindRoomUiState.Error -> {
Snackbar.make(
@ -1237,11 +1268,17 @@ class ChatActivity :
bringToFront()
}
val deleteNoticeText = binding.conversationDeleteNotice.findViewById<TextView>(R.id.deletion_message)
viewThemeUtils.material.themeCardView(binding.conversationDeleteNotice)
deleteNoticeText.text = String.format(
resources.getString(R.string.nc_conversation_auto_delete_notice),
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 =
@ -1958,8 +1995,8 @@ class ChatActivity :
WorkManager.getInstance().enqueue(downloadWorker)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id)
.observeForever { workInfo: WorkInfo ->
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
.observeForever { workInfo: WorkInfo? ->
if (workInfo?.state == WorkInfo.State.SUCCEEDED) {
funToCallWhenDownloadSuccessful()
}
}
@ -2042,7 +2079,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()
@ -2297,15 +2334,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")
@ -2319,7 +2349,7 @@ class ChatActivity :
if (position != null && position >= 0) {
binding.messagesListView.scrollToPosition(position)
} else {
startContextChatWindowForMessage(messageId)
Log.d(TAG, "message $messageId that should be scrolled to was not found (scrollToMessageWithId)")
}
}
@ -2332,10 +2362,12 @@ class ChatActivity :
binding.messagesListView.height / 2
)
} else {
startContextChatWindowForMessage(messageId)
Log.d(
TAG,
"message $messageId that should be scrolled " +
"to was not found (scrollToAndCenterMessageWithId)"
)
}
} ?: run {
startContextChatWindowForMessage(messageId)
}
}
@ -2797,7 +2829,7 @@ class ChatActivity :
}
if (this::spreedCapabilities.isInitialized) {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
deleteExpiredMessages()
}
} else {
@ -3044,6 +3076,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)
@ -3057,7 +3090,6 @@ class ChatActivity :
loadAvatarForStatusBar()
setActionBarTitle()
}
return true
}
@ -3065,7 +3097,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()
}
@ -3086,7 +3118,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)
@ -3145,10 +3177,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(
@ -3597,7 +3629,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
@ -3911,7 +3943,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
)
@ -3921,7 +3953,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
@ -4186,5 +4218,6 @@ class ChatActivity :
const val OUT_OF_OFFICE_ALPHA = 76
const val ZERO_INDEX = 0
const val ONE_INDEX = 1
const val MAX_AMOUNT_MEDIA_FILE_PICKER = 10
}
}

View File

@ -64,6 +64,7 @@ import com.nextcloud.talk.chat.viewmodels.ChatViewModel
import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.databinding.FragmentMessageInputBinding
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
import com.nextcloud.talk.models.json.chat.ChatUtils
import com.nextcloud.talk.models.json.mention.Mention
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
import com.nextcloud.talk.presenters.MentionAutocompletePresenter
@ -77,6 +78,7 @@ import com.nextcloud.talk.utils.CharPolicy
import com.nextcloud.talk.utils.ImageEmojiEditText
import com.nextcloud.talk.utils.SpreedFeatures
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import com.nextcloud.talk.utils.message.MessageUtils
import com.nextcloud.talk.utils.text.Spans
import com.otaliastudios.autocomplete.Autocomplete
import com.stfalcon.chatkit.commons.models.IMessage
@ -124,6 +126,9 @@ class MessageInputFragment : Fragment() {
@Inject
lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var messageUtils: MessageUtils
lateinit var binding: FragmentMessageInputBinding
private lateinit var conversationInternalId: String
private var typedWhileTypingTimerIsRunning: Boolean = false
@ -200,7 +205,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,
@ -412,10 +417,22 @@ class MessageInputFragment : Fragment() {
}
binding.fragmentMessageInputView.editMessageButton.setOnClickListener {
val text = binding.fragmentMessageInputView.inputEditText.text.toString()
val editable = binding.fragmentMessageInputView.inputEditText!!.editableText
replaceMentionChipSpans(editable)
val inputEditText = editable.toString()
val message = chatActivity.messageInputViewModel.getEditChatMessage.value as ChatMessage
if (message.message!!.trim() != text.trim()) {
editMessageAPI(message, text)
if (message.message!!.trim() != inputEditText.trim()) {
if (message.messageParameters != null) {
val editedMessage = messageUtils.processEditMessageParameters(
message.messageParameters!!,
message,
inputEditText
)
editMessageAPI(message, editedMessage.toString())
} else {
editMessageAPI(message, inputEditText.toString())
}
}
clearEditUI()
}
@ -837,27 +854,7 @@ class MessageInputFragment : Fragment() {
private fun submitMessage(sendWithoutNotification: Boolean) {
if (binding.fragmentMessageInputView.inputEditText != null) {
val editable = binding.fragmentMessageInputView.inputEditText!!.editableText
val mentionSpans = editable.getSpans(
0,
editable.length,
Spans.MentionChipSpan::class.java
)
var mentionSpan: Spans.MentionChipSpan
for (i in mentionSpans.indices) {
mentionSpan = mentionSpans[i]
var mentionId = mentionSpan.id
val shouldQuote = mentionId.contains(" ") ||
mentionId.contains("@") ||
mentionId.startsWith("guest/") ||
mentionId.startsWith("group/") ||
mentionId.startsWith("email/") ||
mentionId.startsWith("team/")
if (shouldQuote) {
mentionId = "\"" + mentionId + "\""
}
editable.replace(editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@$mentionId")
}
replaceMentionChipSpans(editable)
binding.fragmentMessageInputView.inputEditText?.setText("")
sendStopTypingMessage()
val replyMessageId = binding.fragmentMessageInputView
@ -887,6 +884,31 @@ class MessageInputFragment : Fragment() {
)
}
private fun replaceMentionChipSpans(editable: Editable) {
val mentionSpans = editable.getSpans(
0,
editable.length,
Spans.MentionChipSpan::class.java
)
for (mentionSpan in mentionSpans) {
var mentionId = mentionSpan.id
val shouldQuote = mentionId.contains(" ") ||
mentionId.contains("@") ||
mentionId.startsWith("guest/") ||
mentionId.startsWith("group/") ||
mentionId.startsWith("email/") ||
mentionId.startsWith("team/")
if (shouldQuote) {
mentionId = "\"$mentionId\""
}
editable.replace(
editable.getSpanStart(mentionSpan),
editable.getSpanEnd(mentionSpan),
"@$mentionId"
)
}
}
private fun showSendButtonMenu() {
val popupMenu = PopupMenu(
ContextThemeWrapper(requireContext(), R.style.ChatSendButtonMenu),
@ -932,8 +954,12 @@ class MessageInputFragment : Fragment() {
}
private fun setEditUI(message: ChatMessage) {
binding.fragmentEditView.editMessage.text = message.message
binding.fragmentMessageInputView.inputEditText.setText(message.message)
val editedMessage = ChatUtils.getParsedMessage(message.message, message.messageParameters)
binding.fragmentEditView.editMessage.text = editedMessage
binding.fragmentMessageInputView.inputEditText.setText(editedMessage)
if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
mentionAutocomplete?.dismissPopup()
}
val end = binding.fragmentMessageInputView.inputEditText.text.length
binding.fragmentMessageInputView.inputEditText.setSelection(end)
binding.fragmentMessageInputView.messageSendButton.visibility = View.GONE

View File

@ -76,8 +76,6 @@ interface ChatMessageRepository : LifecycleAwareManager {
*/
suspend fun getMessage(messageId: Long, bundle: Bundle): Flow<ChatMessage>
suspend fun checkIfMessageIsSaved(messageId: Long): Boolean
@Suppress("LongParameterList")
suspend fun sendChatMessage(
credentials: String,
@ -112,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

@ -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)
}
@ -475,15 +491,6 @@ class OfflineFirstChatRepository @Inject constructor(
.map(ChatMessageEntity::asModel)
}
override suspend fun checkIfMessageIsSaved(messageId: Long): Boolean {
try {
chatDao.getChatMessageForConversation(internalConversationId, messageId)
return true
} catch (_: Exception) {
return false
}
}
@Suppress("UNCHECKED_CAST", "MagicNumber", "Detekt.TooGenericExceptionCaught")
private fun getMessagesFromServer(bundle: Bundle): Pair<Int, List<ChatMessageJson>>? {
val fieldMap = bundle.getSerializable(BundleKeys.KEY_FIELD_MAP) as HashMap<String, Int>
@ -852,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 ->
@ -862,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()
@ -882,22 +900,28 @@ class OfflineFirstChatRepository @Inject constructor(
sendWithoutNotification: Boolean,
referenceId: String
): Flow<Result<ChatMessage?>> {
val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first()
messageToResend.sendingFailed = false
chatDao.updateChatMessage(messageToResend)
val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).firstOrNull()
return if (messageToResend != null) {
messageToResend.sendStatus = SendStatus.PENDING
chatDao.updateChatMessage(messageToResend)
val messageToResendModel = messageToResend.asModel()
_updateMessageFlow.emit(messageToResendModel)
val messageToResendModel = messageToResend.asModel()
_updateMessageFlow.emit(messageToResendModel)
return sendChatMessage(
credentials,
url,
message,
displayName,
replyTo,
sendWithoutNotification,
referenceId
)
sendChatMessage(
credentials,
url,
message,
displayName,
replyTo,
sendWithoutNotification,
referenceId
)
} else {
flow {
emit(Result.failure(IllegalStateException("No temporary message found to resend")))
}
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
@ -939,8 +963,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,
@ -1034,7 +1058,7 @@ class OfflineFirstChatRepository @Inject constructor(
actorDisplayName = currentUser.displayName!!,
referenceId = referenceId,
isTemporary = true,
sendingFailed = false,
sendStatus = SendStatus.PENDING,
silent = sendWithoutNotification
)
return entity

View File

@ -23,10 +23,8 @@ import com.nextcloud.talk.utils.message.SendMessageUtils
import io.reactivex.Observable
import retrofit2.Response
class RetrofitChatNetwork(
private val ncApi: NcApi,
private val ncApiCoroutines: NcApiCoroutines
) : ChatNetworkDataSource {
class RetrofitChatNetwork(private val ncApi: NcApi, private val ncApiCoroutines: NcApiCoroutines) :
ChatNetworkDataSource {
override fun getRoom(user: User, roomToken: String): Observable<ConversationModel> {
val credentials: String = ApiUtils.getCredentials(user.username, user.token)!!
val apiVersion = ApiUtils.getConversationApiVersion(user, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V3, 1))
@ -187,12 +185,11 @@ class RetrofitChatNetwork(
credentials: String,
baseUrl: String,
userId: String
): UserAbsenceOverall {
return ncApiCoroutines.getOutOfOfficeStatusForUser(
): UserAbsenceOverall =
ncApiCoroutines.getOutOfOfficeStatusForUser(
credentials,
ApiUtils.getUrlForOutOfOffice(baseUrl, userId)
)
}
override suspend fun getContextForChatMessage(
credentials: String,

View File

@ -85,9 +85,7 @@ class ChatViewModel @Inject constructor(
var mediaPlayerDuration = mediaPlayerManager.mediaPlayerDuration
val mediaPlayerPosition = mediaPlayerManager.mediaPlayerPosition
fun getChatRepository(): ChatMessageRepository {
return chatRepository
}
fun getChatRepository(): ChatMessageRepository = chatRepository
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
@ -285,10 +283,6 @@ class ChatViewModel @Inject constructor(
conversationRepository.getRoom(token)
}
suspend fun isMessageSaved(messageId: Long): Boolean {
return chatRepository.checkIfMessageIsSaved(messageId)
}
fun getCapabilities(user: User, token: String, conversationModel: ConversationModel) {
Log.d(TAG, "Remote server ${conversationModel.remoteServer}")
if (conversationModel.remoteServer.isNullOrEmpty()) {

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,6 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@ -9,16 +8,41 @@
package com.nextcloud.talk.components
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
@Composable
fun SetupSystemBars() {
fun ColoredStatusBar() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
Box(modifier = Modifier.fillMaxSize()) {
Box(
Modifier
.windowInsetsTopHeight(WindowInsets.statusBars)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
)
}
} else {
ColorLegacyStatusBar()
}
}
@Composable
private fun ColorLegacyStatusBar() {
val view = LocalView.current
val isDarkMode = isSystemInDarkTheme()
val statusBarColor = MaterialTheme.colorScheme.surface.toArgb()

View File

@ -0,0 +1,24 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun VerticallyCenteredRow(content: @Composable RowScope.() -> Unit) {
Row(
modifier = Modifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically,
content = content
)
}

View File

@ -18,9 +18,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import autodagger.AutoInjector
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.contacts.CompanionClass.Companion.KEY_HIDE_ALREADY_EXISTING_PARTICIPANTS
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
import com.nextcloud.talk.utils.bundle.BundleKeys
import javax.inject.Inject
@ -64,11 +64,11 @@ class ContactsActivity : BaseActivity() {
MaterialTheme(
colorScheme = colorScheme
) {
ColoredStatusBar()
ContactsScreen(
contactsViewModel = contactsViewModel,
uiState = uiState.value
)
SetupSystemBars()
}
}
}

View File

@ -15,7 +15,9 @@ import coil.memory.MemoryCache
import coil.util.DebugLogger
import com.nextcloud.talk.utils.ContactUtils
class ContactsApplication : Application(), ImageLoaderFactory {
class ContactsApplication :
Application(),
ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
val imageLoader = ImageLoader.Builder(this)
.memoryCache {

View File

@ -86,13 +86,12 @@ class ContactsRepositoryImpl @Inject constructor(
return response
}
override fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
return ApiUtils.getUrlForAvatar(
override fun getImageUri(avatarId: String, requestBigSize: Boolean): String =
ApiUtils.getUrlForAvatar(
_currentUser.baseUrl,
avatarId,
requestBigSize
)
}
companion object {
private val TAG = ContactsRepositoryImpl::class.simpleName

View File

@ -11,16 +11,18 @@ package com.nextcloud.talk.contacts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nextcloud.talk.R
import com.nextcloud.talk.contacts.components.AppBar
import com.nextcloud.talk.contacts.components.ContactsAppBar
import com.nextcloud.talk.contacts.components.ContactsList
import com.nextcloud.talk.contacts.components.ContactsSearchAppBar
import com.nextcloud.talk.contacts.components.ConversationCreationOptions
@Composable
@ -32,35 +34,37 @@ fun ContactsScreen(contactsViewModel: ContactsViewModel, uiState: ContactsUiStat
val enableAddButton by contactsViewModel.enableAddButton.collectAsStateWithLifecycle()
Scaffold(
modifier = Modifier
.statusBarsPadding(),
topBar = {
AppBar(
title = stringResource(R.string.nc_app_product_name),
searchQuery = searchQuery,
isSearchActive = isSearchActive,
isAddParticipants = isAddParticipants,
autocompleteUsers = autocompleteUsers,
onEnableSearch = {
contactsViewModel.setSearchActive(true)
},
onDisableSearch = {
contactsViewModel.setSearchActive(false)
},
onUpdateSearchQuery = {
contactsViewModel.updateSearchQuery(query = it)
},
onUpdateAutocompleteUsers = {
contactsViewModel.getContactsFromSearchParams()
},
enableAddButton = enableAddButton,
clickAddButton = {
contactsViewModel.modifyClickAddButton(it)
}
)
if (isSearchActive) {
ContactsSearchAppBar(
searchQuery = searchQuery,
onTextChange = {
contactsViewModel.updateSearchQuery(it)
contactsViewModel.getContactsFromSearchParams()
},
onCloseSearch = {
contactsViewModel.updateSearchQuery("")
contactsViewModel.setSearchActive(false)
contactsViewModel.getContactsFromSearchParams()
},
enableAddButton = enableAddButton,
isAddParticipants = isAddParticipants,
clickAddButton = { contactsViewModel.modifyClickAddButton(true) }
)
} else {
ContactsAppBar(
isAddParticipants = isAddParticipants,
autocompleteUsers = autocompleteUsers,
onStartSearch = { contactsViewModel.setSearchActive(true) }
)
}
},
content = {
content = { paddingValues ->
Column(
Modifier
.padding(it)
.padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp)
.background(colorResource(id = R.color.bg_default))
) {
if (!isAddParticipants) {

View File

@ -17,9 +17,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
class ContactsViewModel @Inject constructor(
private val repository: ContactsRepository
) : ViewModel() {
class ContactsViewModel @Inject constructor(private val repository: ContactsRepository) : ViewModel() {
private val _contactsViewState = MutableStateFlow<ContactsUiState>(ContactsUiState.None)
val contactsViewState: StateFlow<ContactsUiState> = _contactsViewState
@ -141,9 +139,8 @@ class ContactsViewModel @Inject constructor(
}
}
}
fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
return repository.getImageUri(avatarId, requestBigSize)
}
fun getImageUri(avatarId: String, requestBigSize: Boolean): String =
repository.getImageUri(avatarId, requestBigSize)
}
sealed class ContactsUiState {

View File

@ -1,117 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts.components
import android.annotation.SuppressLint
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
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
import com.nextcloud.talk.R
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
@SuppressLint("UnrememberedMutableState")
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongParameterList", "LongMethod")
@Composable
fun AppBar(
title: String,
searchQuery: String,
isSearchActive: Boolean,
isAddParticipants: Boolean,
autocompleteUsers: List<AutocompleteUser>,
onEnableSearch: () -> Unit,
onDisableSearch: () -> Unit,
onUpdateSearchQuery: (String) -> Unit,
onUpdateAutocompleteUsers: () -> Unit,
enableAddButton: Boolean,
clickAddButton: (Boolean) -> Unit
) {
val context = LocalContext.current
val appTitle = if (!isSearchActive) {
title
} else {
""
}
TopAppBar(
title = { Text(text = appTitle) },
navigationIcon = {
IconButton(onClick = {
(context as? Activity)?.finish()
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_button))
}
},
actions = {
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()
}
)
}
}
}
)
if (isSearchActive) {
Row(modifier = Modifier.fillMaxWidth()) {
SearchComponent(
text = searchQuery,
onTextChange = { searchQuery ->
onUpdateSearchQuery(searchQuery)
onUpdateAutocompleteUsers()
},
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

@ -0,0 +1,77 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts.components
import android.app.Activity
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.nextcloud.talk.R
import com.nextcloud.talk.components.VerticallyCenteredRow
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ContactsAppBar(isAddParticipants: Boolean, autocompleteUsers: List<AutocompleteUser>, onStartSearch: () -> Unit) {
val context = LocalContext.current
TopAppBar(
modifier = Modifier
.height(60.dp),
title = {
VerticallyCenteredRow {
Text(
text = if (isAddParticipants) {
stringResource(R.string.nc_participants_add)
} else {
stringResource(R.string.nc_new_conversation)
}
)
}
},
navigationIcon = {
VerticallyCenteredRow {
IconButton(onClick = { (context as? Activity)?.finish() }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_button))
}
}
},
actions = {
VerticallyCenteredRow {
IconButton(onClick = onStartSearch) {
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()
}
)
}
}
}
)
}

View File

@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider
@ -44,9 +43,13 @@ fun ContactsItem(contacts: List<AutocompleteUser>, contactsViewModel: ContactsVi
}
LazyColumn(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
contentPadding = PaddingValues(all = 10.dp),
contentPadding = PaddingValues(
top = 10.dp,
bottom = 40.dp,
start = 10.dp,
end = 10.dp
),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
groupedContacts.forEach { (initial, contactsForInitial) ->

View File

@ -0,0 +1,117 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Sowjanya Kota <sowjanya.kch@gmail.com>
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.contacts.components
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import com.nextcloud.talk.R
import com.nextcloud.talk.components.VerticallyCenteredRow
@Suppress("LongParameterList")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ContactsSearchAppBar(
searchQuery: String,
onTextChange: (String) -> Unit,
onCloseSearch: () -> Unit,
enableAddButton: Boolean,
isAddParticipants: Boolean,
clickAddButton: (Boolean) -> Unit
) {
val keyboardController = LocalSoftwareKeyboardController.current
Surface(
modifier = Modifier.height(60.dp)
) {
VerticallyCenteredRow {
IconButton(
modifier = Modifier.padding(start = 4.dp),
onClick = onCloseSearch
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_button)
)
}
TextField(
value = searchQuery,
onValueChange = onTextChange,
placeholder = { Text(text = stringResource(R.string.nc_search)) },
singleLine = true,
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = searchKeyboardActions(searchQuery, keyboardController),
colors = searchTextFieldColors(),
trailingIcon = {
if (searchQuery.isNotEmpty()) {
IconButton(onClick = { onTextChange("") }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.nc_search_clear)
)
}
}
}
)
if (isAddParticipants) {
TextButton(
onClick = {
onCloseSearch()
clickAddButton(true)
},
enabled = enableAddButton
) {
Text(text = stringResource(R.string.add_participants))
}
}
}
}
}
@Composable
fun searchTextFieldColors() =
TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent
)
fun searchKeyboardActions(text: String, keyboardController: SoftwareKeyboardController?) =
KeyboardActions(
onSearch = {
if (text.trim().isNotEmpty()) {
keyboardController?.hide()
}
}
)

View File

@ -1,105 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* 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.height
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.nextcloud.talk.R
@Composable
fun SearchComponent(
text: String,
onTextChange: (String) -> Unit,
onDisableSearch: () -> Unit,
modifier: Modifier = Modifier
) {
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
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,
leadingIcon = { LeadingIcon(onTextChange, onDisableSearch) },
trailingIcon = { TrailingIcon(text, onTextChange) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = searchKeyboardActions(text, keyboardController),
colors = searchTextFieldColors(),
maxLines = 1
)
}
@Composable
fun searchTextFieldColors() =
TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent
)
@Composable
fun LeadingIcon(onTextChange: (String) -> Unit, onDisableSearch: () -> Unit) {
IconButton(
onClick = {
onTextChange("")
onDisableSearch()
}
) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = stringResource(R.string.back_button)
)
}
}
@Composable
fun TrailingIcon(text: String, onTextChange: (String) -> Unit) {
if (text.isNotEmpty()) {
IconButton(
onClick = { onTextChange("") }
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.close_icon)
)
}
}
}
fun searchKeyboardActions(text: String, keyboardController: SoftwareKeyboardController?) =
KeyboardActions(
onSearch = {
if (text.trim().isNotEmpty()) {
keyboardController?.hide()
}
}
)

View File

@ -82,9 +82,8 @@ class RenameConversationDialogFragment : DialogFragment() {
return dialogBuilder.create()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return binding.root
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
binding.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View File

@ -84,8 +84,8 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.components.ColoredStatusBar
import com.nextcloud.talk.contacts.ContactsActivity
import com.nextcloud.talk.components.SetupSystemBars
import com.nextcloud.talk.contacts.loadImage
import com.nextcloud.talk.extensions.getParcelableArrayListExtraProvider
import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser
@ -117,7 +117,6 @@ class ConversationCreationActivity : BaseActivity() {
colorScheme = colorScheme
) {
ConversationCreationScreen(conversationCreationViewModel, context, pickImage)
SetupSystemBars()
}
}
}
@ -172,6 +171,7 @@ fun ConversationCreationScreen(
}
)
ColoredStatusBar()
Scaffold(
topBar = {
TopAppBar(
@ -191,7 +191,7 @@ fun ConversationCreationScreen(
content = { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.padding(0.dp, paddingValues.calculateTopPadding(), 0.dp, 0.dp)
.background(colorResource(id = R.color.bg_default))
.fillMaxSize()
.verticalScroll(rememberScrollState())
@ -289,7 +289,7 @@ fun UploadAvatar(
}
) {
Icon(
painter = painterResource(id = R.drawable.ic_mimetype_folder),
painter = painterResource(id = R.drawable.ic_folder),
contentDescription = null,
modifier = Modifier.size(24.dp)
)

View File

@ -34,8 +34,8 @@ class ConversationCreationRepositoryImpl @Inject constructor(
val credentials = ApiUtils.getCredentials(_currentUser.username, _currentUser.token)
val apiVersion = ApiUtils.getConversationApiVersion(_currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
override suspend fun renameConversation(roomToken: String, roomNameNew: String?): GenericOverall {
return ncApiCoroutines.renameRoom(
override suspend fun renameConversation(roomToken: String, roomNameNew: String?): GenericOverall =
ncApiCoroutines.renameRoom(
credentials,
ApiUtils.getUrlForRoom(
apiVersion,
@ -44,10 +44,9 @@ class ConversationCreationRepositoryImpl @Inject constructor(
),
roomNameNew
)
}
override suspend fun setConversationDescription(roomToken: String, description: String?): GenericOverall {
return ncApiCoroutines.setConversationDescription(
override suspend fun setConversationDescription(roomToken: String, description: String?): GenericOverall =
ncApiCoroutines.setConversationDescription(
credentials,
ApiUtils.getUrlForConversationDescription(
apiVersion,
@ -56,10 +55,9 @@ class ConversationCreationRepositoryImpl @Inject constructor(
),
description
)
}
override suspend fun openConversation(roomToken: String, scope: Int): GenericOverall {
return ncApiCoroutines.openConversation(
override suspend fun openConversation(roomToken: String, scope: Int): GenericOverall =
ncApiCoroutines.openConversation(
credentials,
ApiUtils.getUrlForOpeningConversations(
apiVersion,
@ -68,7 +66,6 @@ class ConversationCreationRepositoryImpl @Inject constructor(
),
scope
)
}
override suspend fun addParticipants(
conversationToken: String?,
@ -110,13 +107,12 @@ class ConversationCreationRepositoryImpl @Inject constructor(
return response
}
override fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
return ApiUtils.getUrlForAvatar(
override fun getImageUri(avatarId: String, requestBigSize: Boolean): String =
ApiUtils.getUrlForAvatar(
_currentUser.baseUrl,
avatarId,
requestBigSize
)
}
override suspend fun setPassword(roomToken: String, password: String): GenericOverall {
val result = ncApiCoroutines.setPassword(

View File

@ -139,9 +139,8 @@ class ConversationCreationViewModel @Inject constructor(
}
}
fun getImageUri(avatarId: String, requestBigSize: Boolean): String {
return repository.getImageUri(avatarId, requestBigSize)
}
fun getImageUri(avatarId: String, requestBigSize: Boolean): String =
repository.getImageUri(avatarId, requestBigSize)
}
sealed class AllowGuestsUiState {

View File

@ -190,7 +190,7 @@ class ConversationInfoActivity :
binding = ActivityConversationInfoBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
setupSystemColors()
initSystemBars()
viewModel =
ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
@ -1065,21 +1065,13 @@ class ConversationInfoActivity :
) {
binding.addParticipantsAction.visibility = GONE
binding.startGroupChat.visibility = VISIBLE
showDeleteAllMessagesOption(conversationCopy)
} else if (ConversationUtils.canModerate(conversationCopy, spreedCapabilities)) {
binding.addParticipantsAction.visibility = VISIBLE
if (hasSpreedFeatureCapability(
spreedCapabilities,
SpreedFeatures.CLEAR_HISTORY
) && conversationCopy.canDeleteConversation
) {
binding.clearConversationHistory.visibility = VISIBLE
} else {
binding.clearConversationHistory.visibility = GONE
}
showDeleteAllMessagesOption(conversationCopy)
showOptionsMenu()
} else {
binding.addParticipantsAction.visibility = GONE
if (ConversationUtils.isNoteToSelfConversation(conversation)) {
binding.notificationSettingsView.notificationSettings.visibility = VISIBLE
} else {
@ -1311,6 +1303,19 @@ class ConversationInfoActivity :
}
}
fun showDeleteAllMessagesOption(conversationCopy: ConversationModel) {
if (hasSpreedFeatureCapability(
spreedCapabilities,
SpreedFeatures.CLEAR_HISTORY
) &&
conversationCopy.canDeleteConversation
) {
binding.clearConversationHistory.visibility = VISIBLE
} else {
binding.clearConversationHistory.visibility = GONE
}
}
private fun submitRecordingConsentChanges() {
val state = if (binding.recordingConsentView.recordingConsentForConversationSwitch.isChecked) {
RECORDING_CONSENT_REQUIRED_FOR_CONVERSATION

View File

@ -319,14 +319,19 @@ class ConversationInfoViewModel @Inject constructor(
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
fun getProfileData(user: User, userId: String) {
val url = ApiUtils.getUrlForProfile(user.baseUrl!!, userId)
viewModelScope.launch {
val profile = conversationsRepository.getProfile(user.getCredentials(), url)
if (profile != null) {
_getProfileViewState.value = GetProfileSuccessState(profile)
} else {
_getProfileViewState.value = GetProfileErrorState
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)
}
}
}

View File

@ -99,7 +99,7 @@ class ConversationInfoEditActivity : BaseActivity() {
binding = ActivityConversationInfoEditBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
setupSystemColors()
initSystemBars()
val extras: Bundle? = intent.extras

View File

@ -24,8 +24,7 @@ class ConversationInfoEditRepositoryImpl(
private val ncApi: NcApi,
private val ncApiCoroutines: NcApiCoroutines,
currentUserProvider: CurrentUserProviderNew
) :
ConversationInfoEditRepository {
) : ConversationInfoEditRepository {
val currentUser: User = currentUserProvider.currentUser.blockingGet()
val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)!!
@ -53,12 +52,11 @@ class ConversationInfoEditRepositoryImpl(
).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) }
}
override fun deleteConversationAvatar(user: User, roomToken: String): Observable<ConversationModel> {
return ncApi.deleteConversationAvatar(
override fun deleteConversationAvatar(user: User, roomToken: String): Observable<ConversationModel> =
ncApi.deleteConversationAvatar(
credentials,
ApiUtils.getUrlForConversationAvatar(1, user.baseUrl!!, roomToken)
).map { ConversationModel.mapToConversationModel(it.ocs?.data!!, user) }
}
override suspend fun renameConversation(roomToken: String, newRoomName: String): GenericOverall {
val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.API_V4, ApiUtils.API_V1))
@ -77,8 +75,8 @@ class ConversationInfoEditRepositoryImpl(
override suspend fun setConversationDescription(
roomToken: String,
conversationDescription: String?
): GenericOverall {
return ncApiCoroutines.setConversationDescription(
): GenericOverall =
ncApiCoroutines.setConversationDescription(
credentials,
ApiUtils.getUrlForConversationDescription(
apiVersion,
@ -87,5 +85,4 @@ class ConversationInfoEditRepositoryImpl(
),
conversationDescription
)
}
}

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,6 +40,9 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ViewCompositionStrategy
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
@ -79,6 +81,7 @@ import com.nextcloud.talk.adapters.items.GenericTextHeaderItem
import com.nextcloud.talk.adapters.items.LoadMoreResultsItem
import com.nextcloud.talk.adapters.items.MessageResultItem
import com.nextcloud.talk.adapters.items.MessagesTextHeaderItem
import com.nextcloud.talk.adapters.items.SpacerItem
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
@ -160,7 +163,6 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import retrofit2.HttpException
import java.io.File
import java.util.Objects
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -264,9 +266,11 @@ class ConversationsListActivity :
binding = ActivityConversationsBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
setupSystemColors()
viewThemeUtils.material.themeCardView(binding.searchToolbar)
viewThemeUtils.material.themeSearchBarText(binding.searchText)
initSystemBars()
viewThemeUtils.material.themeSearchCardView(binding.searchToolbar)
viewThemeUtils.material.colorMaterialButtonContent(binding.menuButton, ColorRole.ON_SURFACE)
viewThemeUtils.platform.colorTextView(binding.searchText, ColorRole.ON_SURFACE_VARIANT)
forwardMessage = intent.getBooleanExtra(KEY_FORWARD_MSG_FLAG, false)
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
@ -291,14 +295,13 @@ class ConversationsListActivity :
override fun onResume() {
super.onResume()
// actionBar?.show()
if (adapter == null) {
adapter = FlexibleAdapter(conversationItems, this, true)
addEmptyItemForEdgeToEdgeIfNecessary()
} else {
binding.loadingContent.visibility = View.GONE
}
adapter!!.addListener(this)
adapter?.addListener(this)
prepareViews()
showNotificationWarning()
@ -314,8 +317,10 @@ class ConversationsListActivity :
showServerEOLDialog()
return
}
if (isUnifiedSearchAvailable(currentUser!!.capabilities!!.spreedCapability!!)) {
searchHelper = MessageSearchHelper(unifiedSearchRepository)
currentUser?.capabilities?.spreedCapability?.let { spreedCapabilities ->
if (isUnifiedSearchAvailable(spreedCapabilities)) {
searchHelper = MessageSearchHelper(unifiedSearchRepository)
}
}
credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
@ -340,6 +345,14 @@ class ConversationsListActivity :
showSearchOrToolbar()
}
// if edge to edge is used, add an empty item at the bottom of the list
@Suppress("MagicNumber")
private fun addEmptyItemForEdgeToEdgeIfNecessary() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
adapter?.addScrollableFooter(SpacerItem(200))
}
}
@Suppress("LongMethod")
private fun initObservers() {
this.lifecycleScope.launch {
@ -409,6 +422,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()
}
@ -525,6 +542,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()
@ -640,7 +680,7 @@ class ConversationsListActivity :
}
}
val archiveFilterOn = filterState[ARCHIVE] ?: false
val archiveFilterOn = filterState[ARCHIVE] == true
if (archiveFilterOn && newItems.isEmpty()) {
binding.noArchivedConversationLayout.visibility = View.VISIBLE
} else {
@ -755,7 +795,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!!)
@ -934,8 +974,7 @@ class ConversationsListActivity :
} else {
showToolbar()
}
colorizeStatusBar()
colorizeNavigationBar()
initSystemBars()
}
}
@ -1217,7 +1256,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
@ -1398,7 +1437,7 @@ class ConversationsListActivity :
adapter?.updateDataSet(conversationItems)
adapter?.setFilter("")
adapter?.filterItems()
val archiveFilterOn = filterState[ARCHIVE] ?: false
val archiveFilterOn = filterState[ARCHIVE] == true
if (archiveFilterOn && adapter!!.isEmpty) {
binding.noArchivedConversationLayout.visibility = View.VISIBLE
} else {
@ -1445,10 +1484,9 @@ class ConversationsListActivity :
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position)
if (item != null) {
when (item.itemViewType) {
MessageResultItem.VIEW_TYPE -> {
val messageItem: MessageResultItem = item as MessageResultItem
val token = messageItem.messageEntry.conversationToken
when (item) {
is MessageResultItem -> {
val token = item.messageEntry.conversationToken
val conversationName = (
conversationItems.first {
(it is ConversationItem) && it.model.token == token
@ -1462,27 +1500,26 @@ class ConversationsListActivity :
bundle.putString(BundleKeys.KEY_CREDENTIALS, credentials!!)
bundle.putString(BundleKeys.KEY_BASE_URL, currentUser!!.baseUrl)
bundle.putString(KEY_ROOM_TOKEN, token)
bundle.putString(BundleKeys.KEY_MESSAGE_ID, messageItem.messageEntry.messageId)
bundle.putString(BundleKeys.KEY_MESSAGE_ID, item.messageEntry.messageId)
bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, conversationName)
ContextChatCompose(bundle).GetDialogView(shouldDismiss, context)
}
}
}
LoadMoreResultsItem.VIEW_TYPE -> {
is LoadMoreResultsItem -> {
loadMoreMessages()
}
ConversationItem.VIEW_TYPE -> {
handleConversation((Objects.requireNonNull(item) as ConversationItem).model)
is ConversationItem -> {
handleConversation(item.model)
}
ContactItem.VIEW_TYPE -> {
val contact = item as ContactItem
is ContactItem -> {
contactsViewModel.createRoom(
ROOM_TYPE_ONE_ONE,
null,
contact.model.actorId!!,
item.model.actorId!!,
null
)
}
@ -1809,7 +1846,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 ||
@ -1938,9 +1975,9 @@ class ConversationsListActivity :
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
.observeForever { workInfo: WorkInfo ->
.observeForever { workInfo: WorkInfo? ->
when (workInfo.state) {
when (workInfo?.state) {
WorkInfo.State.SUCCEEDED -> {
val text = String.format(
context.resources.getString(R.string.nc_deleted_user),
@ -2156,7 +2193,7 @@ class ConversationsListActivity :
binding.filterConversationsButton.let {
viewThemeUtils.platform.colorImageView(
it,
ColorRole.ON_SURFACE_VARIANT
ColorRole.ON_SURFACE
)
}
}
@ -2183,5 +2220,6 @@ class ConversationsListActivity :
const val ROOM_TYPE_ONE_ONE = "1"
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

@ -26,8 +26,7 @@ import javax.inject.Inject
class ConversationsListViewModel @Inject constructor(
private val repository: OfflineConversationsRepository,
var userManager: UserManager
) :
ViewModel() {
) : ViewModel() {
@Inject
lateinit var invitationsRepository: InvitationsRepository

View File

@ -20,24 +20,17 @@ import dagger.Provides
class ManagerModule {
@Provides
fun provideMediaRecorderManager(): MediaRecorderManager {
return MediaRecorderManager()
}
fun provideMediaRecorderManager(): MediaRecorderManager = MediaRecorderManager()
@Provides
fun provideAudioRecorderManager(): AudioRecorderManager {
return AudioRecorderManager()
}
fun provideAudioRecorderManager(): AudioRecorderManager = AudioRecorderManager()
@Provides
fun provideMediaPlayerManager(preferences: AppPreferences): MediaPlayerManager {
return MediaPlayerManager().apply {
fun provideMediaPlayerManager(preferences: AppPreferences): MediaPlayerManager =
MediaPlayerManager().apply {
appPreferences = preferences
}
}
@Provides
fun provideAudioFocusManager(context: Context): AudioFocusRequestManager {
return AudioFocusRequestManager(context)
}
fun provideAudioFocusManager(context: Context): AudioFocusRequestManager = AudioFocusRequestManager(context)
}

View File

@ -136,9 +136,7 @@ class RepositoryModule {
ncApi: NcApi,
ncApiCoroutines: NcApiCoroutines,
userProvider: CurrentUserProviderNew
): ConversationInfoEditRepository {
return ConversationInfoEditRepositoryImpl(ncApi, ncApiCoroutines, userProvider)
}
): ConversationInfoEditRepository = ConversationInfoEditRepositoryImpl(ncApi, ncApiCoroutines, userProvider)
@Provides
fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository = InvitationsRepositoryImpl(ncApi)

View File

@ -20,19 +20,13 @@ import dagger.Reusable
class UtilsModule {
@Provides
@Reusable
fun providePermissionUtil(context: Context): PlatformPermissionUtil {
return PlatformPermissionUtilImpl(context)
}
fun providePermissionUtil(context: Context): PlatformPermissionUtil = PlatformPermissionUtilImpl(context)
@Provides
@Reusable
fun provideDateUtils(context: Context): DateUtils {
return DateUtils(context)
}
fun provideDateUtils(context: Context): DateUtils = DateUtils(context)
@Provides
@Reusable
fun provideMessageUtils(context: Context): MessageUtils {
return MessageUtils(context)
}
fun provideMessageUtils(context: Context): MessageUtils = MessageUtils(context)
}

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 *
@ -60,7 +72,7 @@ interface ChatMessagesDao {
ORDER BY timestamp DESC, id DESC
"""
)
fun getTempMessageForConversation(internalConversationId: String, referenceId: String): Flow<ChatMessageEntity>
fun getTempMessageForConversation(internalConversationId: String, referenceId: String): Flow<ChatMessageEntity?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertChatMessages(chatMessages: List<ChatMessageEntity>)

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

@ -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

@ -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

@ -27,9 +27,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NetworkMonitorImpl @Inject constructor(
private val context: Context
) : NetworkMonitor {
class NetworkMonitorImpl @Inject constructor(private val context: Context) : NetworkMonitor {
private val connectivityManager = context.getSystemService<ConnectivityManager>()!!

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")
@ -76,6 +89,8 @@ object Migrations {
}
}
//endregion
fun migrateToRoom(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE User_new (" +

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 = 16,
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() {

View File

@ -13,13 +13,12 @@ import com.nextcloud.talk.models.json.capabilities.Capabilities
class CapabilitiesConverter {
@TypeConverter
fun fromCapabilitiesToString(capabilities: Capabilities?): String {
return if (capabilities == null) {
fun fromCapabilitiesToString(capabilities: Capabilities?): String =
if (capabilities == null) {
""
} else {
LoganSquare.serialize(capabilities)
}
}
@TypeConverter
fun fromStringToCapabilities(value: String): Capabilities? {

View File

@ -13,13 +13,12 @@ import com.nextcloud.talk.models.ExternalSignalingServer
class ExternalSignalingServerConverter {
@TypeConverter
fun fromExternalSignalingServerToString(externalSignalingServer: ExternalSignalingServer?): String {
return if (externalSignalingServer == null) {
fun fromExternalSignalingServerToString(externalSignalingServer: ExternalSignalingServer?): String =
if (externalSignalingServer == null) {
""
} else {
LoganSquare.serialize(externalSignalingServer)
}
}
@TypeConverter
fun fromStringToExternalSignalingServer(value: String): ExternalSignalingServer? {

View File

@ -44,8 +44,8 @@ class LinkedHashMapConverter {
}
@TypeConverter
fun linkedHashMapToString(map: LinkedHashMap<String, Int>?): String {
return try {
fun linkedHashMapToString(map: LinkedHashMap<String, Int>?): String =
try {
val stringWriter = java.io.StringWriter()
jsonFactory.createGenerator(stringWriter).use { generator ->
converter.serialize(map ?: linkedMapOf(), null, false, generator)
@ -55,5 +55,4 @@ class LinkedHashMapConverter {
// e.printStackTrace()
""
}
}
}

View File

@ -14,13 +14,12 @@ import com.nextcloud.talk.models.json.push.PushConfigurationState
class PushConfigurationConverter {
@TypeConverter
fun fromPushConfigurationToString(pushConfiguration: PushConfigurationState?): String {
return if (pushConfiguration == null) {
fun fromPushConfigurationToString(pushConfiguration: PushConfigurationState?): String =
if (pushConfiguration == null) {
""
} else {
LoganSquare.serialize(pushConfiguration)
}
}
@TypeConverter
fun fromStringToPushConfiguration(value: String?): PushConfigurationState? {

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