Compare commits

..

291 Commits

Author SHA1 Message Date
Sowjanya Kota
186be519ba
Merge pull request #5139 from nextcloud/bugfix/5138/reduceAvatarSize
reduce avatar size for group and all mentions
2025-07-16 17:54:56 +02:00
sowjanyakch
d25d5dc4a4
fix ktlint
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-16 17:36:20 +02:00
sowjanyakch
67e5dcd5da
use circular outlined team icon
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-16 17:33:24 +02:00
sowjanyakch
32bcae77cc
use circular outlined team icon
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-16 17:33:24 +02:00
sowjanyakch
f5faa3b3f9
use outlined team icon
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-16 17:33:24 +02:00
sowjanyakch
b42d8acd02
modify year
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-16 17:33:24 +02:00
sowjanyakch
e0cbb14d10
ktlintFormat
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-16 17:33:23 +02:00
sowjanyakch
76129229f8
add consistent icons for mentions also
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-16 17:33:23 +02:00
sowjanyakch
120974bc00
add consistent icons
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-16 17:33:23 +02:00
sowjanyakch
977427bd4d
add circular group icons
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-16 17:33:22 +02:00
Marcel Hibbe
1b2d419351
Merge pull request #5049 from nextcloud/issue-5041-jump-to-coversation
Preserve Conversation List position
2025-07-16 16:46:47 +02:00
Marcel Hibbe
38b5098492
move saving of conversation list scroll-position to onPause
otherwise, it's would not work when just reopening the app from background

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-07-16 15:58:44 +02:00
Marcel Hibbe
e00ded45de
Merge pull request #5155 from nextcloud/chore/5103/support16kbPageSizes
Support 16 KB page sizes
2025-07-16 15:49:17 +02:00
rapterjet2004
71e454d135
linter
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-07-16 15:36:07 +02:00
rapterjet2004
23e5435092
Updated scroll logic + removed old approach
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-07-16 15:36:06 +02:00
rapterjet2004
2960e693ce
Conversation List jumps to position of clicked conversation item when back pressed
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-07-16 15:36:05 +02:00
Marcel Hibbe
f760ee3437
Merge pull request #5145 from nextcloud/issue-1071-login-qr
Login via QR Code
2025-07-16 15:22:20 +02:00
Andy Scherzinger
cd81dc5cb5
Merge pull request #5154 from nextcloud/renovate/ubuntu-noble
chore(deps): update ubuntu:noble docker digest to c4570d2
2025-07-16 14:20:03 +02:00
Marcel Hibbe
34903931bf
Support 16 KB page sizes
by replacing
net.zetetic:android-database-sqlcipher
with
net.zetetic:sqlcipher-android

which should hopefully solve the error

