Просмотр исходного кода

handle federation invitations

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 1 год назад
Родитель
Сommit
c13f2589ff
41 измененных файлов с 1590 добавлено и 191 удалено
  1. 4 0
      app/src/main/AndroidManifest.xml
  2. 3 3
      app/src/main/java/com/nextcloud/talk/account/SwitchAccountActivity.kt
  3. 7 1
      app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
  4. 0 154
      app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java
  5. 129 0
      app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.kt
  6. 13 0
      app/src/main/java/com/nextcloud/talk/api/NcApi.java
  7. 94 2
      app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt
  8. 23 0
      app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepository.kt
  9. 25 0
      app/src/main/java/com/nextcloud/talk/conversationlist/data/ConversationsListRepositoryImpl.kt
  10. 112 0
      app/src/main/java/com/nextcloud/talk/conversationlist/viewmodels/ConversationsListViewModel.kt
  11. 14 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt
  12. 1 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java
  13. 12 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
  14. 195 0
      app/src/main/java/com/nextcloud/talk/invitation/InvitationsActivity.kt
  15. 104 0
      app/src/main/java/com/nextcloud/talk/invitation/adapters/InvitationsAdapter.kt
  16. 35 0
      app/src/main/java/com/nextcloud/talk/invitation/data/Invitation.kt
  17. 27 0
      app/src/main/java/com/nextcloud/talk/invitation/data/InvitationActionModel.kt
  18. 28 0
      app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsModel.kt
  19. 30 0
      app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepository.kt
  20. 87 0
      app/src/main/java/com/nextcloud/talk/invitation/data/InvitationsRepositoryImpl.kt
  21. 136 0
      app/src/main/java/com/nextcloud/talk/invitation/viewmodels/InvitationsViewModel.kt
  22. 26 2
      app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
  23. 55 0
      app/src/main/java/com/nextcloud/talk/models/json/invitation/Invitation.kt
  24. 38 0
      app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOCS.kt
  25. 35 0
      app/src/main/java/com/nextcloud/talk/models/json/invitation/InvitationOverall.kt
  26. 1 0
      app/src/main/java/com/nextcloud/talk/openconversations/ListOpenConversationsActivity.kt
  27. 1 1
      app/src/main/java/com/nextcloud/talk/openconversations/adapters/OpenConversationsAdapter.kt
  28. 51 11
      app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountDialogFragment.java
  29. 3 4
      app/src/main/java/com/nextcloud/talk/ui/dialog/ChooseAccountShareToDialogFragment.kt
  30. 12 0
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  31. 1 0
      app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt
  32. 26 0
      app/src/main/res/drawable/baseline_notifications_24.xml
  33. 13 0
      app/src/main/res/layout/account_item.xml
  34. 12 4
      app/src/main/res/layout/activity_conversations.xml
  35. 67 0
      app/src/main/res/layout/activity_invitations.xml
  36. 50 0
      app/src/main/res/layout/federated_invitation_hint.xml
  37. 105 0
      app/src/main/res/layout/rv_item_invitation.xml
  38. 1 0
      app/src/main/res/values/colors.xml
  39. 5 0
      app/src/main/res/values/dimens.xml
  40. 8 8
      app/src/main/res/values/strings.xml
  41. 1 1
      scripts/analysis/lint-results.txt

+ 4 - 0
app/src/main/AndroidManifest.xml

@@ -261,6 +261,10 @@
             android:name=".openconversations.ListOpenConversationsActivity"
             android:theme="@style/AppTheme" />
 
+        <activity
+            android:name=".invitation.InvitationsActivity"
+            android:theme="@style/AppTheme" />
+
         <activity
             android:name=".lock.LockedActivity"
             android:theme="@style/AppTheme" />

+ 3 - 3
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)

+ 7 - 1
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) }

+ 0 - 154
app/src/main/java/com/nextcloud/talk/adapters/items/AdvancedUserItem.java

@@ -1,154 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Mario Danic
- * @author Andy Scherzinger
- * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
- * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-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<AdvancedUserItem.UserItemViewHolder> implements
-        IFilterable<String> {
-
-    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);
-        }
-    }
-}

+ 129 - 0
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 <info@andy-scherzinger.de>
+ * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+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<UserItemViewHolder>(), IFilterable<String?> {
+    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<IFlexible<RecyclerView.ViewHolder>>?
+    ): UserItemViewHolder {
+        return UserItemViewHolder(view, adapter)
+    }
+
+    override fun bindViewHolder(
+        adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
+        holder: UserItemViewHolder,
+        position: Int,
+        payloads: MutableList<Any>
+    ) {
+        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!!)
+        }
+    }
+}

+ 13 - 0
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<GenericOverall> setRecordingConsent(@Header("Authorization") String authorization,
                                                    @Url String url,
                                                    @Field("recordingConsent") int recordingConsent);
