From 0b7ce8295cd735c7b300fdedd15a060280a0d248 Mon Sep 17 00:00:00 2001 From: Mario Danic Date: Mon, 16 Sep 2019 11:10:30 +0200 Subject: [PATCH] First draft of participants management Signed-off-by: Mario Danic --- .../talk/adapters/items/UserItem.java | 3 +- .../java/com/nextcloud/talk/api/NcApi.java | 10 + .../talk/controllers/ContactsController.java | 291 +++++++++++------- .../controllers/ConversationInfoController.kt | 141 +++++++-- .../nextcloud/talk/events/EventStatus.java | 2 +- .../jobs/AddParticipantsToConversation.java | 95 ++++++ .../com/nextcloud/talk/utils/ApiUtils.java | 14 + .../nextcloud/talk/utils/bundle/BundleKeys.kt | 4 + ...terialPreferenceCategoryWithRightLink.java | 165 ++++++++++ .../res/layout/category_with_right_action.xml | 56 ++++ .../layout/controller_conversation_info.xml | 5 +- app/src/main/res/layout/progress_layout.xml | 35 +++ app/src/main/res/values/attrs.xml | 26 ++ app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 6 + 15 files changed, 714 insertions(+), 140 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ui/MaterialPreferenceCategoryWithRightLink.java create mode 100644 app/src/main/res/layout/category_with_right_action.xml create mode 100644 app/src/main/res/layout/progress_layout.xml create mode 100644 app/src/main/res/values/attrs.xml diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.java index 735bcbbf1..0deff5fc8 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.java @@ -60,6 +60,7 @@ public class UserItem extends AbstractFlexibleItem private Participant participant; private UserEntity userEntity; private GenericTextHeaderItem header; + public boolean isOnline = true; public UserItem(Participant participant, UserEntity userEntity, GenericTextHeaderItem genericTextHeaderItem) { this.participant = participant; @@ -167,7 +168,7 @@ public class UserItem extends AbstractFlexibleItem holder.simpleDraweeView.getHierarchy().setImage(new BitmapDrawable(DisplayUtils.getRoundedBitmapFromVectorDrawableResource(NextcloudTalkApplication.Companion.getSharedApplication().getResources(), R.drawable.ic_people_group_white_24px)), 100, true); } - if (!isEnabled()) { + if (!isOnline) { holder.itemView.setAlpha(0.38f); } else { holder.itemView.setAlpha(1.0f); diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 84a9afc56..45517c3dd 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -113,6 +113,16 @@ public interface NcApi { @QueryMap Map options); + // also used for removing a guest from a conversation + @DELETE + Observable removeParticipantFromConversation(@Header("Authorization") String authorization, @Url String url, @Query("participant") String participantId); + + @POST + Observable promoteUserToModerator(@Header("Authorization") String authorization, @Url String url, @Query("participant") String participantId); + + @DELETE + Observable demoteModeratorToUser(@Header("Authorization") String authorization, @Url String url, @Query("participant") String participantId); + /* Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room/roomToken/participants/self */ diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java index 832cde49b..f57ef9b98 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java @@ -29,21 +29,18 @@ import android.os.Handler; import android.text.InputType; import android.text.TextUtils; import android.util.Log; -import android.view.*; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.ProgressBar; import android.widget.RelativeLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.SearchView; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.view.MenuItemCompat; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import autodagger.AutoInjector; -import butterknife.BindView; -import butterknife.OnClick; -import butterknife.Optional; + +import com.afollestad.materialdialogs.LayoutMode; +import com.afollestad.materialdialogs.MaterialDialog; import com.bluelinelabs.conductor.RouterTransaction; import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler; import com.bluelinelabs.logansquare.LoganSquare; @@ -59,13 +56,15 @@ import com.nextcloud.talk.controllers.base.BaseController; import com.nextcloud.talk.controllers.bottomsheet.EntryMenuController; import com.nextcloud.talk.controllers.bottomsheet.OperationsMenuController; import com.nextcloud.talk.events.BottomSheetLockEvent; +import com.nextcloud.talk.jobs.AddParticipantsToConversation; +import com.nextcloud.talk.jobs.DeleteConversationWorker; import com.nextcloud.talk.models.RetrofitBucket; import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall; import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser; -import com.nextcloud.talk.models.json.participants.Participant; import com.nextcloud.talk.models.json.conversations.Conversation; import com.nextcloud.talk.models.json.conversations.RoomOverall; +import com.nextcloud.talk.models.json.participants.Participant; import com.nextcloud.talk.models.json.sharees.Sharee; import com.nextcloud.talk.models.json.sharees.ShareesOverall; import com.nextcloud.talk.utils.ApiUtils; @@ -74,6 +73,36 @@ import com.nextcloud.talk.utils.KeyboardUtils; import com.nextcloud.talk.utils.bundle.BundleKeys; import com.nextcloud.talk.utils.database.user.UserUtils; import com.nextcloud.talk.utils.preferences.AppPreferences; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; +import org.parceler.Parcels; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SearchView; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.view.MenuItemCompat; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import autodagger.AutoInjector; +import butterknife.BindView; +import butterknife.OnClick; +import butterknife.Optional; import eu.davidea.fastscroller.FastScroller; import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.SelectableAdapter; @@ -85,13 +114,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import okhttp3.ResponseBody; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.parceler.Parcels; - -import javax.inject.Inject; -import java.util.*; @AutoInjector(NextcloudTalkApplication.class) public class ContactsController extends BaseController implements SearchView.OnQueryTextListener, @@ -162,6 +184,10 @@ public class ContactsController extends BaseController implements SearchView.OnQ private Set selectedUserIds; private Set selectedGroupIds; + private List existingParticipants; + private boolean isAddingParticipantsView; + private String conversationToken; + public ContactsController() { super(); setHasOptionsMenu(true); @@ -172,6 +198,16 @@ public class ContactsController extends BaseController implements SearchView.OnQ setHasOptionsMenu(true); if (args.containsKey(BundleKeys.INSTANCE.getKEY_NEW_CONVERSATION())) { isNewConversationView = true; + existingParticipants = new ArrayList<>(); + } else if (args.containsKey(BundleKeys.INSTANCE.getKEY_ADD_PARTICIPANTS())) { + isAddingParticipantsView = true; + conversationToken = args.getString(BundleKeys.INSTANCE.getKEY_TOKEN()); + + existingParticipants = new ArrayList<>(); + + if (args.containsKey(BundleKeys.INSTANCE.getKEY_EXISTING_PARTICIPANTS())) { + existingParticipants = args.getStringArrayList(BundleKeys.INSTANCE.getKEY_EXISTING_PARTICIPANTS()); + } } selectedGroupIds = new HashSet<>(); @@ -191,6 +227,12 @@ public class ContactsController extends BaseController implements SearchView.OnQ if (isNewConversationView) { toggleNewCallHeaderVisibility(!isPublicCall); } + + if (isAddingParticipantsView) { + joinConversationViaLinkLayout.setVisibility(View.GONE); + conversationPrivacyToogleLayout.setVisibility(View.GONE); + } + } @Override @@ -232,111 +274,128 @@ public class ContactsController extends BaseController implements SearchView.OnQ } private void selectionDone() { - if (!isPublicCall && (selectedGroupIds.size() + selectedUserIds.size() == 1)) { - String userId; - String roomType = "1"; + if (!isAddingParticipantsView) { + if (!isPublicCall && (selectedGroupIds.size() + selectedUserIds.size() == 1)) { + String userId; + String roomType = "1"; - if (selectedGroupIds.size() == 1) { - roomType = "2"; - userId = selectedGroupIds.iterator().next(); - } else { - userId = selectedUserIds.iterator().next(); - } + if (selectedGroupIds.size() == 1) { + roomType = "2"; + userId = selectedGroupIds.iterator().next(); + } else { + userId = selectedUserIds.iterator().next(); + } - RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(currentUser.getBaseUrl(), roomType, - userId, null); - ncApi.createRoom(credentials, - retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { + RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(currentUser.getBaseUrl(), roomType, + userId, null); + ncApi.createRoom(credentials, + retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) { - } - - @Override - public void onNext(RoomOverall roomOverall) { - Intent conversationIntent = new Intent(getActivity(), MagicCallActivity.class); - Bundle bundle = new Bundle(); - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY(), currentUser); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), roomOverall.getOcs().getData().getToken()); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), roomOverall.getOcs().getData().getRoomId()); - - if (currentUser.hasSpreedFeatureCapability("chat-v2")) { - ncApi.getRoom(credentials, - ApiUtils.getRoom(currentUser.getBaseUrl(), - roomOverall.getOcs().getData().getToken())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(RoomOverall roomOverall) { - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_ACTIVE_CONVERSATION(), - Parcels.wrap(roomOverall.getOcs().getData())); - - ConductorRemapping.INSTANCE.remapChatController(getRouter(), currentUser.getId(), - roomOverall.getOcs().getData().getToken(), bundle, true); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - }); - } else { - conversationIntent.putExtras(bundle); - startActivity(conversationIntent); - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - if (!isDestroyed() && !isBeingDestroyed()) { - getRouter().popCurrentController(); - } - } - }, 100); } - } - @Override - public void onError(Throwable e) { + @Override + public void onNext(RoomOverall roomOverall) { + Intent conversationIntent = new Intent(getActivity(), MagicCallActivity.class); + Bundle bundle = new Bundle(); + bundle.putParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY(), currentUser); + bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), roomOverall.getOcs().getData().getToken()); + bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), roomOverall.getOcs().getData().getRoomId()); - } + if (currentUser.hasSpreedFeatureCapability("chat-v2")) { + ncApi.getRoom(credentials, + ApiUtils.getRoom(currentUser.getBaseUrl(), + roomOverall.getOcs().getData().getToken())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { - @Override - public void onComplete() { - } - }); - } else { + @Override + public void onSubscribe(Disposable d) { - Bundle bundle = new Bundle(); - Conversation.ConversationType roomType; - if (isPublicCall) { - roomType = Conversation.ConversationType.ROOM_PUBLIC_CALL; + } + + @Override + public void onNext(RoomOverall roomOverall) { + bundle.putParcelable(BundleKeys.INSTANCE.getKEY_ACTIVE_CONVERSATION(), + Parcels.wrap(roomOverall.getOcs().getData())); + + ConductorRemapping.INSTANCE.remapChatController(getRouter(), currentUser.getId(), + roomOverall.getOcs().getData().getToken(), bundle, true); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + } else { + conversationIntent.putExtras(bundle); + startActivity(conversationIntent); + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + if (!isDestroyed() && !isBeingDestroyed()) { + getRouter().popCurrentController(); + } + } + }, 100); + } + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + } + }); } else { - roomType = Conversation.ConversationType.ROOM_GROUP_CALL; + + Bundle bundle = new Bundle(); + Conversation.ConversationType roomType; + if (isPublicCall) { + roomType = Conversation.ConversationType.ROOM_PUBLIC_CALL; + } else { + roomType = Conversation.ConversationType.ROOM_GROUP_CALL; + } + + ArrayList userIdsArray = new ArrayList<>(selectedUserIds); + ArrayList groupIdsArray = new ArrayList<>(selectedGroupIds); + + + bundle.putParcelable(BundleKeys.INSTANCE.getKEY_CONVERSATION_TYPE(), Parcels.wrap(roomType)); + bundle.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_PARTICIPANTS(), userIdsArray); + bundle.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_GROUP(), groupIdsArray); + bundle.putInt(BundleKeys.INSTANCE.getKEY_OPERATION_CODE(), 11); + prepareAndShowBottomSheetWithBundle(bundle, true); } + } else { + String[] userIdsArray = selectedUserIds.toArray(new String[selectedUserIds.size()]); + String[] groupIdsArray = selectedGroupIds.toArray(new String[selectedGroupIds.size()]); - ArrayList userIdsArray = new ArrayList<>(selectedUserIds); - ArrayList groupIdsArray = new ArrayList<>(selectedGroupIds); + Data.Builder data = new Data.Builder(); + data.putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), currentUser.getId()); + data.putString(BundleKeys.INSTANCE.getKEY_TOKEN(), conversationToken); + data.putStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_USERS(), userIdsArray); + data.putStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_GROUPS(), groupIdsArray); + OneTimeWorkRequest addParticipantsToConversationWorker = + new OneTimeWorkRequest.Builder(AddParticipantsToConversation.class).setInputData(data.build()).build(); + WorkManager.getInstance().enqueue(addParticipantsToConversationWorker); - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_CONVERSATION_TYPE(), Parcels.wrap(roomType)); - bundle.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_PARTICIPANTS(), userIdsArray); - bundle.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_GROUP(), groupIdsArray); - bundle.putInt(BundleKeys.INSTANCE.getKEY_OPERATION_CODE(), 11); - prepareAndShowBottomSheetWithBundle(bundle, true); + getRouter().popCurrentController(); } } @@ -479,7 +538,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ } for (Sharee sharee : shareeHashSet) { - if (!sharee.getValue().getShareWith().equals(currentUser.getUserId())) { + if (!sharee.getValue().getShareWith().equals(currentUser.getUserId()) && !existingParticipants.contains(sharee.getValue().getShareWith())) { participant = new Participant(); participant.setDisplayName(sharee.getLabel()); String headerTitle; @@ -510,7 +569,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ autocompleteUsersHashSet.addAll(autocompleteOverall.getOcs().getData()); for (AutocompleteUser autocompleteUser : autocompleteUsersHashSet) { - if (!autocompleteUser.getId().equals(currentUser.getUserId())) { + if (!autocompleteUser.getId().equals(currentUser.getUserId()) && !existingParticipants.contains(autocompleteUser.getId())) { participant = new Participant(); participant.setUserId(autocompleteUser.getId()); participant.setDisplayName(autocompleteUser.getLabel()); @@ -764,7 +823,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ @Override protected String getTitle() { - if (!isNewConversationView) { + if (!isNewConversationView && !isAddingParticipantsView) { return getResources().getString(R.string.nc_app_name); } else { return getResources().getString(R.string.nc_select_contacts); @@ -840,7 +899,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ @Override public boolean onItemClick(View view, int position) { if (adapter.getItem(position) instanceof UserItem) { - if (!isNewConversationView) { + if (!isNewConversationView && !isAddingParticipantsView) { UserItem userItem = (UserItem) adapter.getItem(position); String roomType = "1"; diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt index 82e06f157..02d5daf5e 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt @@ -30,6 +30,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.ProgressBar +import android.widget.TextView import androidx.appcompat.widget.SwitchCompat import androidx.emoji.widget.EmojiTextView import androidx.recyclerview.widget.RecyclerView @@ -43,6 +44,8 @@ import com.afollestad.materialdialogs.LayoutMode.WRAP_CONTENT import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.bottomsheets.BottomSheet import com.afollestad.materialdialogs.datetime.dateTimePicker +import com.afollestad.materialdialogs.list.listItems +import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.view.SimpleDraweeView @@ -51,6 +54,8 @@ import com.nextcloud.talk.adapters.items.UserItem import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.controllers.base.BaseController +import com.nextcloud.talk.events.BottomSheetLockEvent +import com.nextcloud.talk.events.EventStatus import com.nextcloud.talk.jobs.DeleteConversationWorker import com.nextcloud.talk.jobs.LeaveConversationWorker import com.nextcloud.talk.models.database.UserEntity @@ -65,6 +70,7 @@ import com.nextcloud.talk.utils.DateUtils import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule +import com.nextcloud.talk.utils.ui.MaterialPreferenceCategoryWithRightLink import com.yarolegovich.lovelydialog.LovelySaveStateHandler import com.yarolegovich.lovelydialog.LovelyStandardDialog import com.yarolegovich.mp.* @@ -75,12 +81,15 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import java.util.* import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) -class ConversationInfoController(args: Bundle) : BaseController(args) { +class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleAdapter.OnItemClickListener { @BindView(R.id.notification_settings) lateinit var notificationsPreferenceScreen: MaterialPreferenceScreen @@ -101,7 +110,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args) { @BindView(R.id.display_name_text) lateinit var conversationDisplayName: EmojiTextView @BindView(R.id.participants_list_category) - lateinit var participantsListCategory: MaterialPreferenceCategory + lateinit var participantsListCategory: MaterialPreferenceCategoryWithRightLink @BindView(R.id.recycler_view) lateinit var recyclerView: RecyclerView @BindView(R.id.deleteConversationAction) @@ -112,11 +121,15 @@ class ConversationInfoController(args: Bundle) : BaseController(args) { lateinit var ownOptionsCategory: MaterialPreferenceCategory @BindView(R.id.muteCalls) lateinit var muteCalls: MaterialSwitchPreference + @BindView(R.id.mpc_action) + lateinit var actionTextView: TextView; @set:Inject lateinit var ncApi: NcApi @set:Inject lateinit var context: Context + @set:Inject + lateinit var eventBus: EventBus private val conversationToken: String? private val conversationUser: UserEntity? @@ -168,12 +181,16 @@ class ConversationInfoController(args: Bundle) : BaseController(args) { override fun onAttach(view: View) { super.onAttach(view) + eventBus.register(this) + if (databaseStorageModule == null) { databaseStorageModule = DatabaseStorageModule(conversationUser!!, conversationToken) } notificationsPreferenceScreen.setStorageModule(databaseStorageModule) conversationInfoWebinar.setStorageModule(databaseStorageModule) + + fetchRoomInfo() } override fun onViewBound(view: View) { @@ -182,20 +199,6 @@ class ConversationInfoController(args: Bundle) : BaseController(args) { if (saveStateHandler == null) { saveStateHandler = LovelySaveStateHandler() } - - if (adapter == null) { - fetchRoomInfo() - } else { - loadConversationAvatar() - notificationsPreferenceScreen.visibility = View.VISIBLE - nameCategoryView.visibility = View.VISIBLE - participantsListCategory.visibility = View.VISIBLE - progressBar.visibility = View.GONE - conversationDisplayName.text = conversation!!.displayName - - setupWebinaryView() - setupAdapter() - } } private fun setupWebinaryView() { @@ -292,6 +295,15 @@ class ConversationInfoController(args: Bundle) : BaseController(args) { } } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(eventStatus: EventStatus) { + getListOfParticipants() + } + + override fun onDetach(view: View) { + super.onDetach(view) + eventBus.unregister(this) + } private fun showDeleteConversationDialog(savedInstanceState: Bundle?) { if (activity != null) { @@ -334,6 +346,26 @@ class ConversationInfoController(args: Bundle) : BaseController(args) { recyclerView.layoutManager = layoutManager recyclerView.setHasFixedSize(true) recyclerView.adapter = adapter + + adapter!!.addListener(this) + actionTextView.setOnClickListener { + val bundle = Bundle() + val existingParticipantsId = arrayListOf() + + recyclerViewItems.forEach { + val userItem = it as UserItem + existingParticipantsId.add(userItem.model.userId) + } + + bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true); + bundle.putStringArrayList(BundleKeys.KEY_EXISTING_PARTICIPANTS, existingParticipantsId) + bundle.putString(BundleKeys.KEY_TOKEN, conversation!!.token) + + getRouter().pushController((RouterTransaction.with(ContactsController(bundle)) + .pushChangeHandler(HorizontalChangeHandler()) + .popChangeHandler(HorizontalChangeHandler()))); + + } } } @@ -347,11 +379,11 @@ class ConversationInfoController(args: Bundle) : BaseController(args) { for (i in participants.indices) { participant = participants[i] userItem = UserItem(participant, conversationUser, null) - userItem.isEnabled = participant.sessionId != "0" + userItem.isOnline = !participant.sessionId.equals("0") if (!TextUtils.isEmpty(participant.userId) && participant.userId == conversationUser!!.userId) { ownUserItem = userItem - userItem.model.sessionId = "-1" - userItem.isEnabled = true + ownUserItem.model.sessionId = "-1" + ownUserItem.isOnline = true } else { recyclerViewItems.add(userItem) } @@ -365,7 +397,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args) { setupAdapter() participantsListCategory.visibility = View.VISIBLE - adapter!!.notifyDataSetChanged() + adapter!!.updateDataSet(recyclerViewItems) } override fun getTitle(): String? { @@ -555,6 +587,75 @@ class ConversationInfoController(args: Bundle) : BaseController(args) { } } + override fun onItemClick(view: View?, position: Int): Boolean { + val userItem = adapter?.getItem(position) as UserItem + val participant = userItem.model + + if (participant.userId != conversationUser!!.userId) { + val items = mutableListOf( + context.getString(R.string.nc_promote), + context.getString(R.string.nc_demote), + context.getString(R.string.nc_remove_participant) + ) + + if (participant.type == Participant.ParticipantType.MODERATOR) { + items.removeAt(0) + } else if (participant.type == Participant.ParticipantType.USER) { + items.removeAt(1) + } + + if (!conversation!!.canModerate(conversationUser)) { + items.removeAt(1); + } + + MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show { + cornerRadius(res = R.dimen.corner_radius) + title(text = participant.displayName) + listItems(items = items) { dialog, index, text -> + + if (index == 0) { + if (participant.type == Participant.ParticipantType.MODERATOR) { + ncApi.demoteModeratorToUser(credentials, ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token), participant.userId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + getListOfParticipants() + } + } else if (participant.type == Participant.ParticipantType.USER) { + ncApi.promoteUserToModerator(credentials, ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token), participant.userId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + getListOfParticipants() + } + } + } else if (index == 1) { + if (participant.type == Participant.ParticipantType.GUEST || + participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK) { + ncApi.removeParticipantFromConversation(credentials, ApiUtils.getUrlForRemovingParticipantFromConversation(conversationUser.baseUrl, conversation!!.token, true), participant.sessionId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + getListOfParticipants() + } + + } else { + ncApi.removeParticipantFromConversation(credentials, ApiUtils.getUrlForRemovingParticipantFromConversation(conversationUser.baseUrl, conversation!!.token, false), participant.userId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + getListOfParticipants() + // get participants again + } + } + } + } + } + } + + return true; + } + companion object { private const val ID_DELETE_CONVERSATION_DIALOG = 0 diff --git a/app/src/main/java/com/nextcloud/talk/events/EventStatus.java b/app/src/main/java/com/nextcloud/talk/events/EventStatus.java index c84ba1b54..ef3e72b15 100644 --- a/app/src/main/java/com/nextcloud/talk/events/EventStatus.java +++ b/app/src/main/java/com/nextcloud/talk/events/EventStatus.java @@ -35,7 +35,7 @@ public class EventStatus { } public enum EventType { - PUSH_REGISTRATION, CAPABILITIES_FETCH, SIGNALING_SETTINGS, CONVERSATION_UPDATE + PUSH_REGISTRATION, CAPABILITIES_FETCH, SIGNALING_SETTINGS, CONVERSATION_UPDATE, PARTICIPANTS_UPDATE } } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java b/app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java new file mode 100644 index 000000000..cb2d8cb4d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java @@ -0,0 +1,95 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2019 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.jobs; + +import android.content.Context; + +import com.nextcloud.talk.api.NcApi; +import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.events.EventStatus; +import com.nextcloud.talk.models.RetrofitBucket; +import com.nextcloud.talk.models.database.UserEntity; +import com.nextcloud.talk.utils.ApiUtils; +import com.nextcloud.talk.utils.bundle.BundleKeys; +import com.nextcloud.talk.utils.database.user.UserUtils; + +import org.greenrobot.eventbus.EventBus; + +import java.util.ArrayList; + +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import autodagger.AutoInjector; +import io.reactivex.schedulers.Schedulers; + +@AutoInjector(NextcloudTalkApplication.class) +public class AddParticipantsToConversation extends Worker { + @Inject + NcApi ncApi; + + @Inject + UserUtils userUtils; + + @Inject + EventBus eventBus; + + public AddParticipantsToConversation(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); + } + + @NonNull + @Override + public Result doWork() { + Data data = getInputData(); + String[] selectedUserIds = data.getStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_USERS()); + String[] selectedGroupIds = data.getStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_GROUPS()); + UserEntity user = userUtils.getUserWithInternalId(data.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), -1)); + String conversationToken = data.getString(BundleKeys.INSTANCE.getKEY_TOKEN()); + String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken()); + + RetrofitBucket retrofitBucket; + for (String userId : selectedUserIds) { + retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipant(user.getBaseUrl(), conversationToken, + userId); + + ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) + .subscribeOn(Schedulers.io()) + .blockingSubscribe(); + } + + for (String groupId : selectedGroupIds) { + retrofitBucket = ApiUtils.getRetrofitBucketForAddGroupParticipant(user.getBaseUrl(), conversationToken, + groupId); + + ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) + .subscribeOn(Schedulers.io()) + .blockingSubscribe(); + } + + eventBus.post(new EventStatus(user.getId(), EventStatus.EventType.PARTICIPANTS_UPDATE, true)); + return Result.success(); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 9261c807d..c83b53b1e 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -46,6 +46,16 @@ public class ApiUtils { return getRoom(baseUrl, token) + "/webinary/lobby"; } + public static String getUrlForRemovingParticipantFromConversation(String baseUrl, String roomToken, boolean isGuest) { + String url = getUrlForParticipants(baseUrl, roomToken); + + if (isGuest) { + url += "/guests"; + } + + return url; + } + public static RetrofitBucket getRetrofitBucketForContactsSearch(String baseUrl, @Nullable String searchQuery) { RetrofitBucket retrofitBucket = new RetrofitBucket(); retrofitBucket.setUrl(baseUrl + ocsApiVersion + "/apps/files_sharing/api/v1/sharees"); @@ -200,6 +210,10 @@ public class ApiUtils { } } + public static String getUrlForModerators(String baseUrl, String roomToken) { + return getRoom(baseUrl, roomToken) + "/moderators"; + } + public static String getUrlForSignalingSettings(String baseUrl) { return getUrlForSignaling(baseUrl, null) + "/settings"; } diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index 85cec858b..9a3be493b 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -21,6 +21,8 @@ package com.nextcloud.talk.utils.bundle object BundleKeys { + val KEY_SELECTED_USERS = "KEY_SELECTED_USERS"; + val KEY_SELECTED_GROUPS = "KEY_SELECTED_GROUPS"; val KEY_USERNAME = "KEY_USERNAME" val KEY_TOKEN = "KEY_TOKEN" val KEY_BASE_URL = "KEY_BASE_URL" @@ -36,6 +38,8 @@ object BundleKeys { val KEY_ROOM_TOKEN = "KEY_ROOM_TOKEN" val KEY_USER_ENTITY = "KEY_USER_ENTITY" val KEY_NEW_CONVERSATION = "KEY_NEW_CONVERSATION" + val KEY_ADD_PARTICIPANTS = "KEY_ADD_PARTICIPANTS" + val KEY_EXISTING_PARTICIPANTS = "KEY_EXISTING_PARTICIPANTS" val KEY_CALL_URL = "KEY_CALL_URL" val KEY_MODIFIED_BASE_URL = "KEY_MODIFIED_BASE_URL" val KEY_NOTIFICATION_SUBJECT = "KEY_NOTIFICATION_SUBJECT" diff --git a/app/src/main/java/com/nextcloud/talk/utils/ui/MaterialPreferenceCategoryWithRightLink.java b/app/src/main/java/com/nextcloud/talk/utils/ui/MaterialPreferenceCategoryWithRightLink.java new file mode 100644 index 000000000..4e4f4996b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ui/MaterialPreferenceCategoryWithRightLink.java @@ -0,0 +1,165 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2019 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.utils.ui; + +import android.content.Context; +import android.content.res.TypedArray; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.nextcloud.talk.R; +import com.yarolegovich.mp.util.Utils; + +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.cardview.widget.CardView; +import androidx.core.content.ContextCompat; + +public class MaterialPreferenceCategoryWithRightLink extends CardView { + + private ViewGroup container; + private TextView title; + private TextView action; + + public MaterialPreferenceCategoryWithRightLink(Context context) { + super(context); + init(null); + } + + public MaterialPreferenceCategoryWithRightLink(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs); + } + + public MaterialPreferenceCategoryWithRightLink(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs); + } + + private void init(AttributeSet attrs) { + int titleColor = -1; + String titleText = ""; + String actionText = ""; + if (attrs != null) { + TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.MaterialPreferenceCategory); + try { + if (ta.hasValue(R.styleable.MaterialPreferenceCategory_mpc_title)) { + titleText = ta.getString(R.styleable.MaterialPreferenceCategory_mpc_title); + } + + if (ta.hasValue(R.styleable.MaterialPreferenceCategory_mpc_action)) { + actionText = ta.getString(R.styleable.MaterialPreferenceCategory_mpc_action); + } + + titleColor = ta.getColor(R.styleable.MaterialPreferenceCategory_mpc_title_color, -1); + } finally { + ta.recycle(); + } + } + + inflate(getContext(), R.layout.category_with_right_action, this); + + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(0, 0, 0, Utils.dpToPixels(getContext(), 4)); + + setUseCompatPadding(true); + + setRadius(0); + + container = (ViewGroup) findViewById(R.id.mpc_container); + title = (TextView) findViewById(R.id.mpc_title); + action = findViewById(R.id.mpc_action); + + if (!TextUtils.isEmpty(titleText)) { + title.setVisibility(View.VISIBLE); + title.setText(titleText); + } + + if (!TextUtils.isEmpty(actionText)) { + action.setVisibility(View.VISIBLE); + action.setText(actionText); + } + + if (titleColor != -1) { + title.setTextColor(titleColor); + } + } + + public void setAction(String actionText) { + action.setText(actionText); + } + + public void setTitle(String titleText) { + title.setVisibility(View.VISIBLE); + title.setText(titleText); + } + + public void setTitleColor(@ColorInt int color) { + title.setTextColor(color); + } + + public void setTitleColorRes(@ColorRes int colorRes) { + title.setTextColor(ContextCompat.getColor(getContext(), colorRes)); + } + + @Override + public void addView(View child) { + if (container != null) { + container.addView(child); + } else { + super.addView(child); + } + } + + @Override + public void addView(View child, int index) { + if (container != null) { + container.addView(child, index); + } else { + super.addView(child, index); + } + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (container != null) { + container.addView(child, params); + } else { + super.addView(child, params); + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (container != null) { + container.addView(child, index, params); + } else { + super.addView(child, index, params); + } + } + +} diff --git a/app/src/main/res/layout/category_with_right_action.xml b/app/src/main/res/layout/category_with_right_action.xml new file mode 100644 index 000000000..39072a674 --- /dev/null +++ b/app/src/main/res/layout/category_with_right_action.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/controller_conversation_info.xml b/app/src/main/res/layout/controller_conversation_info.xml index 1156d0536..c2181be53 100644 --- a/app/src/main/res/layout/controller_conversation_info.xml +++ b/app/src/main/res/layout/controller_conversation_info.xml @@ -96,13 +96,14 @@ - @@ -112,7 +113,7 @@ android:layout_height="wrap_content" tools:listitem="@layout/rv_item_contact" /> - + + + + + + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 000000000..60fca1909 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 0505c5e3d..c2fba98f4 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -43,4 +43,5 @@ 192dp 80dp + 16dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0026751cd..9d203d22c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,6 +73,7 @@ Please confirm your intent to remove the current account. Remove account Add a new account + Add Only current account can be reauthorized Talk app is not installed on the server you tried to authenticate against Your already existing account was updated, instead of adding a new one @@ -225,6 +226,11 @@ Select authentication certificate Change authentication certificate + + Demote from moderator + Promote to moderator + Remove participant + Enter a message… Yesterday