Exception java.lang.UnsatisfiedLinkError: dlopen failed: empty/missing DT_HASH/DT_GNU_HASH in "/data/app/~~HfdEzwuB9IXXVa0Fvds_Kw==/com.nextcloud.talk2-yCC7T_sy5lQWb9w17SZlVQ==/base.apk!/lib/arm64-v8a/libsqlcipher.so" (new hash type from the future?)
  at java.lang.Runtime.loadLibrary0 (Runtime.java:1081)
  at java.lang.Runtime.loadLibrary0 (Runtime.java:1003)
  at java.lang.System.loadLibrary (System.java:1765)
  at net.sqlcipher.database.SQLiteDatabase$1.loadLibraries (SQLiteDatabase.java:230)
  at net.sqlcipher.database.SQLiteDatabase.loadLibs (SQLiteDatabase.java:247)
  at net.sqlcipher.database.SQLiteDatabase.loadLibs (SQLiteDatabase.java:226)
  at net.sqlcipher.database.SQLiteDatabase.loadLibs (SQLiteDatabase.java:219)
  at net.sqlcipher.database.SupportHelper.<init> (SupportHelper.java:31)
  at net.sqlcipher.database.SupportFactory.create (SupportFactory.java:43)
  at androidx.room.RoomConnectionManager.<init> (RoomConnectionManager.android.kt:68)
  at androidx.room.RoomDatabase.createConnectionManager$room_runtime_release (RoomDatabase.android.kt:318)
  at androidx.room.RoomDatabase.init (RoomDatabase.android.kt:229)
  at androidx.room.RoomDatabase$Builder.build (RoomDatabase.android.kt:1769)
  at com.nextcloud.talk.data.source.local.TalkDatabase$Companion.build (TalkDatabase.kt:136)
  at com.nextcloud.talk.data.source.local.TalkDatabase$Companion.getInstance (TalkDatabase.kt:89)
  at com.nextcloud.talk.data.source.local.TalkDatabase.getInstance (Unknown Source:2)
  at com.nextcloud.talk.dagger.modules.DatabaseModule.provideTalkDatabase (DatabaseModule.java:47)
  at com.nextcloud.talk.dagger.modules.DatabaseModule_ProvideTalkDatabaseFactory.provideTalkDatabase (DatabaseModule_ProvideTalkDatabaseFactory.java:56)
  at com.nextcloud.talk.dagger.modules.DatabaseModule_ProvideTalkDatabaseFactory.get (DatabaseModule_ProvideTalkDatabaseFactory.java:46)
  at com.nextcloud.talk.dagger.modules.DatabaseModule_ProvideTalkDatabaseFactory.get (DatabaseModule_ProvideTalkDatabaseFactory.java:14)
  at dagger.internal.DoubleCheck.getSynchronized (DoubleCheck.java:54)
  at dagger.internal.DoubleCheck.get (DoubleCheck.java:45)
  at com.nextcloud.talk.dagger.modules.RepositoryModule_ProvideUsersRepositoryFactory.get (RepositoryModule_ProvideUsersRepositoryFactory.java:42)
  at com.nextcloud.talk.dagger.modules.RepositoryModule_ProvideUsersRepositoryFactory.get (RepositoryModule_ProvideUsersRepositoryFactory.java:13)
  at com.nextcloud.talk.utils.database.user.UserModule_Companion_ProvideUserManagerFactory.get (UserModule_Companion_ProvideUserManagerFactory.java:39)
  at com.nextcloud.talk.utils.database.user.UserModule_Companion_ProvideUserManagerFactory.get (UserModule_Companion_ProvideUserManagerFactory.java:13)
  at com.nextcloud.talk.dagger.modules.RestModule_ProvideKeyManagerFactory.get (RestModule_ProvideKeyManagerFactory.java:46)
  at com.nextcloud.talk.dagger.modules.RestModule_ProvideKeyManagerFactory.get (RestModule_ProvideKeyManagerFactory.java:14)
  at dagger.internal.DoubleCheck.getSynchronized (DoubleCheck.java:54)
  at dagger.internal.DoubleCheck.get (DoubleCheck.java:45)
  at com.nextcloud.talk.dagger.modules.RestModule_ProvideSslSocketFactoryCompatFactory.get (RestModule_ProvideSslSocketFactoryCompatFactory.java:46)
  at com.nextcloud.talk.dagger.modules.RestModule_ProvideSslSocketFactoryCompatFactory.get (RestModule_ProvideSslSocketFactoryCompatFactory.java:14)
  at dagger.internal.DoubleCheck.getSynchronized (DoubleCheck.java:54)
  at dagger.internal.DoubleCheck.get (DoubleCheck.java:45)
  at com.nextcloud.talk.dagger.modules.RestModule_ProvideHttpClientFactory.get (RestModule_ProvideHttpClientFactory.java:69)
  at com.nextcloud.talk.dagger.modules.RestModule_ProvideHttpClientFactory.get (RestModule_ProvideHttpClientFactory.java:19)
  at dagger.internal.DoubleCheck.getSynchronized (DoubleCheck.java:54)
  at dagger.internal.DoubleCheck.get (DoubleCheck.java:45)
  at com.nextcloud.talk.application.DaggerNextcloudTalkApplicationComponent$NextcloudTalkApplicationComponentImpl.injectNextcloudTalkApplication (DaggerNextcloudTalkApplicationComponent.java:1629)
  at com.nextcloud.talk.application.DaggerNextcloudTalkApplicationComponent$NextcloudTalkApplicationComponentImpl.inject (DaggerNextcloudTalkApplicationComponent.java:997)
  at com.nextcloud.talk.application.NextcloudTalkApplication.onCreate (NextcloudTalkApplication.kt:147)
  at android.app.Instrumentation.callApplicationOnCreate (Instrumentation.java:1386)
  at android.app.ActivityThread.handleBindApplication (ActivityThread.java:7504)
  at android.app.ActivityThread.-$$Nest$mhandleBindApplication (Unknown Source)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2416)
  at android.os.Handler.dispatchMessage (Handler.java:107)
  at android.os.Looper.loopOnce (Looper.java:232)
  at android.os.Looper.loop (Looper.java:317)
  at android.app.ActivityThread.main (ActivityThread.java:8705)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:580)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:886)

  which is reported in gplay console pre publish checks and blocks new releases

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-07-16 14:08:36 +02:00
renovate[bot]
ce2ec92e6b
chore(deps): update ubuntu:noble docker digest to c4570d2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-16 10:54:06 +00:00
Andy Scherzinger
504878725a
Merge pull request #5152 from nextcloud/renovate/ubuntu-noble
chore(deps): update ubuntu:noble docker digest to e356c06
2025-07-16 07:51:50 +02:00
renovate[bot]
09b7993e2a
chore(deps): update ubuntu:noble docker digest to e356c06
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-16 05:49:51 +00:00
Andy Scherzinger
2b76e3e415
Merge pull request #5151 from nextcloud/renovate/ubuntu-noble
chore(deps): update ubuntu:noble docker digest to a328b84
2025-07-16 07:48:36 +02:00
renovate[bot]
21469588af
chore(deps): update ubuntu:noble docker digest to a328b84
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-16 01:37:23 +00:00
rapterjet2004
9e019440d4
- aligned qr dependency w/ files app
- aligned qr image with files
- Exiting qr directs back to ServerSelectionActivity

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-07-15 11:52:38 -05:00
Nextcloud Android Bot
42efbead33 Weekly 22.0.0 Alpha 10 2025-07-14 03:11:10 +00:00
Nextcloud bot
ca90a6decf
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-07-14 02:53:02 +00:00
Nextcloud bot
dad5389c03
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-07-13 02:52:59 +00:00
Nextcloud bot
042fc9a9e3
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-07-12 02:53:55 +00:00
rapterjet2004
1918349c93
linter
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-07-11 10:46:53 -05:00
rapterjet2004
e643039488
got the logic down - need to check if it works
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-07-11 10:16:45 -05:00
Andy Scherzinger
04ad846a18
Merge pull request #5146 from nextcloud/renovate/com.android.tools.build-gradle-8.x
fix(deps): update dependency com.android.tools.build:gradle to v8.11.1
2025-07-11 14:58:18 +02:00
Andy Scherzinger
7f2f0a606a
ci(chksm): Update meta-data
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
2025-07-11 13:19:38 +02:00
renovate[bot]
57b5241985
fix(deps): update dependency com.android.tools.build:gradle to v8.11.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 13:12:35 +02:00
Andy Scherzinger
b905ab6be3
Merge pull request #5147 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.2
2025-07-11 13:10:51 +02:00
Nextcloud bot
263969982f
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-07-11 02:55:30 +00:00
renovate[bot]
d274a777be
fix(deps): update dependency com.github.spotbugs.snom:spotbugs-gradle-plugin to v6.2.2
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 01:32:40 +00:00
Sowjanya Kota
d5d0feb947
Merge pull request #5137 from nextcloud/scale_icons
use scaled icons for groups and teams in participants view
2025-07-10 22:20:04 +02:00
sowjanyakch
13b82a69ed
rename icon name + use scaled icons for participants view
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-10 21:55:24 +02:00
Sowjanya Kota
e2f5ce3d84
Merge pull request #5117 from nextcloud/user_status_dialog
Bug in user status dialog
2025-07-10 21:17:17 +02:00
sowjanyakch
d62c1b62fa
fix bug
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-07-10 21:02:40 +02:00
Julius Linus
fa9768d767
Merge pull request #5100 from nextcloud/issue-4711-login-flow-v2
Login flow v2
2025-07-10 08:34:25 -05:00
rapterjet2004
dad5f1714a
Added new login option
renamed WebViewLoginActivity.kt to BrowserLoginActivity.kt

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
2025-07-10 08:12:27 -05:00
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 #5133 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 #5141 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 #5131 from nextcloud/fix_autocomplete
Fix mentions of phone conversation
2025-07-08 13:16:17 +02:00
Sowjanya Kota
5704be67fd
Merge pull request #5110 from nextcloud/bug_fix
Scale DatePicker for smaller screens
2025-07-08 12:39:36 +02:00
Sowjanya Kota
c5942fe575
Merge pull request #5111 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 #5072 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 #5127 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 #5124 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 #5122 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 #5121 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 #4402 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 #5119 from nextcloud/bumpJacksonCore
bump jackson core version
2025-07-03 12:30:07 +02:00
Andy Scherzinger
7f6f077680
Merge pull request #5108 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 #5116 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 #5114 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 #5107 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 #5099 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 #5102 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 #5101 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 #5092 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 #5088 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 #5098 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 #5089 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 #5094 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 #5090 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 #5087 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 #5086 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 #4994 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 #4481 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 #4932 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 #4616 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 #4954 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 #5081 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 #5077 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 #5082 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 #4933 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 #5083 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 #5084 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 #5080 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 #5074 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 #5075 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 #4401 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 #4618 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 #4914 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 #4876 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 #5022 from nextcloud/bugfix/4921/avoidDupMessagesByBetterSentStatus
avoid duplicate messages
2025-06-18 18:36:17 +02:00
Nextcloud bot
2a7359c1e9
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-18 03:08:20 +00:00
Marcel Hibbe
86bfaa8657
improve send status handling
replace sendingFailed with SendStatus

add auto migration (incl deleting of column sendingFailed)

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

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

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

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

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

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

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

Without this fix there would be the crash:

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

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