+
+    @GET
+    Observable<InvitationOverall> getInvitations(@Header("Authorization") String authorization,
+                                                 @Url String url);
+
+    @POST
+    Observable<GenericOverall> acceptInvitation(@Header("Authorization") String authorization,
+                                                 @Url String url);
+
+    @DELETE
+    Observable<GenericOverall> rejectInvitation(@Header("Authorization") String authorization,
+                                                   @Url String url);
 }

+ 94 - 2
app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt

@@ -8,7 +8,7 @@
  * @author Ezhil Shanmugham
  * Copyright (C) 2022 Álvaro Brey <alvaro.brey@nextcloud.com>
  * 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 <ezhil56x.contact@gmail.com>
  *
@@ -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
     }
 }

+ 23 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.conversationlist.data
+
+interface ConversationsListRepository

+ 25 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.conversationlist.data
+
+import com.nextcloud.talk.api.NcApi
+
+class ConversationsListRepositoryImpl(private val ncApi: NcApi) : ConversationsListRepository

+ 112 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<ViewState> =
+        MutableLiveData(GetFederationInvitationsStartState)
+    val getFederationInvitationsViewState: LiveData<ViewState>
+        get() = _getFederationInvitationsViewState
+
+    object ShowBadgeStartState : ViewState
+    object ShowBadgeErrorState : ViewState
+    open class ShowBadgeSuccessState(val showBadge: Boolean) : ViewState
+
+    private val _showBadgeViewState: MutableLiveData<ViewState> = MutableLiveData(ShowBadgeStartState)
+    val showBadgeViewState: LiveData<ViewState>
+        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<InvitationsModel> {
+        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
+    }
+}

+ 14 - 0
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)
+    }
 }

+ 1 - 0
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();
 

+ 12 - 0
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
 }

+ 195 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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)
+    }
+}

+ 104 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<Invitation, InvitationsAdapter.InvitationsViewHolder>(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<Invitation>() {
+    override fun areItemsTheSame(oldItem: Invitation, newItem: Invitation): Boolean {
+        return oldItem == newItem
+    }
+
+    override fun areContentsTheSame(oldItem: Invitation, newItem: Invitation): Boolean {
+        return oldItem.id == newItem.id
+    }
+}

+ 35 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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
+)

+ 27 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.invitation.data
+enum class ActionEnum { ACCEPT, REJECT }
+data class InvitationActionModel(
+    var action: ActionEnum,
+    var statusCode: Int,
+    var invitation: Invitation
+)

+ 28 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.invitation.data
+
+import com.nextcloud.talk.data.user.model.User
+
+data class InvitationsModel(
+    var user: User,
+    var invitations: List<Invitation>
+)

+ 30 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<InvitationsModel>
+    fun acceptInvitation(user: User, invitation: Invitation): Observable<InvitationActionModel>
+    fun rejectInvitation(user: User, invitation: Invitation): Observable<InvitationActionModel>
+}

+ 87 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<InvitationsModel> {
+        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<InvitationActionModel> {
+        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<InvitationActionModel> {
+        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<com.nextcloud.talk.models.json.invitation.Invitation>
+    ): 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
+    }
+}

+ 136 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<Invitation>) : ViewState
+
+    private val _fetchInvitationsViewState: MutableLiveData<ViewState> = MutableLiveData(FetchInvitationsStartState)
+    val fetchInvitationsViewState: LiveData<ViewState>
+        get() = _fetchInvitationsViewState
+
+    object InvitationActionStartState : ViewState
+    object InvitationActionErrorState : ViewState
+
+    private val _invitationActionViewState: MutableLiveData<ViewState> = MutableLiveData(InvitationActionStartState)
+
+    open class InvitationActionSuccessState(val action: ActionEnum, val invitation: Invitation) : ViewState
+
+    val invitationActionViewState: LiveData<ViewState>
+        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<InvitationsModel> {
+        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<InvitationActionModel> {
+        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
+    }
+}

+ 26 - 2
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

+ 55 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+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)
+}

+ 38 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+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<Invitation>?
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, null)
+}

+ 35 - 0
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 <dev@mhibbe.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+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)
+}

+ 1 - 0
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

+ 1 - 1
app/src/main/java/com/nextcloud/talk/openconversations/adapters/OpenConversationsAdapter.kt

@@ -18,7 +18,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package com.nextcloud.talk.openconversations
+package com.nextcloud.talk.openconversations.adapters
 
 import android.view.LayoutInflater
 import android.view.ViewGroup

+ 51 - 11
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();

+ 3 - 4
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<AdvancedUserItem>? = 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()

+ 12 - 0
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;
+    }
 }

+ 1 - 0
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"
 }

