Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2024-02-02 08:45:08 +01:00
Родитель 2c911d998a
Коммит c13f2589ff
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: C793F8B59F43CE7B
41 изменённых файлов: 1590 добавлений и 191 удалений

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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);
}
}
}

Просмотреть файл

@ -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!!)
}
}
}

Просмотреть файл

@ -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);
}

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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();

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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();

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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