startContextChatWindowForMessage

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

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

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-03 12:16:16 +02:00
Marcel Hibbe
14fd9b4af8
rely on conversationOperationLeave to show "Leave conversation" button
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-03 12:12:04 +02:00
Marcel Hibbe
9fe39603c3
rely on canDeleteConversation to show "Delete conversation" button
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
2025-06-03 12:09:50 +02:00
Nextcloud bot
707371603d
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-03 03:05:04 +00:00
sowjanyakch
632a26d3cb
format
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-06-02 15:28:50 +02:00
sowjanyakch
262205b615
Include mentions in the attachments
Signed-off-by: sowjanyakch <sowjanya.kch@gmail.com>
2025-06-02 14:59:32 +02:00
Nextcloud bot
131723317b
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-02 03:39:23 +00:00
Nextcloud Android Bot
e299023ef1 Weekly 22.0.0 Alpha 04 2025-06-02 03:11:10 +00:00
Nextcloud bot
92e1028354
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-06-01 03:03:48 +00:00
Andy Scherzinger
ebc41c33d0
Merge pull request #5015 from nextcloud/repo-sync/android-config/master
🔄 synced file(s) with nextcloud/android-config
2025-05-31 09:05:48 +02:00
nextcloud-android-bot
b11e5a474c 🔄 synced local '.github/workflows/' with remote 'config/workflows/'
Signed-off-by: nextcloud-android-bot <android@nextcloud.com>
2025-05-31 03:08:55 +00:00
Nextcloud bot
54ac15d089
fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2025-05-31 03:04:30 +00:00
github-actions[bot]
b3c7990c88
Merge pull request #5013 from nextcloud/renovate/junit5-monorepo
Update dependency org.junit.vintage:junit-vintage-engine to v5.13.0
2025-05-30 21:17:46 +02:00
github-actions[bot]
88ed309220
Merge pull request #5012 from nextcloud/renovate/com.github.spotbugs.snom-spotbugs-gradle-plugin-6.x
Update dependency com.github.spotbugs.snom:spotbugs-gradle-plugin to v6.1.13
2025-05-30 21:15:00 +02:00
github-actions[bot]
5855ec7b38
Merge pull request #5011 from nextcloud/renovate/com.android.tools.build-gradle-8.x
Update dependency com.android.tools.build:gradle to v8.10.1
2025-05-30 21:07:58 +02:00
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
418 changed files with 9578 additions and 3282 deletions

View File