+ 26 - 0
app/src/main/res/drawable/baseline_notifications_24.xml

@@ -0,0 +1,26 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2021 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
+</vector>

+ 13 - 0
app/src/main/res/layout/account_item.xml

@@ -99,5 +99,18 @@
 
         </LinearLayout>
 
+        <ImageView
+            android:id="@+id/action_required"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_alignParentEnd="true"
+            android:layout_centerVertical="true"
+            android:clickable="true"
+            android:contentDescription="@null"
+            android:focusable="true"
+            android:padding="@dimen/standard_padding"
+            android:src="@drawable/accent_circle"
+            app:tint="@color/badge_color" />
+
     </RelativeLayout>
 </com.google.android.material.card.MaterialCardView>

+ 12 - 4
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 <dev@mhibbe.de>
   ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
   ~
   ~ 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">
 
                     <com.google.android.material.button.MaterialButton
                         android:id="@+id/switch_account_button"
@@ -215,16 +218,21 @@
         android:visibility="gone"
         app:layout_behavior="com.nextcloud.talk.utils.FABAwareScrollingViewBehavior">
 
-        <FrameLayout
+        <LinearLayout
             android:layout_width="match_parent"
-            android:layout_height="match_parent">
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+
+            <include
+                android:id="@+id/conversation_list_hint_include"
+                layout="@layout/federated_invitation_hint" />
 
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/recycler_view"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent" />
 
-        </FrameLayout>
+        </LinearLayout>
 
     </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
 

+ 67 - 0
app/src/main/res/layout/activity_invitations.xml

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ 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 <http://www.gnu.org/licenses/>.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:animateLayoutChanges="true">
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/invitations_appbar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+        <com.google.android.material.appbar.MaterialToolbar
+            android:id="@+id/invitations_toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="?attr/actionBarSize"
+            android:background="@color/appbar"
+            android:theme="?attr/actionBarPopupTheme"
+            app:title="@string/nc_invitations"
+            app:layout_scrollFlags="scroll|enterAlways"
+            app:navigationIconTint="@color/fontAppbar"
+            app:popupTheme="@style/appActionBarPopupMenu"
+            app:titleTextColor="@color/fontAppbar" />
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/invitations_recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:layoutManager="LinearLayoutManager"/>
+
+    <LinearLayout
+        android:id="@+id/progress_bar_wrapper"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:visibility="gone">
+        <ProgressBar
+            style="?android:attr/progressBarStyle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"/>
+    </LinearLayout>
+
+    <include
+        android:id="@+id/emptyList"
+        layout="@layout/empty_list" />
+
+</LinearLayout>

+ 50 - 0
app/src/main/res/layout/federated_invitation_hint.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2024 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ 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 <http://www.gnu.org/licenses/>.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/conversation_list_hint_layout"
+    android:layout_width="match_parent"
+    android:layout_height="60dp"
+    android:layout_gravity="center"
+    android:gravity="center_vertical|center_horizontal"
+    android:orientation="vertical">
+
+    <com.google.android.material.card.MaterialCardView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginTop="@dimen/standard_half_margin"
+        android:layout_marginStart="@dimen/standard_margin"
+        android:layout_marginEnd="@dimen/standard_margin"
+        app:cardCornerRadius="8dp"
+        app:cardElevation="2dp"
+        app:strokeWidth="0dp">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingStart="@dimen/standard_padding"
+            android:paddingEnd="@dimen/standard_padding"
+            android:textAlignment="center"
+            android:padding="@dimen/standard_padding"
+            android:text="@string/nc_federation_pending_invitation_hint" />
+
+    </com.google.android.material.card.MaterialCardView>
+</LinearLayout>

+ 105 - 0
app/src/main/res/layout/rv_item_invitation.xml

