From c13f2589ffd0abef391136ef2c50ff091df7c2d9 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Fri, 2 Feb 2024 08:45:08 +0100 Subject: [PATCH] handle federation invitations Signed-off-by: Marcel Hibbe --- app/src/main/AndroidManifest.xml | 4 + .../talk/account/SwitchAccountActivity.kt | 6 +- .../nextcloud/talk/activities/MainActivity.kt | 8 +- .../talk/adapters/items/AdvancedUserItem.java | 154 -------------- .../talk/adapters/items/AdvancedUserItem.kt | 129 ++++++++++++ .../java/com/nextcloud/talk/api/NcApi.java | 13 ++ .../ConversationsListActivity.kt | 96 ++++++++- .../data/ConversationsListRepository.kt | 23 +++ .../data/ConversationsListRepositoryImpl.kt | 25 +++ .../viewmodels/ConversationsListViewModel.kt | 112 ++++++++++ .../talk/dagger/modules/RepositoryModule.kt | 14 ++ .../talk/dagger/modules/RestModule.java | 1 + .../talk/dagger/modules/ViewModelModule.kt | 12 ++ .../talk/invitation/InvitationsActivity.kt | 195 ++++++++++++++++++ .../invitation/adapters/InvitationsAdapter.kt | 104 ++++++++++ .../talk/invitation/data/Invitation.kt | 35 ++++ .../invitation/data/InvitationActionModel.kt | 27 +++ .../talk/invitation/data/InvitationsModel.kt | 28 +++ .../invitation/data/InvitationsRepository.kt | 30 +++ .../data/InvitationsRepositoryImpl.kt | 87 ++++++++ .../viewmodels/InvitationsViewModel.kt | 136 ++++++++++++ .../nextcloud/talk/jobs/NotificationWorker.kt | 28 ++- .../talk/models/json/invitation/Invitation.kt | 55 +++++ .../models/json/invitation/InvitationOCS.kt | 38 ++++ .../json/invitation/InvitationOverall.kt | 35 ++++ .../ListOpenConversationsActivity.kt | 1 + .../adapters/OpenConversationsAdapter.kt | 2 +- .../dialog/ChooseAccountDialogFragment.java | 62 +++++- .../ChooseAccountShareToDialogFragment.kt | 7 +- .../com/nextcloud/talk/utils/ApiUtils.java | 12 ++ .../nextcloud/talk/utils/bundle/BundleKeys.kt | 1 + .../drawable/baseline_notifications_24.xml | 26 +++ app/src/main/res/layout/account_item.xml | 13 ++ .../res/layout/activity_conversations.xml | 16 +- .../main/res/layout/activity_invitations.xml | 67 ++++++ .../res/layout/federated_invitation_hint.xml | 50 +++++ .../main/res/layout/rv_item_invitation.xml | 105 ++++++++++ app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/strings.xml | 16 +- scripts/analysis/lint-results.txt | 2 +- 41 files changed, 1590 insertions(+), 191 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/data/Invitation.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/data/InvitationActionModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/invitation/Invitation.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOCS.kt create mode 100644 app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOverall.kt create mode 100644 app/src/main/res/drawable/baseline_notifications_24.xml create mode 100644 app/src/main/res/layout/activity_invitations.xml create mode 100644 app/src/main/res/layout/federated_invitation_hint.xml create mode 100644 app/src/main/res/layout/rv_item_invitation.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ba4e7ddcb..78ee53876 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -261,6 +261,10 @@ android:name=".openconversations.ListOpenConversationsActivity" android:theme="@style/AppTheme" /> + + diff --git a/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt index c19c3223f..c00d23ab4 100644 --- a/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt @@ -84,7 +84,7 @@ class SwitchAccountActivity : BaseActivity() { if (userItems.size > position) { val user = (userItems[position] as AdvancedUserItem).user - if (userManager.setUserAsActive(user).blockingGet()) { + if (userManager.setUserAsActive(user!!).blockingGet()) { cookieManager.cookieStore.removeAll() finish() } @@ -146,7 +146,7 @@ class SwitchAccountActivity : BaseActivity() { participant.actorType = Participant.ActorType.USERS participant.actorId = userId participant.displayName = user.displayName - userItems.add(AdvancedUserItem(participant, user, null, viewThemeUtils)) + userItems.add(AdvancedUserItem(participant, user, null, viewThemeUtils, 0)) } } adapter!!.addListener(onSwitchItemClickListener) @@ -164,7 +164,7 @@ class SwitchAccountActivity : BaseActivity() { participant.displayName = importAccount.getUsername() user = User() user.baseUrl = importAccount.getBaseUrl() - userItems.add(AdvancedUserItem(participant, user, account, viewThemeUtils)) + userItems.add(AdvancedUserItem(participant, user, account, viewThemeUtils, 0)) } adapter!!.addListener(onImportItemClickListener) adapter!!.updateDataSet(userItems, false) diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index ce631df93..81d918448 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -49,6 +49,7 @@ import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.conversationlist.ConversationsListActivity import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivityMainBinding +import com.nextcloud.talk.invitation.InvitationsActivity import com.nextcloud.talk.lock.LockedActivity import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.users.UserManager @@ -258,7 +259,12 @@ class MainActivity : BaseActivity(), ActionBarProvider { } if (user != null && userManager.setUserAsActive(user).blockingGet()) { - if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { + if (intent.hasExtra(BundleKeys.KEY_REMOTE_TALK_SHARE)) { + if (intent.getBooleanExtra(BundleKeys.KEY_REMOTE_TALK_SHARE, false)) { + val intent = Intent(this, InvitationsActivity::class.java) + startActivity(intent) + } + } else if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { if (intent.getBooleanExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false)) { val callNotificationIntent = Intent(this, CallNotificationActivity::class.java) intent.extras?.let { callNotificationIntent.putExtras(it) } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java deleted file mode 100644 index 494918d39..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * @author Andy Scherzinger - * Copyright (C) 2022 Andy Scherzinger - * Copyright (C) 2017 Mario Danic - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.nextcloud.talk.adapters.items; - -import android.accounts.Account; -import android.net.Uri; -import android.text.TextUtils; -import android.view.View; - -import com.nextcloud.talk.R; -import com.nextcloud.talk.data.user.model.User; -import com.nextcloud.talk.databinding.AccountItemBinding; -import com.nextcloud.talk.extensions.ImageViewExtensionsKt; -import com.nextcloud.talk.models.json.participants.Participant; -import com.nextcloud.talk.ui.theme.ViewThemeUtils; - -import java.util.List; -import java.util.regex.Pattern; - -import androidx.annotation.Nullable; -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; -import eu.davidea.flexibleadapter.items.IFilterable; -import eu.davidea.viewholders.FlexibleViewHolder; - -public class AdvancedUserItem extends AbstractFlexibleItem implements - IFilterable { - - private final Participant participant; - private final User user; - @Nullable - private final Account account; - private final ViewThemeUtils viewThemeUtils; - - public AdvancedUserItem(Participant participant, - User user, - @Nullable Account account, - ViewThemeUtils viewThemeUtils) { - this.participant = participant; - this.user = user; - this.account = account; - this.viewThemeUtils = viewThemeUtils; - } - - @Override - public boolean equals(Object o) { - if (o instanceof AdvancedUserItem inItem) { - return participant.equals(inItem.getModel()); - } - return false; - } - - @Override - public int hashCode() { - return participant.hashCode(); - } - - /** - * @return the model object - */ - public Participant getModel() { - return participant; - } - - public User getUser() { - return user; - } - - @Nullable - public Account getAccount() { - return account; - } - - @Override - public int getLayoutRes() { - return R.layout.account_item; - } - - @Override - public UserItemViewHolder createViewHolder(View view, FlexibleAdapter adapter) { - return new UserItemViewHolder(view, adapter); - } - - @Override - public void bindViewHolder(FlexibleAdapter adapter, UserItemViewHolder holder, int position, List payloads) { - - if (adapter.hasFilter()) { - viewThemeUtils.talk.themeAndHighlightText( - holder.binding.userName, - participant.getDisplayName(), - String.valueOf(adapter.getFilter(String.class))); - } else { - holder.binding.userName.setText(participant.getDisplayName()); - } - - if (user != null && !TextUtils.isEmpty(user.getBaseUrl())) { - String host = Uri.parse(user.getBaseUrl()).getHost(); - if (!TextUtils.isEmpty(host)) { - holder.binding.account.setText(Uri.parse(user.getBaseUrl()).getHost()); - } else { - holder.binding.account.setText(user.getBaseUrl()); - } - } - - if (user != null && - user.getBaseUrl() != null && - (user.getBaseUrl().startsWith("http://") || user.getBaseUrl().startsWith("https://"))) { - ImageViewExtensionsKt.loadUserAvatar(holder.binding.userIcon, user, participant.getCalculatedActorId(), - true, false); - } - } - - @Override - public boolean filter(String constraint) { - return participant.getDisplayName() != null && - Pattern - .compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL) - .matcher(participant.getDisplayName().trim()) - .find(); - } - - static class UserItemViewHolder extends FlexibleViewHolder { - - public AccountItemBinding binding; - - /** - * Default constructor. - */ - UserItemViewHolder(View view, FlexibleAdapter adapter) { - super(view, adapter); - binding = AccountItemBinding.bind(view); - } - } -} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.kt new file mode 100644 index 000000000..5d70920af --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.kt @@ -0,0 +1,129 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2017 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.adapters.items + +import android.accounts.Account +import android.net.Uri +import android.text.TextUtils +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.items.AdvancedUserItem.UserItemViewHolder +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.AccountItemBinding +import com.nextcloud.talk.extensions.loadUserAvatar +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFilterable +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder +import java.util.regex.Pattern + +class AdvancedUserItem( + /** + * @return the model object + */ + val model: Participant, + @JvmField val user: User?, + val account: Account?, + private val viewThemeUtils: ViewThemeUtils, + private val actionRequiredCount: Int +) : AbstractFlexibleItem(), IFilterable { + override fun equals(o: Any?): Boolean { + return if (o is AdvancedUserItem) { + model == o.model + } else { + false + } + } + + override fun hashCode(): Int { + return model.hashCode() + } + + override fun getLayoutRes(): Int { + return R.layout.account_item + } + + override fun createViewHolder( + view: View?, + adapter: FlexibleAdapter>? + ): UserItemViewHolder { + return UserItemViewHolder(view, adapter) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: UserItemViewHolder, + position: Int, + payloads: MutableList + ) { + if (adapter.hasFilter()) { + viewThemeUtils.talk.themeAndHighlightText( + holder.binding.userName, + model.displayName, + adapter.getFilter(String::class.java).toString() + ) + } else { + holder.binding.userName.text = model.displayName + } + if (user != null && !TextUtils.isEmpty(user.baseUrl)) { + val host = Uri.parse(user.baseUrl).host + if (!TextUtils.isEmpty(host)) { + holder.binding.account.text = Uri.parse(user.baseUrl).host + } else { + holder.binding.account.text = user.baseUrl + } + } + if (user?.baseUrl != null && + (user.baseUrl!!.startsWith("http://") || user.baseUrl!!.startsWith("https://")) + ) { + holder.binding.userIcon.loadUserAvatar(user, model.calculatedActorId!!, true, false) + } + if (actionRequiredCount > 0) { + holder.binding.actionRequired.visibility = View.VISIBLE + } else { + holder.binding.actionRequired.visibility = View.GONE + } + } + + override fun filter(constraint: String?): Boolean { + return model.displayName != null && + Pattern + .compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(model.displayName!!.trim { it <= ' ' }) + .find() + } + + class UserItemViewHolder(view: View?, adapter: FlexibleAdapter<*>?) : FlexibleViewHolder(view, adapter) { + var binding: AccountItemBinding + + /** + * Default constructor. + */ + init { + binding = AccountItemBinding.bind(view!!) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 493067de2..cbc446eb1 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -33,6 +33,7 @@ import com.nextcloud.talk.models.json.conversations.RoomsOverall; import com.nextcloud.talk.models.json.generic.GenericOverall; import com.nextcloud.talk.models.json.generic.Status; import com.nextcloud.talk.models.json.hovercard.HoverCardOverall; +import com.nextcloud.talk.models.json.invitation.InvitationOverall; import com.nextcloud.talk.models.json.mention.MentionOverall; import com.nextcloud.talk.models.json.notifications.NotificationOverall; import com.nextcloud.talk.models.json.opengraph.OpenGraphOverall; @@ -706,4 +707,16 @@ public interface NcApi { Observable setRecordingConsent(@Header("Authorization") String authorization, @Url String url, @Field("recordingConsent") int recordingConsent); + + @GET + Observable getInvitations(@Header("Authorization") String authorization, + @Url String url); + + @POST + Observable acceptInvitation(@Header("Authorization") String authorization, + @Url String url); + + @DELETE + Observable rejectInvitation(@Header("Authorization") String authorization, + @Url String url); } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 38a81eebd..e2309f977 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -8,7 +8,7 @@ * @author Ezhil Shanmugham * Copyright (C) 2022 Álvaro Brey * Copyright (C) 2022 Andy Scherzinger (info@andy-scherzinger.de) - * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de) + * Copyright (C) 2022-2024 Marcel Hibbe (dev@mhibbe.de) * Copyright (C) 2017-2020 Mario Danic (mario@lovelyhq.com) * Copyright (C) 2023 Ezhil Shanmugham * @@ -53,10 +53,12 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.activity.OnBackPressedCallback +import androidx.annotation.OptIn import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.view.MenuItemCompat import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView import androidx.work.Data import androidx.work.OneTimeWorkRequest @@ -68,6 +70,9 @@ import coil.request.ImageRequest import coil.target.Target import coil.transform.CircleCropTransformation import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.badge.BadgeDrawable +import com.google.android.material.badge.BadgeUtils +import com.google.android.material.badge.ExperimentalBadgeUtils import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar @@ -88,10 +93,12 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.contacts.ContactsActivity +import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivityConversationsBinding import com.nextcloud.talk.events.ConversationsListFetchDataEvent import com.nextcloud.talk.events.EventStatus +import com.nextcloud.talk.invitation.InvitationsActivity import com.nextcloud.talk.jobs.AccountRemovalWorker import com.nextcloud.talk.jobs.ContactAddressBookWorker.Companion.run import com.nextcloud.talk.jobs.DeleteConversationWorker @@ -170,6 +177,11 @@ class ConversationsListActivity : @Inject lateinit var arbitraryStorageManager: ArbitraryStorageManager + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + lateinit var conversationsListViewModel: ConversationsListViewModel + override val appBarLayoutType: AppBarLayoutType get() = AppBarLayoutType.SEARCH_BAR @@ -206,6 +218,7 @@ class ConversationsListActivity : FilterConversationFragment.UNREAD to false ) val searchBehaviorSubject = BehaviorSubject.createDefault(false) + private lateinit var accountIconBadge: BadgeDrawable private val onBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { @@ -221,6 +234,8 @@ class ConversationsListActivity : super.onCreate(savedInstanceState) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + conversationsListViewModel = ViewModelProvider(this, viewModelFactory)[ConversationsListViewModel::class.java] + binding = ActivityConversationsBinding.inflate(layoutInflater) setupActionBar() setContentView(binding.root) @@ -230,6 +245,8 @@ class ConversationsListActivity : forwardMessage = intent.getBooleanExtra(KEY_FORWARD_MSG_FLAG, false) onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + + initObservers() } override fun onPostCreate(savedInstanceState: Bundle?) { @@ -279,6 +296,7 @@ class ConversationsListActivity : viewThemeUtils.material.colorMaterialTextButton(binding.switchAccountButton) searchBehaviorSubject.onNext(false) fetchRooms() + fetchPendingInvitations() } else { Log.e(TAG, "userManager.currentUser.blockingGet() returned null") Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() @@ -287,6 +305,48 @@ class ConversationsListActivity : showSearchOrToolbar() } + private fun initObservers() { + conversationsListViewModel.getFederationInvitationsViewState.observe(this) { state -> + when (state) { + is ConversationsListViewModel.GetFederationInvitationsStartState -> { + binding.conversationListHintInclude.conversationListHintLayout.visibility = View.GONE + } + + is ConversationsListViewModel.GetFederationInvitationsSuccessState -> { + if (state.showInvitationsHint) { + binding.conversationListHintInclude.conversationListHintLayout.visibility = View.VISIBLE + } else { + binding.conversationListHintInclude.conversationListHintLayout.visibility = View.GONE + } + } + + is ConversationsListViewModel.GetFederationInvitationsErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + + conversationsListViewModel.showBadgeViewState.observe(this) { state -> + when (state) { + is ConversationsListViewModel.ShowBadgeStartState -> { + showAccountIconBadge(false) + } + + is ConversationsListViewModel.ShowBadgeSuccessState -> { + showAccountIconBadge(state.showBadge) + } + + is ConversationsListViewModel.ShowBadgeErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + } + fun filterConversation() { val accountId = UserIdUtils.getIdForUser(userManager.currentUser.blockingGet()) filterState[FilterConversationFragment.UNREAD] = ( @@ -469,6 +529,22 @@ class ConversationsListActivity : return true } + @OptIn(ExperimentalBadgeUtils::class) + fun showAccountIconBadge(showBadge: Boolean) { + if (!::accountIconBadge.isInitialized) { + accountIconBadge = BadgeDrawable.create(binding.switchAccountButton.context) + accountIconBadge.verticalOffset = BADGE_OFFSET + accountIconBadge.horizontalOffset = BADGE_OFFSET + accountIconBadge.backgroundColor = resources.getColor(R.color.badge_color, null) + } + + if (showBadge) { + BadgeUtils.attachBadgeDrawable(accountIconBadge, binding.switchAccountButton) + } else { + BadgeUtils.detachBadgeDrawable(accountIconBadge, binding.switchAccountButton) + } + } + override fun onPrepareOptionsMenu(menu: Menu): Boolean { super.onPrepareOptionsMenu(menu) @@ -673,6 +749,18 @@ class ConversationsListActivity : } } + private fun fetchPendingInvitations() { + binding.conversationListHintInclude.conversationListHintLayout.setOnClickListener { + val intent = Intent(this, InvitationsActivity::class.java) + startActivity(intent) + } + + // TODO create mvvm, fetch pending invitations for all users and store in database for users, if current user + // has invitation -> show hint, if one or more other users have invitations -> show badge + + conversationsListViewModel.getFederationInvitations() + } + private fun initOverallLayout(isConversationListNotEmpty: Boolean) { if (isConversationListNotEmpty) { if (binding?.emptyLayout?.visibility != View.GONE) { @@ -857,7 +945,10 @@ class ConversationsListActivity : } false } - binding?.swipeRefreshLayoutView?.setOnRefreshListener { fetchRooms() } + binding?.swipeRefreshLayoutView?.setOnRefreshListener { + fetchRooms() + fetchPendingInvitations() + } binding?.swipeRefreshLayoutView?.let { viewThemeUtils.androidx.themeSwipeRefreshLayout(it) } binding?.emptyLayout?.setOnClickListener { showNewConversationsScreen() } binding?.floatingActionButton?.setOnClickListener { @@ -1716,5 +1807,6 @@ class ConversationsListActivity : const val HTTP_SERVICE_UNAVAILABLE = 503 const val MAINTENANCE_MODE_HEADER_KEY = "X-Nextcloud-Maintenance-Mode" const val REQUEST_POST_NOTIFICATIONS_PERMISSION = 111 + const val BADGE_OFFSET = 35 } } diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepository.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepository.kt new file mode 100644 index 000000000..3e1bb83d0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepository.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.conversationlist.data + +interface ConversationsListRepository diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepositoryImpl.kt new file mode 100644 index 000000000..7cf0fca08 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepositoryImpl.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.conversationlist.data + +import com.nextcloud.talk.api.NcApi + +class ConversationsListRepositoryImpl(private val ncApi: NcApi) : ConversationsListRepository diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt new file mode 100644 index 000000000..7d95f4754 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt @@ -0,0 +1,112 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.conversationlist.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.conversationlist.data.ConversationsListRepository +import com.nextcloud.talk.invitation.data.InvitationsModel +import com.nextcloud.talk.invitation.data.InvitationsRepository +import com.nextcloud.talk.users.UserManager +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class ConversationsListViewModel @Inject constructor( + private val conversationsListRepository: ConversationsListRepository +) : + ViewModel() { + + @Inject + lateinit var invitationsRepository: InvitationsRepository + + @Inject + lateinit var userManager: UserManager + + sealed interface ViewState + + object GetFederationInvitationsStartState : ViewState + object GetFederationInvitationsErrorState : ViewState + + open class GetFederationInvitationsSuccessState(val showInvitationsHint: Boolean) : ViewState + + private val _getFederationInvitationsViewState: MutableLiveData = + MutableLiveData(GetFederationInvitationsStartState) + val getFederationInvitationsViewState: LiveData + get() = _getFederationInvitationsViewState + + object ShowBadgeStartState : ViewState + object ShowBadgeErrorState : ViewState + open class ShowBadgeSuccessState(val showBadge: Boolean) : ViewState + + private val _showBadgeViewState: MutableLiveData = MutableLiveData(ShowBadgeStartState) + val showBadgeViewState: LiveData + get() = _showBadgeViewState + + fun getFederationInvitations() { + _getFederationInvitationsViewState.value = GetFederationInvitationsStartState + _showBadgeViewState.value = ShowBadgeStartState + + userManager.users.blockingGet()?.forEach { + invitationsRepository.fetchInvitations(it) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(FederatedInvitationsObserver()) + } + } + + inner class FederatedInvitationsObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(invitationsModel: InvitationsModel) { + if (invitationsModel.user.userId?.equals(userManager.currentUser.blockingGet().userId) == true) { + if (invitationsModel.invitations.isNotEmpty()) { + _getFederationInvitationsViewState.value = GetFederationInvitationsSuccessState(true) + } else { + _getFederationInvitationsViewState.value = GetFederationInvitationsSuccessState(false) + } + } else { + if (invitationsModel.invitations.isNotEmpty()) { + _showBadgeViewState.value = ShowBadgeSuccessState(true) + } + } + } + + override fun onError(e: Throwable) { + _getFederationInvitationsViewState.value = GetFederationInvitationsErrorState + Log.e(TAG, "Failed to fetch pending invitations", e) + } + + override fun onComplete() { + // unused atm + } + } + + companion object { + private val TAG = ConversationsListViewModel::class.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index 19694599a..d1e891216 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -32,11 +32,15 @@ import com.nextcloud.talk.conversation.repository.ConversationRepository import com.nextcloud.talk.conversation.repository.ConversationRepositoryImpl import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepository import com.nextcloud.talk.conversationinfoedit.data.ConversationInfoEditRepositoryImpl +import com.nextcloud.talk.conversationlist.data.ConversationsListRepository +import com.nextcloud.talk.conversationlist.data.ConversationsListRepositoryImpl import com.nextcloud.talk.data.source.local.TalkDatabase import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository import com.nextcloud.talk.data.storage.ArbitraryStoragesRepositoryImpl import com.nextcloud.talk.data.user.UsersRepository import com.nextcloud.talk.data.user.UsersRepositoryImpl +import com.nextcloud.talk.invitation.data.InvitationsRepository +import com.nextcloud.talk.invitation.data.InvitationsRepositoryImpl import com.nextcloud.talk.openconversations.data.OpenConversationsRepository import com.nextcloud.talk.openconversations.data.OpenConversationsRepositoryImpl import com.nextcloud.talk.polls.repositories.PollRepository @@ -135,6 +139,11 @@ class RepositoryModule { return TranslateRepositoryImpl(ncApi) } + @Provides + fun provideConversationsListRepository(ncApi: NcApi): ConversationsListRepository { + return ConversationsListRepositoryImpl(ncApi) + } + @Provides fun provideChatRepository(ncApi: NcApi): ChatRepository { return ChatRepositoryImpl(ncApi) @@ -152,4 +161,9 @@ class RepositoryModule { fun provideConversationRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): ConversationRepository { return ConversationRepositoryImpl(ncApi, userProvider) } + + @Provides + fun provideInvitationsRepository(ncApi: NcApi): InvitationsRepository { + return InvitationsRepositoryImpl(ncApi) + } } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java index d1a241fd9..6da4de8c3 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java @@ -250,6 +250,7 @@ public class RestModule { .header("User-Agent", ApiUtils.getUserAgent()) .header("Accept", "application/json") .header("OCS-APIRequest", "true") + .header("ngrok-skip-browser-warning", "true") .method(original.method(), original.body()) .build(); diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt index 596fb30d2..e9ddd400b 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -28,6 +28,8 @@ import com.nextcloud.talk.chat.viewmodels.ChatViewModel import com.nextcloud.talk.conversation.viewmodel.ConversationViewModel import com.nextcloud.talk.conversation.viewmodel.RenameConversationViewModel import com.nextcloud.talk.conversationinfoedit.viewmodel.ConversationInfoEditViewModel +import com.nextcloud.talk.conversationlist.viewmodels.ConversationsListViewModel +import com.nextcloud.talk.invitation.viewmodels.InvitationsViewModel import com.nextcloud.talk.messagesearch.MessageSearchViewModel import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel @@ -120,6 +122,11 @@ abstract class ViewModelModule { @ViewModelKey(OpenConversationsViewModel::class) abstract fun openConversationsViewModel(viewModel: OpenConversationsViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(ConversationsListViewModel::class) + abstract fun conversationsListViewModel(viewModel: ConversationsListViewModel): ViewModel + @Binds @IntoMap @ViewModelKey(ChatViewModel::class) @@ -144,4 +151,9 @@ abstract class ViewModelModule { @IntoMap @ViewModelKey(ConversationViewModel::class) abstract fun conversationViewModel(viewModel: ConversationViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(InvitationsViewModel::class) + abstract fun invitationsViewModel(viewModel: InvitationsViewModel): ViewModel } diff --git a/app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt b/app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt new file mode 100644 index 000000000..71f1519e3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt @@ -0,0 +1,195 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.invitation + +import android.content.Intent +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.lifecycle.ViewModelProvider +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.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.conversationlist.ConversationsListActivity +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.ActivityInvitationsBinding +import com.nextcloud.talk.invitation.adapters.InvitationsAdapter +import com.nextcloud.talk.invitation.data.ActionEnum +import com.nextcloud.talk.invitation.data.Invitation +import com.nextcloud.talk.invitation.viewmodels.InvitationsViewModel +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class InvitationsActivity : BaseActivity() { + + private lateinit var binding: ActivityInvitationsBinding + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var userProvider: CurrentUserProviderNew + + lateinit var invitationsViewModel: InvitationsViewModel + + lateinit var adapter: InvitationsAdapter + + private lateinit var currentUser: User + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val intent = Intent(this@InvitationsActivity, ConversationsListActivity::class.java) + startActivity(intent) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + invitationsViewModel = ViewModelProvider(this, viewModelFactory)[InvitationsViewModel::class.java] + + currentUser = userProvider.currentUser.blockingGet() + invitationsViewModel.fetchInvitations(currentUser) + + binding = ActivityInvitationsBinding.inflate(layoutInflater) + setupActionBar() + setContentView(binding.root) + setupSystemColors() + + adapter = InvitationsAdapter(currentUser) { invitation, action -> + handleInvitation(invitation, action) + } + + binding.invitationsRecyclerView.adapter = adapter + + initObservers() + + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + enum class InvitationAction { + ACCEPT, + REJECT + } + + private fun handleInvitation(invitation: Invitation, action: InvitationAction) { + when (action) { + InvitationAction.ACCEPT -> { + invitationsViewModel.acceptInvitation(currentUser, invitation) + } + + InvitationAction.REJECT -> { + invitationsViewModel.rejectInvitation(currentUser, invitation) + } + } + } + + private fun initObservers() { + invitationsViewModel.fetchInvitationsViewState.observe(this) { state -> + when (state) { + is InvitationsViewModel.FetchInvitationsStartState -> { + binding.invitationsRecyclerView.visibility = View.GONE + binding.progressBarWrapper.visibility = View.VISIBLE + } + + is InvitationsViewModel.FetchInvitationsSuccessState -> { + binding.invitationsRecyclerView.visibility = View.VISIBLE + binding.progressBarWrapper.visibility = View.GONE + adapter.submitList(state.invitations) + } + + is InvitationsViewModel.FetchInvitationsEmptyState -> { + binding.invitationsRecyclerView.visibility = View.GONE + binding.progressBarWrapper.visibility = View.GONE + + binding.emptyList.emptyListView.visibility = View.VISIBLE + binding.emptyList.emptyListViewHeadline.text = getString(R.string.nc_federation_no_invitations) + binding.emptyList.emptyListIcon.setImageResource(R.drawable.baseline_info_24) + binding.emptyList.emptyListIcon.visibility = View.VISIBLE + binding.emptyList.emptyListViewText.visibility = View.VISIBLE + } + + is InvitationsViewModel.FetchInvitationsErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + + invitationsViewModel.invitationActionViewState.observe(this) { state -> + when (state) { + is InvitationsViewModel.InvitationActionStartState -> { + } + + is InvitationsViewModel.InvitationActionSuccessState -> { + if (state.action == ActionEnum.ACCEPT) { + // val bundle = Bundle() + // bundle.putString(BundleKeys.KEY_ROOM_TOKEN, ????) // ??? + // + // val chatIntent = Intent(context, ChatActivity::class.java) + // chatIntent.putExtras(bundle) + // chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + // startActivity(chatIntent) + + val intent = Intent(this, ConversationsListActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } else { + // adapter.currentList.remove(state.invitation) + // adapter.notifyDataSetChanged() // leads to UnsupportedOperationException ?! + + // Just reload activity as lazy workaround to not deal with adapter for now. + // Might be fine until switching to jetpack compose. + finish() + startActivity(intent) + } + } + + is InvitationsViewModel.InvitationActionErrorState -> { + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + } + + else -> {} + } + } + } + + private fun setupActionBar() { + setSupportActionBar(binding.invitationsToolbar) + binding.invitationsToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setIcon(ColorDrawable(resources!!.getColor(R.color.transparent, null))) + viewThemeUtils.material.themeToolbar(binding.invitationsToolbar) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt b/app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt new file mode 100644 index 000000000..6631e8ecb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt @@ -0,0 +1,104 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.invitation.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.RvItemInvitationBinding +import com.nextcloud.talk.invitation.InvitationsActivity +import com.nextcloud.talk.invitation.data.Invitation +import com.nextcloud.talk.ui.theme.ViewThemeUtils +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class InvitationsAdapter( + val user: User, + private val handleInvitation: (Invitation, InvitationsActivity.InvitationAction) -> Unit +) : ListAdapter(InvitationsCallback) { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + inner class InvitationsViewHolder(private val itemBinding: RvItemInvitationBinding) : + RecyclerView.ViewHolder(itemBinding.root) { + + private var currentInvitation: Invitation? = null + + fun bindItem(invitation: Invitation) { + currentInvitation = invitation + + itemBinding.title.text = invitation.roomName + itemBinding.subject.text = String.format( + itemBinding.root.context.resources.getString(R.string.nc_federation_invited_to_room), + invitation.inviterDisplayName, + invitation.remoteServerUrl + ) + + itemBinding.acceptInvitation.setOnClickListener { + currentInvitation?.let { + handleInvitation(it, InvitationsActivity.InvitationAction.ACCEPT) + } + } + + itemBinding.rejectInvitation.setOnClickListener { + currentInvitation?.let { + handleInvitation(it, InvitationsActivity.InvitationAction.REJECT) + } + } + + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(itemBinding.rejectInvitation) + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(itemBinding.acceptInvitation) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InvitationsViewHolder { + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + return InvitationsViewHolder( + RvItemInvitationBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: InvitationsViewHolder, position: Int) { + val invitation = getItem(position) + holder.bindItem(invitation) + } +} + +object InvitationsCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Invitation, newItem: Invitation): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: Invitation, newItem: Invitation): Boolean { + return oldItem.id == newItem.id + } +} diff --git a/app/src/main/java/com/nextcloud/talk/invitation/data/Invitation.kt b/app/src/main/java/com/nextcloud/talk/invitation/data/Invitation.kt new file mode 100644 index 000000000..6a4150112 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/data/Invitation.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.invitation.data + +data class Invitation( + var id: Int, + var userId: String, + var state: Int, + var localRoomId: Int, + var accessToken: String?, + var remoteServerUrl: String, + var remoteToken: String, + var remoteAttendeeId: Int, + var inviterCloudId: String, + var inviterDisplayName: String, + var roomName: String +) diff --git a/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationActionModel.kt b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationActionModel.kt new file mode 100644 index 000000000..dbe2c1162 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationActionModel.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.invitation.data +enum class ActionEnum { ACCEPT, REJECT } +data class InvitationActionModel( + var action: ActionEnum, + var statusCode: Int, + var invitation: Invitation +) diff --git a/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsModel.kt b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsModel.kt new file mode 100644 index 000000000..c39a95140 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsModel.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.invitation.data + +import com.nextcloud.talk.data.user.model.User + +data class InvitationsModel( + var user: User, + var invitations: List +) diff --git a/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepository.kt b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepository.kt new file mode 100644 index 000000000..2343ce69c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepository.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.invitation.data + +import com.nextcloud.talk.data.user.model.User +import io.reactivex.Observable + +interface InvitationsRepository { + fun fetchInvitations(user: User): Observable + fun acceptInvitation(user: User, invitation: Invitation): Observable + fun rejectInvitation(user: User, invitation: Invitation): Observable +} diff --git a/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepositoryImpl.kt new file mode 100644 index 000000000..6429db880 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepositoryImpl.kt @@ -0,0 +1,87 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.invitation.data + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.utils.ApiUtils +import io.reactivex.Observable + +class InvitationsRepositoryImpl(private val ncApi: NcApi) : + InvitationsRepository { + + override fun fetchInvitations(user: User): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token) + + return ncApi.getInvitations( + credentials, + ApiUtils.getUrlForInvitation(user.baseUrl) + ).map { mapToInvitationsModel(user, it.ocs?.data!!) } + } + + override fun acceptInvitation(user: User, invitation: Invitation): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token) + + return ncApi.acceptInvitation( + credentials, + ApiUtils.getUrlForInvitationAccept(user.baseUrl, invitation.id) + ).map { InvitationActionModel(ActionEnum.ACCEPT, it.ocs?.meta?.statusCode!!, invitation) } + } + + override fun rejectInvitation(user: User, invitation: Invitation): Observable { + val credentials: String = ApiUtils.getCredentials(user.username, user.token) + + return ncApi.rejectInvitation( + credentials, + ApiUtils.getUrlForInvitationReject(user.baseUrl, invitation.id) + ).map { InvitationActionModel(ActionEnum.REJECT, it.ocs?.meta?.statusCode!!, invitation) } + } + + private fun mapToInvitationsModel( + user: User, + invitations: List + ): InvitationsModel { + val filteredInvitations = invitations.filter { it.state == OPEN_PENDING_INVITATION } + + return InvitationsModel( + user, + filteredInvitations.map { invitation -> + Invitation( + invitation.id, + invitation.userId!!, + invitation.state, + invitation.localRoomId, + invitation.accessToken!!, + invitation.remoteServerUrl!!, + invitation.remoteToken!!, + invitation.remoteAttendeeId, + invitation.inviterCloudId!!, + invitation.inviterDisplayName!!, + invitation.roomName!! + ) + } + ) + } + + companion object { + private const val OPEN_PENDING_INVITATION = 0 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt b/app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt new file mode 100644 index 000000000..6b36a98aa --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt @@ -0,0 +1,136 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2023 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.invitation.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.invitation.data.ActionEnum +import com.nextcloud.talk.invitation.data.Invitation +import com.nextcloud.talk.invitation.data.InvitationActionModel +import com.nextcloud.talk.invitation.data.InvitationsModel +import com.nextcloud.talk.invitation.data.InvitationsRepository +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class InvitationsViewModel @Inject constructor(private val repository: InvitationsRepository) : + ViewModel() { + + sealed interface ViewState + + object FetchInvitationsStartState : ViewState + object FetchInvitationsEmptyState : ViewState + object FetchInvitationsErrorState : ViewState + open class FetchInvitationsSuccessState(val invitations: List) : ViewState + + private val _fetchInvitationsViewState: MutableLiveData = MutableLiveData(FetchInvitationsStartState) + val fetchInvitationsViewState: LiveData + get() = _fetchInvitationsViewState + + object InvitationActionStartState : ViewState + object InvitationActionErrorState : ViewState + + private val _invitationActionViewState: MutableLiveData = MutableLiveData(InvitationActionStartState) + + open class InvitationActionSuccessState(val action: ActionEnum, val invitation: Invitation) : ViewState + + val invitationActionViewState: LiveData + get() = _invitationActionViewState + + fun fetchInvitations(user: User) { + _fetchInvitationsViewState.value = FetchInvitationsStartState + repository.fetchInvitations(user) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(FetchInvitationsObserver()) + } + + fun acceptInvitation(user: User, invitation: Invitation) { + repository.acceptInvitation(user, invitation) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(InvitationActionObserver()) + } + + fun rejectInvitation(user: User, invitation: Invitation) { + repository.rejectInvitation(user, invitation) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(InvitationActionObserver()) + } + + inner class FetchInvitationsObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(model: InvitationsModel) { + if (model.invitations.isEmpty()) { + _fetchInvitationsViewState.value = FetchInvitationsEmptyState + } else { + _fetchInvitationsViewState.value = FetchInvitationsSuccessState(model.invitations) + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when fetching invitations") + _fetchInvitationsViewState.value = FetchInvitationsErrorState + } + + override fun onComplete() { + // unused atm + } + } + + inner class InvitationActionObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(model: InvitationActionModel) { + if (model.statusCode == HTTP_OK) { + _invitationActionViewState.value = InvitationActionSuccessState(model.action, model.invitation) + } else { + _invitationActionViewState.value = InvitationActionErrorState + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error when handling invitation") + _invitationActionViewState.value = InvitationActionErrorState + } + + override fun onComplete() { + // unused atm + } + } + + companion object { + private val TAG = InvitationsViewModel::class.simpleName + private const val OPEN_PENDING_INVITATION = "0" + private const val HTTP_OK = 200 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index cb4c3871f..6a31a389b 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -94,6 +94,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MESSAGE_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_RESTRICT_DELETION import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_TIMESTAMP +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_REMOTE_TALK_SHARE import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARE_RECORDING_TO_CHAT_URL import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID @@ -175,6 +176,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor Log.d(TAG, "pushMessage.type: " + pushMessage.type) when (pushMessage.type) { TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING, TYPE_REMINDER -> handleNonCallPushMessage() + TYPE_REMOTE_TALK_SHARE -> handleRemoteTalkSharePushMessage() TYPE_CALL -> handleCallPushMessage() else -> Log.e(TAG, "unknown pushMessage.type") } @@ -194,6 +196,21 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor } } + private fun handleRemoteTalkSharePushMessage() { + val mainActivityIntent = Intent(context, MainActivity::class.java) + mainActivityIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + val bundle = Bundle() + bundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!) + bundle.putBoolean(KEY_REMOTE_TALK_SHARE, true) + mainActivityIntent.putExtras(bundle) + + if (pushMessage.notificationId != Long.MIN_VALUE) { + getNcDataAndShowNotification(mainActivityIntent) + } else { + showNotification(mainActivityIntent, null) + } + } + private fun handleCallPushMessage() { val fullScreenIntent = Intent(context, CallNotificationActivity::class.java) val bundle = Bundle() @@ -402,7 +419,10 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor ) { var category = "" when (pushMessage.type) { - TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING, TYPE_REMINDER -> category = Notification.CATEGORY_MESSAGE + TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING, TYPE_REMINDER, TYPE_REMOTE_TALK_SHARE -> { + category = Notification.CATEGORY_MESSAGE + } + TYPE_CALL -> category = Notification.CATEGORY_CALL else -> Log.e(TAG, "unknown pushMessage.type") } @@ -459,7 +479,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { when (pushMessage.type) { - TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING, TYPE_REMINDER -> { + TYPE_CHAT, TYPE_ROOM, TYPE_RECORDING, TYPE_REMINDER, TYPE_REMOTE_TALK_SHARE -> { notificationBuilder.setChannelId( NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name ) @@ -510,12 +530,15 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!! } + "group" -> largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!! + "public" -> largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_link_black_24px)?.toBitmap()!! + else -> // assuming one2one largeIcon = if (TYPE_CHAT == pushMessage.type || TYPE_ROOM == pushMessage.type) { ContextCompat.getDrawable(context!!, R.drawable.ic_comment)?.toBitmap()!! @@ -987,6 +1010,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor private const val TYPE_ROOM = "room" private const val TYPE_CALL = "call" private const val TYPE_RECORDING = "recording" + private const val TYPE_REMOTE_TALK_SHARE = "remote_talk_share" private const val TYPE_REMINDER = "reminder" private const val SPREED_APP = "spreed" private const val TIMER_START = 1 diff --git a/app/src/main/java/com/nextcloud/talk/models/json/invitation/Invitation.kt b/app/src/main/java/com/nextcloud/talk/models/json/invitation/Invitation.kt new file mode 100644 index 000000000..56fbc2373 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/invitation/Invitation.kt @@ -0,0 +1,55 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2024 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.models.json.invitation + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class Invitation( + @JsonField(name = ["id"]) + var id: Int = 0, + @JsonField(name = ["userId"]) + var userId: String? = null, + @JsonField(name = ["state"]) + var state: Int = 0, + @JsonField(name = ["localRoomId"]) + var localRoomId: Int = 0, + @JsonField(name = ["accessToken"]) + var accessToken: String? = null, + @JsonField(name = ["remoteServerUrl"]) + var remoteServerUrl: String? = null, + @JsonField(name = ["remoteToken"]) + var remoteToken: String? = null, + @JsonField(name = ["remoteAttendeeId"]) + var remoteAttendeeId: Int = 0, + @JsonField(name = ["inviterCloudId"]) + var inviterCloudId: String? = null, + @JsonField(name = ["inviterDisplayName"]) + var inviterDisplayName: String? = null, + @JsonField(name = ["roomName"]) + var roomName: String? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(0, null, 0, 0, null, null, null, 0, null, null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOCS.kt new file mode 100644 index 000000000..1bc86ea02 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOCS.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2024 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.models.json.invitation + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import com.nextcloud.talk.models.json.generic.GenericMeta +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class InvitationOCS( + @JsonField(name = ["meta"]) + var meta: GenericMeta?, + @JsonField(name = ["data"]) + var data: List? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOverall.kt new file mode 100644 index 000000000..9c133c064 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOverall.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2024 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.models.json.invitation + +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonObject +data class InvitationOverall( + @JsonField(name = ["ocs"]) + var ocs: InvitationOCS? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt index aa1b19c97..51b4c1a7a 100644 --- a/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt @@ -33,6 +33,7 @@ import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.databinding.ActivityOpenConversationsBinding +import com.nextcloud.talk.openconversations.adapters.OpenConversationsAdapter import com.nextcloud.talk.openconversations.data.OpenConversation import com.nextcloud.talk.openconversations.viewmodels.OpenConversationsViewModel import com.nextcloud.talk.utils.bundle.BundleKeys diff --git a/app/src/main/java/com/nextcloud/talk/openconversations/adapters/OpenConversationsAdapter.kt b/app/src/main/java/com/nextcloud/talk/openconversations/adapters/OpenConversationsAdapter.kt index 79087c68b..96f608377 100644 --- a/app/src/main/java/com/nextcloud/talk/openconversations/adapters/OpenConversationsAdapter.kt +++ b/app/src/main/java/com/nextcloud/talk/openconversations/adapters/OpenConversationsAdapter.kt @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.openconversations +package com.nextcloud.talk.openconversations.adapters import android.view.LayoutInflater import android.view.ViewGroup diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java index 4dfb482bb..261df9edc 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java @@ -43,6 +43,8 @@ import com.nextcloud.talk.conversationlist.ConversationsListActivity; import com.nextcloud.talk.data.user.model.User; import com.nextcloud.talk.databinding.DialogChooseAccountBinding; import com.nextcloud.talk.extensions.ImageViewExtensionsKt; +import com.nextcloud.talk.invitation.data.InvitationsModel; +import com.nextcloud.talk.invitation.data.InvitationsRepository; import com.nextcloud.talk.models.json.participants.Participant; import com.nextcloud.talk.models.json.status.Status; import com.nextcloud.talk.models.json.status.StatusOverall; @@ -79,6 +81,8 @@ public class ChooseAccountDialogFragment extends DialogFragment { private static final float STATUS_SIZE_IN_DP = 9f; + Disposable disposable; + @Inject UserManager userManager; @@ -91,6 +95,9 @@ public class ChooseAccountDialogFragment extends DialogFragment { @Inject ViewThemeUtils viewThemeUtils; + @Inject + InvitationsRepository invitationsRepository; + private DialogChooseAccountBinding binding; private View dialogView; @@ -150,7 +157,6 @@ public class ChooseAccountDialogFragment extends DialogFragment { adapter = new FlexibleAdapter<>(userItems, getActivity(), false); User userEntity; - Participant participant; for (User userItem : userManager.getUsers().blockingGet()) { userEntity = userItem; @@ -167,17 +173,48 @@ public class ChooseAccountDialogFragment extends DialogFragment { userId = userEntity.getUsername(); } - participant = new Participant(); - participant.setActorType(Participant.ActorType.USERS); - participant.setActorId(userId); - participant.setDisplayName(userEntity.getDisplayName()); - userItems.add(new AdvancedUserItem(participant, userEntity, null, viewThemeUtils)); + User finalUserEntity = userEntity; + invitationsRepository.fetchInvitations(userItem) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer<>() { + @Override + public void onSubscribe(Disposable d) { + disposable = d; + } + + @Override + public void onNext(InvitationsModel invitationsModel) { + Participant participant; + participant = new Participant(); + participant.setActorType(Participant.ActorType.USERS); + participant.setActorId(userId); + participant.setDisplayName(finalUserEntity.getDisplayName()); + userItems.add( + new AdvancedUserItem( + participant, + finalUserEntity, + null, + viewThemeUtils, + invitationsModel.getInvitations().size() + )); + adapter.addListener(onSwitchItemClickListener); + adapter.addListener(onSwitchItemLongClickListener); + adapter.updateDataSet(userItems, false); + } + + @Override + public void onError(@io.reactivex.annotations.NonNull Throwable e) { + Log.e(TAG, "Failed to fetch invitations", e); + } + + @Override + public void onComplete() { + // no actions atm + } + }); } } - - adapter.addListener(onSwitchItemClickListener); - adapter.addListener(onSwitchItemLongClickListener); - adapter.updateDataSet(userItems, false); } } @@ -291,6 +328,9 @@ public class ChooseAccountDialogFragment extends DialogFragment { @Override public void onDestroyView() { super.onDestroyView(); + if (disposable != null && !disposable.isDisposed()) { + disposable.dispose(); + } binding = null; } @@ -299,7 +339,7 @@ public class ChooseAccountDialogFragment extends DialogFragment { @Override public boolean onItemClick(View view, int position) { if (userItems.size() > position) { - User user = (userItems.get(position)).getUser(); + User user = (userItems.get(position)).user; if (userManager.setUserAsActive(user).blockingGet()) { cookieManager.getCookieStore().removeAll(); diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt index 3fe01b131..7e024a1a9 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt @@ -59,9 +59,8 @@ class ChooseAccountShareToDialogFragment : DialogFragment() { @Inject var cookieManager: CookieManager? = null - @JvmField @Inject - var viewThemeUtils: ViewThemeUtils? = null + lateinit var viewThemeUtils: ViewThemeUtils private var binding: DialogChooseAccountShareToBinding? = null private var dialogView: View? = null private var adapter: FlexibleAdapter? = null @@ -121,7 +120,7 @@ class ChooseAccountShareToDialogFragment : DialogFragment() { participant.actorType = Participant.ActorType.USERS participant.actorId = userId participant.displayName = userEntity.displayName - userItems.add(AdvancedUserItem(participant, userEntity, null, viewThemeUtils)) + userItems.add(AdvancedUserItem(participant, userEntity, null, viewThemeUtils, 0)) } } adapter!!.addListener(onSwitchItemClickListener) @@ -158,7 +157,7 @@ class ChooseAccountShareToDialogFragment : DialogFragment() { private val onSwitchItemClickListener = FlexibleAdapter.OnItemClickListener { view, position -> if (userItems.size > position) { val user = userItems[position].user - if (userManager!!.setUserAsActive(user).blockingGet()) { + if (userManager!!.setUserAsActive(user!!).blockingGet()) { cookieManager!!.cookieStore.removeAll() activity?.recreate() dismiss() diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 298ced754..c36d882f1 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -544,4 +544,16 @@ public class ApiUtils { public static String getUrlForRecordingConsent(int version, String baseUrl, String token) { return getUrlForRoom(version, baseUrl, token) + "/recording-consent"; } + + public static String getUrlForInvitation(String baseUrl) { + return baseUrl + ocsApiVersion + spreedApiVersion + "/federation/invitation"; + } + + public static String getUrlForInvitationAccept(String baseUrl, int id) { + return getUrlForInvitation(baseUrl) + "/" + id; + } + + public static String getUrlForInvitationReject(String baseUrl, int id) { + return getUrlForInvitation(baseUrl) + "/" + id; + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index 4e2331e31..a8cb1e405 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -88,4 +88,5 @@ object BundleKeys { const val SAVED_TRANSLATED_MESSAGE = "SAVED_TRANSLATED_MESSAGE" const val KEY_REAUTHORIZE_ACCOUNT = "KEY_REAUTHORIZE_ACCOUNT" const val KEY_PASSWORD = "KEY_PASSWORD" + const val KEY_REMOTE_TALK_SHARE = "KEY_REMOTE_TALK_SHARE" } diff --git a/app/src/main/res/drawable/baseline_notifications_24.xml b/app/src/main/res/drawable/baseline_notifications_24.xml new file mode 100644 index 000000000..87fc0e0eb --- /dev/null +++ b/app/src/main/res/drawable/baseline_notifications_24.xml @@ -0,0 +1,26 @@ + + + + diff --git a/app/src/main/res/layout/account_item.xml b/app/src/main/res/layout/account_item.xml index a5322ef05..21adc51b9 100644 --- a/app/src/main/res/layout/account_item.xml +++ b/app/src/main/res/layout/account_item.xml @@ -99,5 +99,18 @@ + + diff --git a/app/src/main/res/layout/activity_conversations.xml b/app/src/main/res/layout/activity_conversations.xml index 7128158ad..bbf0a5d32 100644 --- a/app/src/main/res/layout/activity_conversations.xml +++ b/app/src/main/res/layout/activity_conversations.xml @@ -2,6 +2,8 @@ ~ Nextcloud Talk application ~ ~ @author Mario Danic + ~ @author Marcel Hibbe + ~ Copyright (C) 2023-2024 Marcel Hibbe ~ Copyright (C) 2017-2018 Mario Danic ~ ~ This program is free software: you can redistribute it and/or modify @@ -108,7 +110,8 @@ android:layout_centerVertical="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="parent" + android:theme="@style/Theme.MaterialComponents.DayNight.Bridge"> - + android:layout_height="match_parent" + android:orientation="vertical"> + + - + diff --git a/app/src/main/res/layout/activity_invitations.xml b/app/src/main/res/layout/activity_invitations.xml new file mode 100644 index 000000000..91a8980e6 --- /dev/null +++ b/app/src/main/res/layout/activity_invitations.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/federated_invitation_hint.xml b/app/src/main/res/layout/federated_invitation_hint.xml new file mode 100644 index 000000000..5f1867c9d --- /dev/null +++ b/app/src/main/res/layout/federated_invitation_hint.xml @@ -0,0 +1,50 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/rv_item_invitation.xml b/app/src/main/res/layout/rv_item_invitation.xml new file mode 100644 index 000000000..9cc2b096c --- /dev/null +++ b/app/src/main/res/layout/rv_item_invitation.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 982641554..dab4ba2a4 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -104,5 +104,6 @@ #FFFFFF #99000000 + #EF3B02 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 89956198c..3cdc2baa7 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -93,4 +93,9 @@ 30dp 16dp + 24dp + 24dp + 21dp + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9de2c754b..4c6e8b6a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -498,10 +498,6 @@ How to translate with transifex: The meeting will start soon Not set - Allow guests - Could not leave conversation - You need to promote a new moderator before you can leave %1$s. - Copy Forward @@ -593,7 +589,6 @@ How to translate with transifex: Encrypted Avatar - Account icon No personal info set Add name, picture and contact details on your profile page. Failed to retrieve personal user information. @@ -643,7 +638,6 @@ How to translate with transifex: Switch account Maintenance mode Server is currently in maintenance mode. - Close app Take a photo @@ -727,8 +721,6 @@ How to translate with transifex: Private poll Multiple answers - Attachments - All Send without notification Call without notification @@ -750,6 +742,14 @@ How to translate with transifex: Switch to main room Switch to breakout room + + Invitations + from %1$s at %2$s + Accept + Reject + You have pending invitations + No pending invitations + You are not allowed to activate audio! You are not allowed to activate video! Scroll to bottom diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index 992f68d81..19f1c33c2 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 8 errors and 80 warnings + Lint Report: 8 errors and 79 warnings