@ -1,4 +1,4 @@
FROM ubuntu:noble@sha256:6015f66923d7afbc53558d7ccffd325d43b4e249f41a6e93eef074c9505d2233
FROM ubuntu:noble@sha256:c4570d2f4665d5d118ae29fb494dee4f8db8fcfaee0e37a2e19b827f399070d3
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 210010053
versionName "21.1.0 RC3"
versionCode 220000010
versionName "22.0.0 Alpha 10"
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"
@ -210,6 +210,7 @@ dependencies {
exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser
})
implementation 'org.conscrypt:conscrypt-android:2.5.3'
implementation "com.github.nextcloud-deps:qrcodescanner:0.1.2.4" // "com.github.blikoon:QRCodeScanner:0.1.2"
implementation "androidx.camera:camera-core:${androidxCameraVersion}"
implementation "androidx.camera:camera-camera2:${androidxCameraVersion}"
@ -236,7 +237,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}"
@ -251,7 +252,7 @@ dependencies {
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
implementation 'org.greenrobot:eventbus:3.3.1'
implementation 'net.zetetic:android-database-sqlcipher:4.5.4'
implementation 'net.zetetic:sqlcipher-android:4.9.0'
implementation "androidx.room:room-runtime:${roomVersion}"
implementation "androidx.room:room-rxjava2:${roomVersion}"
@ -301,24 +302,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 +344,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

@ -123,7 +123,7 @@
android:theme="@style/AppTheme" />
<activity
android:name=".account.WebViewLoginActivity"
android:name=".account.BrowserLoginActivity"
android:theme="@style/AppTheme" />
<activity android:name=".contacts.ContactsActivity"

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()
}
@ -158,7 +158,7 @@ class AccountVerificationActivity : BaseActivity() {
bundle.putString(KEY_USERNAME, username)
bundle.putString(KEY_PASSWORD, "")
val intent = Intent(context, WebViewLoginActivity::class.java)
val intent = Intent(context, BrowserLoginActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
} else {
@ -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

@ -0,0 +1,417 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.account
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import androidx.activity.OnBackPressedCallback
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
import com.google.android.material.snackbar.Snackbar
import com.google.gson.JsonParser
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ActivityWebViewLoginBinding
import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.models.LoginData
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
import com.nextcloud.talk.utils.ssl.SSLSocketFactoryCompat
import com.nextcloud.talk.utils.ssl.TrustManager
import io.reactivex.disposables.Disposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.ConnectionSpec
import okhttp3.CookieJar
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.net.URLDecoder
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.net.ssl.SSLHandshakeException
import javax.net.ssl.SSLSession
@AutoInjector(NextcloudTalkApplication::class)
class BrowserLoginActivity : BaseActivity() {
private lateinit var binding: ActivityWebViewLoginBinding
@Inject
lateinit var userManager: UserManager
@Inject
lateinit var trustManager: TrustManager
@Inject
lateinit var socketFactory: SSLSocketFactoryCompat
private var userQueryDisposable: Disposable? = null
private var baseUrl: String? = null
private var reauthorizeAccount = false
private var username: String? = null
private var password: String? = null
private val loginFlowExecutorService: ScheduledExecutorService? = Executors.newSingleThreadScheduledExecutor()
private var isLoginProcessCompleted = false
private var token: String = ""
private lateinit var okHttpClient: OkHttpClient
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
}
}
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedApplication!!.componentApplication.inject(this)
binding = ActivityWebViewLoginBinding.inflate(layoutInflater)
okHttpClient = OkHttpClient.Builder()
.cookieJar(CookieJar.NO_COOKIES)
.connectionSpecs(listOf(ConnectionSpec.COMPATIBLE_TLS))
.sslSocketFactory(socketFactory, trustManager)
.hostnameVerifier { _: String?, _: SSLSession? -> true }
.build()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
setContentView(binding.root)
actionBar?.hide()
initSystemBars()
initViews()
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
handleIntent()
lifecycle.addObserver(lifecycleEventObserver)
}
private fun handleIntent() {
val extras = intent.extras!!
baseUrl = extras.getString(KEY_BASE_URL)
username = extras.getString(KEY_USERNAME)
if (extras.containsKey(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)) {
reauthorizeAccount = extras.getBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)
}
if (extras.containsKey(BundleKeys.KEY_PASSWORD)) {
password = extras.getString(BundleKeys.KEY_PASSWORD)
}
if (extras.containsKey(BundleKeys.KEY_FROM_QR)) {
val resultData = extras.getString(BundleKeys.KEY_FROM_QR)
try {
parseLoginDataUrl(resultData!!)
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Error in scanning QR Code: $e")
}
} else {
anonymouslyPostLoginRequest()
}
}
private fun initViews() {
viewThemeUtils.material.colorMaterialButtonFilledOnPrimary(binding.cancelLoginBtn)
viewThemeUtils.material.colorProgressBar(binding.progressBar)
binding.cancelLoginBtn.setOnClickListener {
lifecycle.removeObserver(lifecycleEventObserver)
onBackPressedDispatcher.onBackPressed()
}
}
private fun anonymouslyPostLoginRequest() {
CoroutineScope(Dispatchers.IO).launch {
val url = "$baseUrl/index.php/login/v2"
try {
val response = getResponseOfAnonymouslyPostLoginRequest(url)
val jsonObject: com.google.gson.JsonObject = JsonParser.parseString(response).asJsonObject
val loginUrl: String = getLoginUrl(jsonObject)
withContext(Dispatchers.Main) {
launchDefaultWebBrowser(loginUrl)
}
token = jsonObject.getAsJsonObject("poll").get("token").asString
} catch (e: SSLHandshakeException) {
Log.e(TAG, "Error caught at anonymouslyPostLoginRequest: $e")
}
}
}
private fun getResponseOfAnonymouslyPostLoginRequest(url: String): String? {
val request = Request.Builder()
.url(url)
.post(FormBody.Builder().build())
.addHeader("Clear-Site-Data", "cookies")
.build()
okHttpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("Unexpected code $response")
}
return response.body?.string()
}
}
private fun getLoginUrl(response: com.google.gson.JsonObject): String {
var result: String? = response.get("login").asString
if (result == null) {
result = ""
}
return result
}
private fun launchDefaultWebBrowser(url: String) {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
private val lifecycleEventObserver = LifecycleEventObserver { lifecycleOwner, event ->
if (event === Lifecycle.Event.ON_START && token != "") {
Log.d(TAG, "Start poolLogin")
poolLogin()
}
}
private fun poolLogin() {
loginFlowExecutorService?.scheduleWithFixedDelay({
if (!isLoginProcessCompleted) {
performLoginFlowV2()
}
}, 0, INTERVAL, TimeUnit.SECONDS)
}
private fun performLoginFlowV2() {
val postRequestUrl = "$baseUrl/login/v2/poll"
val requestBody: RequestBody = FormBody.Builder()
.add("token", token)
.build()
val request = Request.Builder()
.url(postRequestUrl)
.post(requestBody)
.build()
try {
okHttpClient.newCall(request).execute()
.use { response ->
if (!response.isSuccessful) {
throw IOException("Unexpected code $response")
}
val status: Int = response.code
val response = response.body?.string()
Log.d(TAG, "performLoginFlowV2 status: $status")
Log.d(TAG, "performLoginFlowV2 response: $response")
if (response?.isNotEmpty() == true) {
runOnUiThread { completeLoginFlow(response, status) }
}
}
} catch (e: IllegalStateException) {
Log.e(TAG, "Error caught at performLoginFlowV2: $e")
}
}
/**
* QR returns a URI of format `nc://login/server:xxx&user:xxx&password:xxx`
* with the variables not always been in the provided order
*
* @throws IllegalArgumentException
*/
fun parseLoginDataUrl(dataString: String) {
if (!dataString.startsWith(PREFIX)) {
throw IllegalArgumentException("Invalid login URL detected")
}
val data = dataString.removePrefix(PREFIX)
val values = data.split('&')
if (values.size !in 1..MAX_ARGS) {
throw IllegalArgumentException("Illegal number of login URL elements detected: ${values.size}")
}
val loginData = LoginData()
values.forEach { value ->
when {
value.startsWith(USER_KEY) -> {
loginData.username = URLDecoder.decode(value.removePrefix(USER_KEY), "UTF-8")
}
value.startsWith(PASS_KEY) -> {
loginData.token = URLDecoder.decode(value.removePrefix(PASS_KEY), "UTF-8")
}
value.startsWith(SERVER_KEY) -> {
loginData.serverUrl = URLDecoder.decode(value.removePrefix(SERVER_KEY), "UTF-8")
baseUrl = loginData.serverUrl
}
}
}
parseAndLogin(loginData)
}
private fun completeLoginFlow(response: String, status: Int) {
try {
val jsonObject = JSONObject(response)
val server: String = jsonObject.getString("server")
val loginName: String = jsonObject.getString("loginName")
val appPassword: String = jsonObject.getString("appPassword")
val loginData = LoginData()
loginData.serverUrl = server
loginData.username = loginName
loginData.token = appPassword
isLoginProcessCompleted =
(status == HTTP_OK && !server.isEmpty() && !loginName.isEmpty() && !appPassword.isEmpty())
parseAndLogin(loginData)
} catch (e: JSONException) {
Log.e(TAG, "Error caught at completeLoginFlow: $e")
}
loginFlowExecutorService?.shutdown()
lifecycle.removeObserver(lifecycleEventObserver)
}
private fun dispose() {
if (userQueryDisposable != null && !userQueryDisposable!!.isDisposed) {
userQueryDisposable!!.dispose()
}
userQueryDisposable = null
}
private fun parseAndLogin(loginData: LoginData) {
dispose()
if (userManager.checkIfUserIsScheduledForDeletion(loginData.username!!, baseUrl!!).blockingGet()) {
Log.e(TAG, "Tried to add already existing user who is scheduled for deletion.")
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
// however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it.
startAccountRemovalWorkerAndRestartApp()
} else if (userManager.checkIfUserExists(loginData.username!!, baseUrl!!).blockingGet()) {
if (reauthorizeAccount) {
updateUserAndRestartApp(loginData)
} else {
Log.w(TAG, "It was tried to add an account that account already exists. Skipped user creation.")
restartApp()
}
} else {
startAccountVerification(loginData)
}
}
private fun startAccountVerification(loginData: LoginData) {
val bundle = Bundle()
bundle.putString(KEY_USERNAME, loginData.username)
bundle.putString(KEY_TOKEN, loginData.token)
bundle.putString(KEY_BASE_URL, loginData.serverUrl)
var protocol = ""
if (baseUrl!!.startsWith("http://")) {
protocol = "http://"
} else if (baseUrl!!.startsWith("https://")) {
protocol = "https://"
}
if (!TextUtils.isEmpty(protocol)) {
bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol)
}
val intent = Intent(context, AccountVerificationActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
}
private fun restartApp() {
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
}
private fun updateUserAndRestartApp(loginData: LoginData) {
val currentUser = currentUserProvider.currentUser.blockingGet()
if (currentUser != null) {
currentUser.clientCertificate = appPreferences.temporaryClientCertAlias
currentUser.token = loginData.token
val rowsUpdated = userManager.updateOrCreateUser(currentUser).blockingGet()
Log.d(TAG, "User rows updated: $rowsUpdated")
restartApp()
}
}
private fun startAccountRemovalWorkerAndRestartApp() {
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
.observeForever { workInfo: WorkInfo? ->
when (workInfo?.state) {
WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> {
restartApp()
}
else -> {}
}
}
}
public override fun onDestroy() {
super.onDestroy()
dispose()
}
init {
sharedApplication!!.componentApplication.inject(this)
}
override val appBarLayoutType: AppBarLayoutType
get() = AppBarLayoutType.EMPTY
companion object {
private val TAG = BrowserLoginActivity::class.java.simpleName
private const val INTERVAL = 30L
private const val HTTP_OK = 200
private const val USER_KEY = "user:"
private const val SERVER_KEY = "server:"
private const val PASS_KEY = "password:"
private const val PREFIX = "nc://login/"
private const val MAX_ARGS = 3
}
}