@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Nextcloud Android client application
+
+  @author Tobias Kaminsky
+  @author TSI-mc
+  @author Marcel Hibbe
+  Copyright (C) 2023 Andy Scherzinger
+  Copyright (C) 2023 TSI-mc
+  Copyright (C) 2018 Tobias Kaminsky
+  Copyright (C) 2018 Nextcloud GmbH
+  Copyright (C) 2024 Marcel Hibbe <dev@mhibbe.de>
+
+  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 <https://www.gnu.org/licenses/>.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:paddingStart="@dimen/standard_padding"
+    android:paddingTop="@dimen/standard_padding"
+    android:paddingEnd="@dimen/standard_padding"
+    android:paddingBottom="@dimen/standard_padding"
+    tools:ignore="UseCompoundDrawables">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="@dimen/notification_icon_width"
+        android:layout_height="@dimen/notification_icon_height"
+        android:layout_alignParentTop="true"
+        android:layout_marginEnd="@dimen/notification_icon_layout_right_end_margin"
+        android:contentDescription="@null"
+        android:src="@drawable/ic_email" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_alignTop="@id/icon"
+        android:layout_toEndOf="@id/icon">
+
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:ellipsize="end"
+            android:textAppearance="?android:attr/textAppearanceListItem"
+            android:paddingBottom="@dimen/standard_half_padding"
+            tools:text="Ghostbusters" />
+
+        <TextView
+            android:id="@+id/subject"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:ellipsize="end"
+            android:paddingBottom="@dimen/standard_half_padding"
+            tools:text="from Bill Murray at 127.0.0.123" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/standard_half_margin"
+            android:orientation="horizontal">
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/reject_invitation"
+                style="@style/Button.Borderless"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:text="@string/nc_federation_invitation_reject"
+                android:theme="@style/Button.Primary"
+                app:cornerRadius="@dimen/button_corner_radius" />
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/accept_invitation"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_marginEnd="@dimen/standard_half_margin"
+                android:layout_weight="1"
+                android:text="@string/nc_federation_invitation_accept"
+                android:textColor="@color/high_emphasis_text"
+                android:theme="@style/Button.Primary"
+                app:cornerRadius="@dimen/button_corner_radius" />
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</RelativeLayout>

+ 1 - 0
app/src/main/res/values/colors.xml

@@ -104,5 +104,6 @@
     <color name="dialog_background">#FFFFFF</color>
 
     <color name="icon_on_bg_default">#99000000</color>
+    <color name="badge_color">#EF3B02</color>
 
 </resources>

+ 5 - 0
app/src/main/res/values/dimens.xml

@@ -93,4 +93,9 @@
     <dimen name="sm_icon_height">30dp</dimen>
     <dimen name="side_margin">16dp</dimen>
 
+    <dimen name="notification_icon_width">24dp</dimen>
+    <dimen name="notification_icon_height">24dp</dimen>
+    <dimen name="notification_icon_layout_right_end_margin">21dp</dimen>
+
+
 </resources>

+ 8 - 8
app/src/main/res/values/strings.xml

@@ -498,10 +498,6 @@ How to translate with transifex:
     <string name="nc_lobby_start_soon">The meeting will start soon</string>
     <string name="nc_manual">Not set</string>
 
-    <string name="nc_allow_guests">Allow guests</string>
-    <string name="nc_last_moderator_title">Could not leave conversation</string>
-    <string name="nc_last_moderator">You need to promote a new moderator before you can leave %1$s.</string>
-
     <!-- Chat -->
     <string name="nc_copy_message">Copy</string>
     <string name="nc_forward_message">Forward</string>
@@ -593,7 +589,6 @@ How to translate with transifex:
     <string name="encrypted">Encrypted</string>
 
     <string name="avatar">Avatar</string>
-    <string name="account_icon">Account icon</string>
     <string name="userinfo_no_info_headline">No personal info set</string>
     <string name="userinfo_no_info_text">Add name, picture and contact details on your profile page.</string>
     <string name="userinfo_error_text">Failed to retrieve personal user information.</string>
@@ -643,7 +638,6 @@ How to translate with transifex:
     <string name="nc_switch_account">Switch account</string>
     <string name="nc_dialog_maintenance_mode">Maintenance mode</string>
     <string name="nc_dialog_maintenance_mode_description">Server is currently in maintenance mode.</string>
-    <string name="nc_close_app">Close app</string>
 
     <!-- Take photo -->
     <string name="take_photo">Take a photo</string>
@@ -727,8 +721,6 @@ How to translate with transifex:
     <string name="polls_private_poll">Private poll</string>
     <string name="polls_multiple_answers">Multiple answers</string>
 
-    <string name="title_attachments">Attachments</string>
-
     <string name="reactions_tab_all">All</string>
     <string name="send_without_notification">Send without notification</string>
     <string name="call_without_notification">Call without notification</string>
@@ -750,6 +742,14 @@ How to translate with transifex:
     <string name="switch_to_main_room">Switch to main room</string>
     <string name="switch_to_breakout_room">Switch to breakout room</string>
 
+    <!-- Invitations -->
+    <string name="nc_invitations">Invitations</string>
+    <string name="nc_federation_invited_to_room">from %1$s at %2$s</string>
+    <string name="nc_federation_invitation_accept">Accept</string>
+    <string name="nc_federation_invitation_reject">Reject</string>
+    <string name="nc_federation_pending_invitation_hint">You have pending invitations</string>
+    <string name="nc_federation_no_invitations">No pending invitations</string>
+
     <string name="nc_not_allowed_to_activate_audio">You are not allowed to activate audio!</string>
     <string name="nc_not_allowed_to_activate_video">You are not allowed to activate video!</string>
     <string name="scroll_to_bottom">Scroll to bottom</string>

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 8 errors and 80 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 8 errors and 79 warnings</span>