View File

@ -8,6 +8,7 @@
*/
package com.nextcloud.talk.account
import android.Manifest
import android.accounts.Account
import android.annotation.SuppressLint
import android.content.Intent
@ -21,8 +22,13 @@ import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import autodagger.AutoInjector
import com.blikoon.qrcodescanner.QrCodeActivity
import com.github.dhaval2404.imagepicker.util.PermissionUtil
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.api.NcApi
@ -78,7 +84,7 @@ class ServerSelectionActivity : BaseActivity() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
setContentView(binding.root)
actionBar?.hide()
setupSystemColors()
initSystemBars()
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
@ -120,6 +126,8 @@ class ServerSelectionActivity : BaseActivity() {
}
binding.certTextView.setOnClickListener { onCertClick() }
binding.scanQr.setOnClickListener { onScan() }
if (ApplicationWideMessageHolder.getInstance().messageType != null) {
if (ApplicationWideMessageHolder.getInstance().messageType
== ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
@ -154,10 +162,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 +211,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")
@ -333,7 +339,7 @@ class ServerSelectionActivity : BaseActivity() {
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_BASE_URL, queryUrl.replace("/status.php", ""))
val intent = Intent(context, WebViewLoginActivity::class.java)
val intent = Intent(context, BrowserLoginActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
}
@ -362,9 +368,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
@ -393,6 +398,52 @@ class ServerSelectionActivity : BaseActivity() {
}
}
private val requestCameraPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
// Permission was granted
startQRScanner()
}
}
fun onScan() {
if (PermissionUtil.isPermissionGranted(this, Manifest.permission.CAMERA)) {
startQRScanner()
} else {
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
private fun startQRScanner() {
val intent = Intent(this, QrCodeActivity::class.java)
qrScanResultLauncher.launch(intent)
}
private val qrScanResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val data = result.data
if (data == null) {
return@registerForActivityResult
}
val resultData = data.getStringExtra(QR_URI)
if (resultData == null || !resultData.startsWith("nc")) {
Snackbar.make(binding.root, getString(R.string.qr_code_error), Snackbar.LENGTH_SHORT).show()
return@registerForActivityResult
}
val intent = Intent(this, BrowserLoginActivity::class.java)
val bundle = bundleOf().apply {
putString(BundleKeys.KEY_FROM_QR, resultData)
}
intent.putExtras(bundle)
startActivity(intent)
}
}
public override fun onDestroy() {
super.onDestroy()
dispose()
@ -411,5 +462,6 @@ class ServerSelectionActivity : BaseActivity() {
companion object {
private val TAG = ServerSelectionActivity::class.java.simpleName
const val MIN_SERVER_MAJOR_VERSION = 13
private const val QR_URI = "com.blikoon.qrcodescanner.got_qr_scan_relult"
}
}

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

@ -1,459 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.account
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.Bitmap
import android.net.http.SslError
import android.os.Build
import android.os.Bundle
import android.security.KeyChain
import android.security.KeyChainException
import android.text.TextUtils
import android.util.Log
import android.view.View
import android.webkit.ClientCertRequest
import android.webkit.CookieSyncManager
import android.webkit.SslErrorHandler
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.OnBackPressedCallback
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ActivityWebViewLoginBinding
import com.nextcloud.talk.events.CertificateEvent
import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.models.LoginData
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
import com.nextcloud.talk.utils.ssl.TrustManager
import de.cotech.hw.fido.WebViewFidoBridge
import de.cotech.hw.fido2.WebViewWebauthnBridge
import de.cotech.hw.fido2.ui.WebauthnDialogOptions
import io.reactivex.disposables.Disposable
import java.lang.reflect.Field
import java.net.CookieManager
import java.net.URLDecoder
import java.security.PrivateKey
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.Locale
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class WebViewLoginActivity : BaseActivity() {
private lateinit var binding: ActivityWebViewLoginBinding
@Inject
lateinit var userManager: UserManager
@Inject
lateinit var trustManager: TrustManager
@Inject
lateinit var cookieManager: CookieManager
private var assembledPrefix: String? = null
private var userQueryDisposable: Disposable? = null
private var baseUrl: String? = null
private var reauthorizeAccount = false
private var username: String? = null
private var password: String? = null
private var loginStep = 0
private var automatedLoginAttempted = false
private var webViewFidoBridge: WebViewFidoBridge? = null
private var webViewWebauthnBridge: WebViewWebauthnBridge? = null
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
}
}
private val webLoginUserAgent: String
get() = (
Build.MANUFACTURER.substring(0, 1).uppercase(Locale.getDefault()) +
Build.MANUFACTURER.substring(1).uppercase(Locale.getDefault()) +
" " +
Build.MODEL +
" (" +
resources!!.getString(R.string.nc_app_product_name) +
")"
)
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedApplication!!.componentApplication.inject(this)
binding = ActivityWebViewLoginBinding.inflate(layoutInflater)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
setContentView(binding.root)
actionBar?.hide()
setupSystemColors()
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
handleIntent()
setupWebView()
}
private fun handleIntent() {
val extras = intent.extras!!
baseUrl = extras.getString(KEY_BASE_URL)
username = extras.getString(KEY_USERNAME)
if (extras.containsKey(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)) {
reauthorizeAccount = extras.getBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)
}
if (extras.containsKey(BundleKeys.KEY_PASSWORD)) {
password = extras.getString(BundleKeys.KEY_PASSWORD)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
assembledPrefix = resources!!.getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/"
binding.webview.settings.allowFileAccess = false
binding.webview.settings.allowFileAccessFromFileURLs = false
binding.webview.settings.javaScriptEnabled = true
binding.webview.settings.javaScriptCanOpenWindowsAutomatically = false
binding.webview.settings.domStorageEnabled = true
binding.webview.settings.userAgentString = webLoginUserAgent
binding.webview.settings.saveFormData = false
binding.webview.settings.savePassword = false
binding.webview.settings.setRenderPriority(WebSettings.RenderPriority.HIGH)
binding.webview.clearCache(true)
binding.webview.clearFormData()
binding.webview.clearHistory()
WebView.clearClientCertPreferences(null)
webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView(this, binding.webview)
val webauthnOptionsBuilder = WebauthnDialogOptions.builder().setShowSdkLogo(true).setAllowSkipPin(true)
webViewWebauthnBridge = WebViewWebauthnBridge.createInstanceForWebView(
this, binding.webview, webauthnOptionsBuilder
)
CookieSyncManager.createInstance(this)
android.webkit.CookieManager.getInstance().removeAllCookies(null)
val headers: MutableMap<String, String> = HashMap()
headers["OCS-APIRequest"] = "true"
binding.webview.webViewClient = object : WebViewClient() {
private var basePageLoaded = false
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
webViewFidoBridge?.delegateShouldInterceptRequest(view, request)
webViewWebauthnBridge?.delegateShouldInterceptRequest(view, request)
return super.shouldInterceptRequest(view, request)
}
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
webViewFidoBridge?.delegateOnPageStarted(view, url, favicon)
webViewWebauthnBridge?.delegateOnPageStarted(view, url, favicon)
}
@Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (url.startsWith(assembledPrefix!!)) {
parseAndLoginFromWebView(url)
return true
}
return false
}
@Suppress("Detekt.TooGenericExceptionCaught")
override fun onPageFinished(view: WebView, url: String) {
loginStep++
if (!basePageLoaded) {
binding.progressBar.visibility = View.GONE
binding.webview.visibility = View.VISIBLE
basePageLoaded = true
}
if (!TextUtils.isEmpty(username)) {
if (loginStep == 1) {
binding.webview.loadUrl(
"javascript: {document.getElementsByClassName('login')[0].click(); };"
)
} else if (!automatedLoginAttempted) {
automatedLoginAttempted = true
if (TextUtils.isEmpty(password)) {
binding.webview.loadUrl(
"javascript:var justStore = document.getElementById('user').value = '$username';"
)
} else {
binding.webview.loadUrl(
"javascript: {" +
"document.getElementById('user').value = '" + username + "';" +
"document.getElementById('password').value = '" + password + "';" +
"document.getElementById('submit').click(); };"
)
}
}
}
super.onPageFinished(view, url)
}
override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest) {
var alias: String? = null
if (!reauthorizeAccount) {
alias = appPreferences.temporaryClientCertAlias
}
val user = currentUserProvider.currentUser.blockingGet()
if (TextUtils.isEmpty(alias) && user != null) {
alias = user.clientCertificate
}
if (!TextUtils.isEmpty(alias)) {
val finalAlias = alias
Thread {
try {
val privateKey = KeyChain.getPrivateKey(applicationContext, finalAlias!!)
val certificates = KeyChain.getCertificateChain(
applicationContext,
finalAlias
)
if (privateKey != null && certificates != null) {
request.proceed(privateKey, certificates)
} else {
request.cancel()
}
} catch (e: KeyChainException) {
request.cancel()
} catch (e: InterruptedException) {
request.cancel()
}
}.start()
} else {
KeyChain.choosePrivateKeyAlias(
this@WebViewLoginActivity,
{ chosenAlias: String? ->
if (chosenAlias != null) {
appPreferences!!.temporaryClientCertAlias = chosenAlias
Thread {
var privateKey: PrivateKey? = null
try {
privateKey = KeyChain.getPrivateKey(applicationContext, chosenAlias)
val certificates = KeyChain.getCertificateChain(
applicationContext,
chosenAlias
)
if (privateKey != null && certificates != null) {
request.proceed(privateKey, certificates)
} else {
request.cancel()
}
} catch (e: KeyChainException) {
request.cancel()
} catch (e: InterruptedException) {
request.cancel()
}
}.start()
} else {
request.cancel()
}
},
arrayOf("RSA", "EC"),
null,
request.host,
request.port,
null
)
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
try {
val sslCertificate = error.certificate
val f: Field = sslCertificate.javaClass.getDeclaredField("mX509Certificate")
f.isAccessible = true
val cert = f[sslCertificate] as X509Certificate
if (cert == null) {
handler.cancel()
} else {
try {
trustManager.checkServerTrusted(arrayOf(cert), "generic")
handler.proceed()
} catch (exception: CertificateException) {
eventBus.post(CertificateEvent(cert, trustManager, handler))
}
}
} catch (exception: Exception) {
handler.cancel()
}
}
@Deprecated("Deprecated in super implementation")
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
super.onReceivedError(view, errorCode, description, failingUrl)
}
}
binding.webview.loadUrl("$baseUrl/index.php/login/flow", headers)
}
private fun dispose() {
if (userQueryDisposable != null && !userQueryDisposable!!.isDisposed) {
userQueryDisposable!!.dispose()
}
userQueryDisposable = null
}
private fun parseAndLoginFromWebView(dataString: String) {
val loginData = parseLoginData(assembledPrefix, dataString)
if (loginData != null) {
dispose()
cookieManager.cookieStore.removeAll()
if (userManager.checkIfUserIsScheduledForDeletion(loginData.username!!, baseUrl!!).blockingGet()) {
Log.e(TAG, "Tried to add already existing user who is scheduled for deletion.")
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
// however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it.
startAccountRemovalWorkerAndRestartApp()
} else if (userManager.checkIfUserExists(loginData.username!!, baseUrl!!).blockingGet()) {
if (reauthorizeAccount) {
updateUserAndRestartApp(loginData)
} else {
Log.w(TAG, "It was tried to add an account that account already exists. Skipped user creation.")
restartApp()
}
} else {
startAccountVerification(loginData)
}
}
}
private fun startAccountVerification(loginData: LoginData) {
val bundle = Bundle()
bundle.putString(KEY_USERNAME, loginData.username)
bundle.putString(KEY_TOKEN, loginData.token)
bundle.putString(KEY_BASE_URL, loginData.serverUrl)
var protocol = ""
if (baseUrl!!.startsWith("http://")) {
protocol = "http://"
} else if (baseUrl!!.startsWith("https://")) {
protocol = "https://"
}
if (!TextUtils.isEmpty(protocol)) {
bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol)
}
val intent = Intent(context, AccountVerificationActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
}
private fun restartApp() {
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
}
private fun updateUserAndRestartApp(loginData: LoginData) {
val currentUser = currentUserProvider.currentUser.blockingGet()
if (currentUser != null) {
currentUser.clientCertificate = appPreferences.temporaryClientCertAlias
currentUser.token = loginData.token
val rowsUpdated = userManager.updateOrCreateUser(currentUser).blockingGet()
Log.d(TAG, "User rows updated: $rowsUpdated")
restartApp()
}
}
private fun startAccountRemovalWorkerAndRestartApp() {
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
.observeForever { workInfo: WorkInfo ->
when (workInfo.state) {
WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> {
restartApp()
}
else -> {}
}
}
}
private fun parseLoginData(prefix: String?, dataString: String): LoginData? {
if (dataString.length < prefix!!.length) {
return null
}
val loginData = LoginData()
// format is xxx://login/server:xxx&user:xxx&password:xxx
val data: String = dataString.substring(prefix.length)
val values: Array<String> = data.split("&").toTypedArray()
if (values.size != PARAMETER_COUNT) {
return null
}
for (value in values) {
if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
loginData.username = URLDecoder.decode(
value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
)
} else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
loginData.token = URLDecoder.decode(
value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
)
} else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
loginData.serverUrl = URLDecoder.decode(
value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
)
} else {
return null
}
}
return if (!TextUtils.isEmpty(loginData.serverUrl) && !TextUtils.isEmpty(loginData.username) &&
!TextUtils.isEmpty(loginData.token)
) {
loginData
} else {
null
}
}
public override fun onDestroy() {
super.onDestroy()
dispose()
}
init {
sharedApplication!!.componentApplication.inject(this)
}
override val appBarLayoutType: AppBarLayoutType
get() = AppBarLayoutType.EMPTY
companion object {
private val TAG = WebViewLoginActivity::class.java.simpleName
private const val PROTOCOL_SUFFIX = "://"
private const val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"
private const val PARAMETER_COUNT = 3
}
}

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
@ -27,9 +29,9 @@ import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.talk.R
import com.nextcloud.talk.account.AccountVerificationActivity
import com.nextcloud.talk.account.BrowserLoginActivity
import com.nextcloud.talk.account.ServerSelectionActivity
import com.nextcloud.talk.account.SwitchAccountActivity
import com.nextcloud.talk.account.WebViewLoginActivity
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.events.CertificateEvent
@ -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() {
@ -218,7 +235,7 @@ open class BaseActivity : AppCompatActivity() {
val temporaryClassNames: MutableList<String> = ArrayList()
temporaryClassNames.add(ServerSelectionActivity::class.java.name)
temporaryClassNames.add(AccountVerificationActivity::class.java.name)
temporaryClassNames.add(WebViewLoginActivity::class.java.name)
temporaryClassNames.add(BrowserLoginActivity::class.java.name)
temporaryClassNames.add(SwitchAccountActivity::class.java.name)
if (!temporaryClassNames.contains(javaClass.name)) {
appPreferences.removeTemporaryClientCertAlias()

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

@ -10,7 +10,6 @@
package com.nextcloud.talk.activities
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.provider.ContactsContract
@ -24,8 +23,8 @@ import androidx.lifecycle.ProcessLifecycleOwner
import autodagger.AutoInjector
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.talk.R
import com.nextcloud.talk.account.BrowserLoginActivity
import com.nextcloud.talk.account.ServerSelectionActivity
import com.nextcloud.talk.account.WebViewLoginActivity
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity
@ -49,7 +48,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
@ -90,7 +92,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
}
fun lockScreenIfConditionsApply() {
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
val keyguardManager = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
if (keyguardManager.isKeyguardSecure && appPreferences.isScreenLocked) {
if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout)) {
val lockIntent = Intent(context, LockedActivity::class.java)
@ -101,7 +103,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
private fun launchServerSelection() {
if (isBrandingUrlSet()) {
val intent = Intent(context, WebViewLoginActivity::class.java)
val intent = Intent(context, BrowserLoginActivity::class.java)
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_BASE_URL, resources.getString(R.string.weblogin_url))
intent.putExtras(bundle)
@ -251,10 +253,6 @@ class MainActivity : BaseActivity(), ActionBarProvider {
startActivity(chatIntent)
}
} else {
if (!appPreferences.isDbRoomMigrated) {
appPreferences.isDbRoomMigrated = true
}
userManager.users.subscribe(object : SingleObserver<List<User>> {
override fun onSubscribe(d: Disposable) {
// unused atm

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

@ -59,6 +59,7 @@ class ConversationItem(
IFilterable<String?> {
private var header: GenericTextHeaderItem? = null
private val chatMessage = model.lastMessage?.asModel()
var mHolder: ConversationItemViewHolder? = null
constructor(
conversation: ConversationModel,
@ -83,17 +84,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(
@ -102,6 +98,7 @@ class ConversationItem(
position: Int,
payloads: List<Any>
) {
mHolder = holder
val appContext = sharedApplication!!.applicationContext
holder.binding.dialogName.setTextColor(
ResourcesCompat.getColor(
@ -219,8 +216,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 +240,6 @@ class ConversationItem(
else -> true
}
}
private fun setLastMessage(holder: ConversationItemViewHolder, appContext: Context) {
if (chatMessage != null) {
@ -277,8 +273,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 +297,6 @@ class ConversationItem(
lastMessageDisplayText
)
}
}
private fun showUnreadMessages(holder: ConversationItemViewHolder) {
holder.binding.dialogName.setTypeface(holder.binding.dialogName.typeface, Typeface.BOLD)
@ -343,17 +338,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 +423,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

@ -13,8 +13,7 @@ import android.content.Context
import android.view.View
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 +39,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 +74,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,20 +123,32 @@ 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_small
)
)
}
}
}
SOURCE_GROUPS -> {
holder.binding.avatarView.loadUserAvatar(
viewThemeUtils.talk.themePlaceholderAvatar(holder.binding.avatarView, R.drawable.ic_avatar_group)
viewThemeUtils.talk.themePlaceholderAvatar(
holder.binding.avatarView,
R.drawable
.ic_avatar_group_small
)
)
}
@ -168,19 +174,13 @@ class MentionAutocompleteItem(
}
SOURCE_TEAMS -> {
holder.binding.avatarView.post {
val imageViewWidth = holder.binding.avatarView.width
val imageViewHeight = holder.binding.avatarView.height
val request = ImageRequest.Builder(context)
.data(R.drawable.icon_team)
.size(imageViewWidth, imageViewHeight)
.scale(coil.size.Scale.FILL)
.target(holder.binding.avatarView)
.build()
Coil.imageLoader(context).enqueue(request)
}
holder.binding.avatarView.loadUserAvatar(
viewThemeUtils.talk.themePlaceholderAvatar(
holder.binding.avatarView,
R.drawable
.ic_avatar_team_small
)
)
}
else -> {
@ -234,8 +234,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 +245,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

@ -58,8 +58,6 @@ import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.google.GoogleEmojiProvider
import de.cotech.hw.SecurityKeyManager
import de.cotech.hw.SecurityKeyManagerConfig
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteDatabaseHook
import okhttp3.OkHttpClient
import org.conscrypt.Conscrypt
import org.webrtc.PeerConnectionFactory
@ -86,7 +84,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
@ -101,18 +101,6 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
lateinit var okHttpClient: OkHttpClient
//endregion
val hook: SQLiteDatabaseHook = object : SQLiteDatabaseHook {
override fun preKey(database: SQLiteDatabase) {
// unused atm
}
override fun postKey(database: SQLiteDatabase) {
Log.i("TalkApplication", "DB cipher_migrate START")
database.rawExecSQL("PRAGMA cipher_migrate;")
Log.i("TalkApplication", "DB cipher_migrate END")
}
}
//region private methods
private fun initializeWebRtc() {
try {

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
@ -393,7 +392,6 @@ class ChatActivity :
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val intent = Intent(this@ChatActivity, ConversationsListActivity::class.java)
intent.putExtras(Bundle())
startActivity(intent)
}
}
@ -461,7 +459,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 +508,7 @@ class ChatActivity :
initObservers()
pickMultipleMedia = registerForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(5)
ActivityResultContracts.PickMultipleVisualMedia(MAX_AMOUNT_MEDIA_FILE_PICKER)
) { uris ->
if (uris.isNotEmpty()) {
onChooseFileResult(uris)
@ -549,6 +568,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 +661,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 +706,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 +722,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 +740,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 +1130,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 +1267,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 +1994,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 +2078,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 +2333,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 +2348,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 +2361,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 +2828,7 @@ class ChatActivity :
}
if (this::spreedCapabilities.isInitialized) {
if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
if (hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
deleteExpiredMessages()
}
} else {
@ -3044,6 +3075,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 +3089,6 @@ class ChatActivity :
loadAvatarForStatusBar()
setActionBarTitle()
}
return true
}
@ -3065,7 +3096,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 +3117,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 +3176,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 +3628,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 +3942,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 +3952,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 +4217,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
@ -68,8 +70,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.account.BrowserLoginActivity
import com.nextcloud.talk.account.ServerSelectionActivity
import com.nextcloud.talk.account.WebViewLoginActivity
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.activities.CallActivity
import com.nextcloud.talk.activities.MainActivity
@ -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,24 @@ class ConversationsListActivity :
showSearchOrToolbar()
}
override fun onPause() {
super.onPause()
val firstVisible = layoutManager?.findFirstVisibleItemPosition() ?: 0
val firstItem = adapter?.getItem(firstVisible)
val firstTop = (firstItem as ConversationItem).mHolder?.itemView?.top
val firstOffset = firstTop?.minus(CONVERSATION_ITEM_HEIGHT) ?: 0
appPreferences.setConversationListPositionAndOffset(firstVisible, firstOffset)
}
// 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 +432,13 @@ class ConversationsListActivity :
conversationsListViewModel.getRoomsFlow
.onEach { list ->
setConversationList(list)
val noteToSelf = list
.firstOrNull { ConversationUtils.isNoteToSelfConversation(it) }
val isNoteToSelfAvailable = noteToSelf != null
handleNoteToSelfShortcut(isNoteToSelfAvailable, noteToSelf?.token ?: "")
val pair = appPreferences.conversationListPositionAndOffset
layoutManager?.scrollToPositionWithOffset(pair.first, pair.second)
}.collect()
}
@ -525,6 +555,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 +693,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 +808,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 +987,7 @@ class ConversationsListActivity :
} else {
showToolbar()
}
colorizeStatusBar()
colorizeNavigationBar()
initSystemBars()
}
}
@ -1217,7 +1269,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 +1450,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 +1497,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 +1513,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 +1859,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 ||
@ -1836,7 +1886,6 @@ class ConversationsListActivity :
val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, selectedConversation!!.token)
// bundle.putString(KEY_ROOM_ID, selectedConversation!!.roomId)
bundle.putString(KEY_SHARED_TEXT, textToPaste)
if (selectedMessageId != null) {
bundle.putString(BundleKeys.KEY_MESSAGE_ID, selectedMessageId)
@ -1914,7 +1963,7 @@ class ConversationsListActivity :
deleteUserAndRestartApp()
}
.setNegativeButton(R.string.nc_settings_reauthorize) { _, _ ->
val intent = Intent(context, WebViewLoginActivity::class.java)
val intent = Intent(context, BrowserLoginActivity::class.java)
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_BASE_URL, currentUser!!.baseUrl!!)
bundle.putBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT, true)
@ -1938,9 +1987,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 +2205,7 @@ class ConversationsListActivity :
binding.filterConversationsButton.let {
viewThemeUtils.platform.colorImageView(
it,
ColorRole.ON_SURFACE_VARIANT
ColorRole.ON_SURFACE
)
}
}
@ -2183,5 +2232,7 @@ 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"
private const val CONVERSATION_ITEM_HEIGHT = 44
}
}

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

@ -42,9 +42,8 @@ public class DatabaseModule {
@Provides
@Singleton
public TalkDatabase provideTalkDatabase(@NonNull final Context context,
@NonNull final AppPreferences appPreferences) {
return TalkDatabase.getInstance(context, appPreferences);
public TalkDatabase provideTalkDatabase(@NonNull final Context context) {
return TalkDatabase.getInstance(context);
}
@Provides

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
@ -9,7 +9,6 @@
package com.nextcloud.talk.data.source.local
import android.content.Context
import android.util.Log
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
@ -23,22 +22,21 @@ 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
import com.nextcloud.talk.data.storage.model.ArbitraryStorageEntity
import com.nextcloud.talk.data.user.UsersDao
import com.nextcloud.talk.data.user.model.UserEntity
import com.nextcloud.talk.utils.preferences.AppPreferences
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteDatabaseHook
import net.sqlcipher.database.SupportFactory
import net.zetetic.database.sqlcipher.SupportOpenHelperFactory
import java.util.Locale
@Database(
@ -49,9 +47,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,10 +62,10 @@ import java.util.Locale
SignalingSettingsConverter::class,
HashMapHashMapConverter::class,
LinkedHashMapConverter::class,
ArrayListConverter::class
ArrayListConverter::class,
SendStatusConverter::class
)
abstract class TalkDatabase : RoomDatabase() {
abstract fun usersDao(): UsersDao
abstract fun conversationsDao(): ConversationsDao
abstract fun chatMessagesDao(): ChatMessagesDao
@ -75,27 +74,21 @@ abstract class TalkDatabase : RoomDatabase() {
companion object {
const val TAG = "TalkDatabase"
const val SQL_CIPHER_LIBRARY = "sqlcipher"
@Volatile
private var instance: TalkDatabase? = null
@JvmStatic
fun getInstance(context: Context, appPreferences: AppPreferences): TalkDatabase =
fun getInstance(context: Context): TalkDatabase =
instance ?: synchronized(this) {
instance ?: build(context, appPreferences).also { instance = it }
instance ?: build(context).also { instance = it }
}
private fun build(context: Context, appPreferences: AppPreferences): TalkDatabase {
private fun build(context: Context): TalkDatabase {
val passCharArray = context.getString(R.string.nc_talk_database_encryption_key).toCharArray()
val passphrase: ByteArray = SQLiteDatabase.getBytes(passCharArray)
val factory = if (appPreferences.isDbRoomMigrated) {
Log.i(TAG, "No cipher migration needed")
SupportFactory(passphrase)
} else {
Log.i(TAG, "Add cipher migration hook")
SupportFactory(passphrase, getCipherMigrationHook())
}
val passphrase: ByteArray = getBytesFromChars(passCharArray)
val factory = SupportOpenHelperFactory(passphrase)
val dbName = context
.resources
@ -105,6 +98,8 @@ abstract class TalkDatabase : RoomDatabase() {
.trim() +
".sqlite"
System.loadLibrary(SQL_CIPHER_LIBRARY)
return Room
.databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName)
// comment out openHelperFactory to view the database entries in Android Studio for debugging
@ -122,7 +117,7 @@ abstract class TalkDatabase : RoomDatabase() {
)
.allowMainThreadQueries()
.addCallback(
object : RoomDatabase.Callback() {
object : Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
db.execSQL("PRAGMA defer_foreign_keys = 1")
@ -132,17 +127,6 @@ abstract class TalkDatabase : RoomDatabase() {
.build()
}
private fun getCipherMigrationHook(): SQLiteDatabaseHook =
object : SQLiteDatabaseHook {
override fun preKey(database: SQLiteDatabase) {
// unused atm
}
override fun postKey(database: SQLiteDatabase) {
Log.i(TAG, "DB cipher_migrate START")
database.rawExecSQL("PRAGMA cipher_migrate;")
Log.i(TAG, "DB cipher_migrate END")
}
}
private fun getBytesFromChars(chars: CharArray): ByteArray = String(chars).toByteArray(Charsets.UTF_8)
}
}

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? {

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