Browse Source

Migrate ContactsController to kotlin + viewbinding

Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Andy Scherzinger 3 years ago
parent
commit
e6a78405ed

+ 0 - 1005
app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java

@@ -1,1005 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Mario Danic
- * @author Marcel Hibbe
- * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
- * Copyright (C) 2022 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.controllers;
-
-import static com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum.OPS_CODE_INVITE_USERS;
-import static com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM;
-
-import android.app.SearchManager;
-import android.content.Context;
-import android.graphics.PorterDuff;
-import android.os.Build;
-import android.os.Bundle;
-import android.text.InputType;
-import android.util.Log;
-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.ImageView;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
-
-import com.bluelinelabs.logansquare.LoganSquare;
-import com.nextcloud.talk.R;
-import com.nextcloud.talk.adapters.items.GenericTextHeaderItem;
-import com.nextcloud.talk.adapters.items.ContactItem;
-import com.nextcloud.talk.api.NcApi;
-import com.nextcloud.talk.application.NextcloudTalkApplication;
-import com.nextcloud.talk.controllers.base.BaseController;
-import com.nextcloud.talk.events.OpenConversationEvent;
-import com.nextcloud.talk.jobs.AddParticipantsToConversation;
-import com.nextcloud.talk.models.RetrofitBucket;
-import com.nextcloud.talk.models.database.CapabilitiesUtil;
-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.conversations.Conversation;
-import com.nextcloud.talk.models.json.conversations.RoomOverall;
-import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter;
-import com.nextcloud.talk.models.json.participants.Participant;
-import com.nextcloud.talk.ui.dialog.ContactsBottomDialog;
-import com.nextcloud.talk.utils.ApiUtils;
-import com.nextcloud.talk.utils.ConductorRemapping;
-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.jetbrains.annotations.NotNull;
-import org.parceler.Parcels;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-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.content.res.ResourcesCompat;
-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.flexibleadapter.FlexibleAdapter;
-import eu.davidea.flexibleadapter.SelectableAdapter;
-import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager;
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
-import io.reactivex.Observer;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
-import okhttp3.ResponseBody;
-
-@AutoInjector(NextcloudTalkApplication.class)
-public class ContactsController extends BaseController implements SearchView.OnQueryTextListener,
-    FlexibleAdapter.OnItemClickListener {
-
-    public static final String TAG = "ContactsController";
-
-    @Nullable
-    @BindView(R.id.initial_relative_layout)
-    RelativeLayout initialRelativeLayout;
-
-    @Nullable
-    @BindView(R.id.secondary_relative_layout)
-    RelativeLayout secondaryRelativeLayout;
-
-    @BindView(R.id.loading_content)
-    LinearLayout loadingContent;
-
-    @BindView(R.id.recycler_view)
-    RecyclerView recyclerView;
-
-    @BindView(R.id.swipe_refresh_layout)
-    SwipeRefreshLayout swipeRefreshLayout;
-
-    @BindView(R.id.call_header_layout)
-    RelativeLayout conversationPrivacyToogleLayout;
-
-    @BindView(R.id.joinConversationViaLinkRelativeLayout)
-    RelativeLayout joinConversationViaLinkLayout;
-
-    @BindView(R.id.joinConversationViaLinkImageView)
-    ImageView joinConversationViaLinkImageView;
-
-    @BindView(R.id.public_call_link)
-    ImageView publicCallLinkImageView;
-
-    @BindView(R.id.generic_rv_layout)
-    CoordinatorLayout genericRvLayout;
-
-    @Inject
-    UserUtils userUtils;
-
-    @Inject
-    EventBus eventBus;
-
-    @Inject
-    AppPreferences appPreferences;
-
-    @Inject
-    NcApi ncApi;
-
-    private String credentials;
-    private UserEntity currentUser;
-    private Disposable contactsQueryDisposable;
-    private Disposable cacheQueryDisposable;
-    private FlexibleAdapter adapter;
-    private List<AbstractFlexibleItem> contactItems;
-
-    private SmoothScrollLinearLayoutManager layoutManager;
-
-    private MenuItem searchItem;
-    private SearchView searchView;
-
-    private boolean isNewConversationView;
-    private boolean isPublicCall;
-
-    private HashMap<String, GenericTextHeaderItem> userHeaderItems = new HashMap<>();
-
-    private boolean alreadyFetching = false;
-
-    private MenuItem doneMenuItem;
-
-    private Set<String> selectedUserIds;
-    private Set<String> selectedGroupIds;
-    private Set<String> selectedCircleIds;
-    private Set<String> selectedEmails;
-    private List<String> existingParticipants;
-    private boolean isAddingParticipantsView;
-    private String conversationToken;
-
-    private ContactsBottomDialog contactsBottomDialog;
-
-    public ContactsController() {
-        super();
-        setHasOptionsMenu(true);
-    }
-
-    public ContactsController(Bundle args) {
-        super(args);
-        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());
-            }
-        }
-
-        selectedUserIds = new HashSet<>();
-        selectedGroupIds = new HashSet<>();
-        selectedEmails = new HashSet<>();
-        selectedCircleIds = new HashSet<>();
-    }
-
-    @Override
-    protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
-        return inflater.inflate(R.layout.controller_contacts_rv, container, false);
-    }
-
-    @Override
-    protected void onAttach(@NonNull View view) {
-        super.onAttach(view);
-        eventBus.register(this);
-
-        if (isNewConversationView) {
-            toggleNewCallHeaderVisibility(!isPublicCall);
-        }
-
-        if (isAddingParticipantsView) {
-            joinConversationViaLinkLayout.setVisibility(View.GONE);
-            conversationPrivacyToogleLayout.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    protected void onViewBound(@NonNull View view) {
-        super.onViewBound(view);
-        NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
-
-        currentUser = userUtils.getCurrentUser();
-
-        if (currentUser != null) {
-            credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken());
-        }
-
-        if (adapter == null) {
-            contactItems = new ArrayList<>();
-            adapter = new FlexibleAdapter<>(contactItems, getActivity(), false);
-
-            if (currentUser != null) {
-                fetchData();
-            }
-        }
-
-        setupAdapter();
-        prepareViews();
-    }
-
-    private void setupAdapter() {
-        adapter.setNotifyChangeOfUnfilteredItems(true)
-            .setMode(SelectableAdapter.Mode.MULTI);
-
-        adapter.setStickyHeaderElevation(5)
-            .setUnlinkAllItemsOnRemoveHeaders(true)
-            .setDisplayHeadersAtStartUp(true)
-            .setStickyHeaders(true);
-
-        adapter.addListener(this);
-    }
-
-    private void selectionDone() {
-        if (!isAddingParticipantsView) {
-            if (!isPublicCall && (selectedCircleIds.size() + selectedGroupIds.size() + selectedUserIds.size() == 1)) {
-                String userId;
-                String sourceType = null;
-                String roomType = "1";
-
-                if (selectedGroupIds.size() == 1) {
-                    roomType = "2";
-                    userId = selectedGroupIds.iterator().next();
-                } else if (selectedCircleIds.size() == 1) {
-                    roomType = "2";
-                    sourceType = "circles";
-                    userId = selectedCircleIds.iterator().next();
-                } else {
-                    userId = selectedUserIds.iterator().next();
-                }
-
-                int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[]{ApiUtils.APIv4, 1});
-                RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(apiVersion,
-                                                                                        currentUser.getBaseUrl(),
-                                                                                        roomType,
-                                                                                        sourceType,
-                                                                                        userId,
-                                                                                        null);
-                ncApi.createRoom(credentials,
-                                 retrofitBucket.getUrl(), retrofitBucket.getQueryMap())
-                    .subscribeOn(Schedulers.io())
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe(new Observer<RoomOverall>() {
-                        @Override
-                        public void onSubscribe(Disposable d) {
-
-                        }
-
-                        @Override
-                        public void onNext(RoomOverall roomOverall) {
-                            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());
-
-                            // FIXME once APIv2 or later is used only, the createRoom already returns all the data
-                            ncApi.getRoom(credentials,
-                                          ApiUtils.getUrlForRoom(apiVersion, currentUser.getBaseUrl(),
-                                                                 roomOverall.getOcs().getData().getToken()))
-                                .subscribeOn(Schedulers.io())
-                                .observeOn(AndroidSchedulers.mainThread())
-                                .subscribe(new Observer<RoomOverall>() {
-
-                                    @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() {
-
-                                    }
-                                });
-                        }
-
-                        @Override
-                        public void onError(Throwable e) {
-
-                        }
-
-                        @Override
-                        public void onComplete() {
-                        }
-                    });
-            } else {
-
-                Bundle bundle = new Bundle();
-                Conversation.ConversationType roomType;
-                if (isPublicCall) {
-                    roomType = Conversation.ConversationType.ROOM_PUBLIC_CALL;
-                } else {
-                    roomType = Conversation.ConversationType.ROOM_GROUP_CALL;
-                }
-
-                ArrayList<String> userIdsArray = new ArrayList<>(selectedUserIds);
-                ArrayList<String> groupIdsArray = new ArrayList<>(selectedGroupIds);
-                ArrayList<String> emailsArray = new ArrayList<>(selectedEmails);
-                ArrayList<String> circleIdsArray = new ArrayList<>(selectedCircleIds);
-
-
-                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.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_EMAIL(), emailsArray);
-                bundle.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_CIRCLE(), circleIdsArray);
-                bundle.putSerializable(BundleKeys.INSTANCE.getKEY_OPERATION_CODE(), OPS_CODE_INVITE_USERS);
-                prepareAndShowBottomSheetWithBundle(bundle);
-            }
-        } else {
-            String[] userIdsArray = selectedUserIds.toArray(new String[selectedUserIds.size()]);
-            String[] groupIdsArray = selectedGroupIds.toArray(new String[selectedGroupIds.size()]);
-            String[] emailsArray = selectedEmails.toArray(new String[selectedEmails.size()]);
-            String[] circleIdsArray = selectedCircleIds.toArray(new String[selectedCircleIds.size()]);
-
-            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);
-            data.putStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_EMAILS(), emailsArray);
-            data.putStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_CIRCLES(), circleIdsArray);
-
-            OneTimeWorkRequest addParticipantsToConversationWorker =
-                new OneTimeWorkRequest.Builder(AddParticipantsToConversation.class).setInputData(data.build()).build();
-            WorkManager.getInstance().enqueue(addParticipantsToConversationWorker);
-
-            getRouter().popCurrentController();
-        }
-    }
-
-    private void initSearchView() {
-        if (getActivity() != null) {
-            SearchManager searchManager = (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE);
-            if (searchItem != null) {
-                searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
-                searchView.setMaxWidth(Integer.MAX_VALUE);
-                searchView.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
-                int imeOptions = EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_FULLSCREEN;
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences.getIsKeyboardIncognito()) {
-                    imeOptions |= EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING;
-                }
-                searchView.setImeOptions(imeOptions);
-                searchView.setQueryHint(getResources().getString(R.string.nc_search));
-                if (searchManager != null) {
-                    searchView.setSearchableInfo(searchManager.getSearchableInfo(getActivity().getComponentName()));
-                }
-                searchView.setOnQueryTextListener(this);
-            }
-        }
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
-        int itemId = item.getItemId();
-        if (itemId == android.R.id.home) {
-            return getRouter().popCurrentController();
-        } else if (itemId == R.id.contacts_selection_done) {
-            selectionDone();
-            return true;
-        }
-        return super.onOptionsItemSelected(item);
-    }
-
-    @Override
-    public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
-        super.onCreateOptionsMenu(menu, inflater);
-        inflater.inflate(R.menu.menu_contacts, menu);
-        searchItem = menu.findItem(R.id.action_search);
-        doneMenuItem = menu.findItem(R.id.contacts_selection_done);
-
-        initSearchView();
-    }
-
-    @Override
-    public void onPrepareOptionsMenu(@NonNull Menu menu) {
-        super.onPrepareOptionsMenu(menu);
-        checkAndHandleDoneMenuItem();
-        if (adapter.hasFilter()) {
-            searchItem.expandActionView();
-            searchView.setQuery((CharSequence) adapter.getFilter(String.class), false);
-        }
-    }
-
-    private void fetchData() {
-        dispose(null);
-
-        alreadyFetching = true;
-        Set<AutocompleteUser> autocompleteUsersHashSet = new HashSet<>();
-
-        userHeaderItems = new HashMap<>();
-
-        String query = (String) adapter.getFilter(String.class);
-
-        RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser.getBaseUrl(), query);
-        Map<String, Object> modifiedQueryMap = new HashMap<String, Object>(retrofitBucket.getQueryMap());
-        modifiedQueryMap.put("limit", 50);
-
-        if (isAddingParticipantsView) {
-            modifiedQueryMap.put("itemId", conversationToken);
-        }
-
-        List<String> shareTypesList;
-
-        shareTypesList = new ArrayList<>();
-        // users
-        shareTypesList.add("0");
-        if (!isAddingParticipantsView) {
-            // groups
-            shareTypesList.add("1");
-        } else if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails")) {
-            // groups
-            shareTypesList.add("1");
-            // emails
-            shareTypesList.add("4");
-        }
-        if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "circles-support")) {
-            // circles
-            shareTypesList.add("7");
-        }
-
-        modifiedQueryMap.put("shareTypes[]", shareTypesList);
-
-        ncApi.getContactsWithSearchParam(
-            credentials,
-            retrofitBucket.getUrl(), shareTypesList, modifiedQueryMap)
-            .subscribeOn(Schedulers.io())
-            .observeOn(AndroidSchedulers.mainThread())
-            .retry(3)
-            .subscribe(new Observer<ResponseBody>() {
-                @Override
-                public void onSubscribe(@NotNull Disposable d) {
-                    contactsQueryDisposable = d;
-                }
-
-                @Override
-                public void onNext(@NotNull ResponseBody responseBody) {
-                    if (responseBody != null) {
-                        Participant participant;
-
-                        List<AbstractFlexibleItem> newUserItemList = new ArrayList<>();
-                        EnumActorTypeConverter actorTypeConverter = new EnumActorTypeConverter();
-
-                        try {
-                            AutocompleteOverall autocompleteOverall = LoganSquare.parse(
-                                responseBody.string(),
-                                AutocompleteOverall.class);
-                            autocompleteUsersHashSet.addAll(autocompleteOverall.getOcs().getData());
-
-                            for (AutocompleteUser autocompleteUser : autocompleteUsersHashSet) {
-                                if (!autocompleteUser.getId().equals(currentUser.getUserId())
-                                    && !existingParticipants.contains(autocompleteUser.getId())) {
-                                    participant = new Participant();
-                                    participant.setActorId(autocompleteUser.getId());
-                                    participant.setActorType(actorTypeConverter.getFromString(autocompleteUser.getSource()));
-                                    participant.setDisplayName(autocompleteUser.getLabel());
-                                    participant.setSource(autocompleteUser.getSource());
-
-                                    String headerTitle;
-
-                                    if (participant.getActorType() == Participant.ActorType.GROUPS) {
-                                        headerTitle = getResources().getString(R.string.nc_groups);
-                                    } else if (participant.getActorType() == Participant.ActorType.CIRCLES) {
-                                        headerTitle = getResources().getString(R.string.nc_circles);
-                                    } else {
-                                        headerTitle =
-                                            participant.getDisplayName().substring(0, 1).toUpperCase(Locale.getDefault());
-                                    }
-
-                                    GenericTextHeaderItem genericTextHeaderItem;
-                                    if (!userHeaderItems.containsKey(headerTitle)) {
-                                        genericTextHeaderItem = new GenericTextHeaderItem(headerTitle);
-                                        userHeaderItems.put(headerTitle, genericTextHeaderItem);
-                                    }
-
-                                    ContactItem newContactItem = new ContactItem(
-                                        participant,
-                                        currentUser,
-                                        userHeaderItems.get(headerTitle)
-                                    );
-
-                                    if (!contactItems.contains(newContactItem)) {
-                                        newUserItemList.add(newContactItem);
-                                    }
-                                }
-                            }
-                        } catch (IOException ioe) {
-                            Log.e(TAG, "Parsing response body failed while getting contacts", ioe);
-                        }
-
-                        userHeaderItems = new HashMap<>();
-                        contactItems.addAll(newUserItemList);
-
-                        Collections.sort(newUserItemList, (o1, o2) -> {
-                            String firstName;
-                            String secondName;
-
-                            if (o1 instanceof ContactItem) {
-                                firstName = ((ContactItem) o1).getModel().getDisplayName();
-                            } else {
-                                firstName = ((GenericTextHeaderItem) o1).getModel();
-                            }
-
-                            if (o2 instanceof ContactItem) {
-                                secondName = ((ContactItem) o2).getModel().getDisplayName();
-                            } else {
-                                secondName = ((GenericTextHeaderItem) o2).getModel();
-                            }
-
-                            if (o1 instanceof ContactItem && o2 instanceof ContactItem) {
-                                String firstSource = ((ContactItem) o1).getModel().getSource();
-                                String secondSource = ((ContactItem) o2).getModel().getSource();
-                                if (firstSource.equals(secondSource)) {
-                                    return firstName.compareToIgnoreCase(secondName);
-                                }
-
-                                // First users
-                                if ("users".equals(firstSource)) {
-                                    return -1;
-                                } else if ("users".equals(secondSource)) {
-                                    return 1;
-                                }
-
-                                // Then groups
-                                if ("groups".equals(firstSource)) {
-                                    return -1;
-                                } else if ("groups".equals(secondSource)) {
-                                    return 1;
-                                }
-
-                                // Then circles
-                                if ("circles".equals(firstSource)) {
-                                    return -1;
-                                } else if ("circles".equals(secondSource)) {
-                                    return 1;
-                                }
-
-                                // Otherwise fall back to name sorting
-                                return firstName.compareToIgnoreCase(secondName);
-                            }
-
-                            return firstName.compareToIgnoreCase(secondName);
-                        });
-
-                        Collections.sort(contactItems, (o1, o2) -> {
-                            String firstName;
-                            String secondName;
-
-                            if (o1 instanceof ContactItem) {
-                                firstName = ((ContactItem) o1).getModel().getDisplayName();
-                            } else {
-                                firstName = ((GenericTextHeaderItem) o1).getModel();
-                            }
-
-                            if (o2 instanceof ContactItem) {
-                                secondName = ((ContactItem) o2).getModel().getDisplayName();
-                            } else {
-                                secondName = ((GenericTextHeaderItem) o2).getModel();
-                            }
-
-                            if (o1 instanceof ContactItem && o2 instanceof ContactItem) {
-                                if ("groups".equals(((ContactItem) o1).getModel().getSource()) &&
-                                    "groups".equals(((ContactItem) o2).getModel().getSource())) {
-                                    return firstName.compareToIgnoreCase(secondName);
-                                } else if ("groups".equals(((ContactItem) o1).getModel().getSource())) {
-                                    return -1;
-                                } else if ("groups".equals(((ContactItem) o2).getModel().getSource())) {
-                                    return 1;
-                                }
-                            }
-
-                            return firstName.compareToIgnoreCase(secondName);
-                        });
-
-                        if (newUserItemList.size() > 0) {
-                            adapter.updateDataSet(newUserItemList);
-                        } else {
-                            adapter.filterItems();
-                        }
-
-                        if (swipeRefreshLayout != null) {
-                            swipeRefreshLayout.setRefreshing(false);
-                        }
-                    }
-                }
-
-                @Override
-                public void onError(@NotNull Throwable e) {
-                    if (swipeRefreshLayout != null) {
-                        swipeRefreshLayout.setRefreshing(false);
-                    }
-                    dispose(contactsQueryDisposable);
-                }
-
-                @Override
-                public void onComplete() {
-                    if (swipeRefreshLayout != null) {
-                        swipeRefreshLayout.setRefreshing(false);
-                    }
-                    dispose(contactsQueryDisposable);
-                    alreadyFetching = false;
-
-                    disengageProgressBar();
-                }
-            });
-    }
-
-    private void prepareViews() {
-        layoutManager = new SmoothScrollLinearLayoutManager(getActivity());
-        recyclerView.setLayoutManager(layoutManager);
-        recyclerView.setHasFixedSize(true);
-        recyclerView.setAdapter(adapter);
-
-        swipeRefreshLayout.setOnRefreshListener(this::fetchData);
-        swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary);
-        swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background);
-
-        joinConversationViaLinkImageView
-            .getBackground()
-            .setColorFilter(ResourcesCompat.getColor(getResources(), R.color.colorBackgroundDarker, null),
-                            PorterDuff.Mode.SRC_IN);
-
-        publicCallLinkImageView
-            .getBackground()
-            .setColorFilter(ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null),
-                            PorterDuff.Mode.SRC_IN);
-
-        disengageProgressBar();
-    }
-
-    private void disengageProgressBar() {
-        if (!alreadyFetching) {
-            loadingContent.setVisibility(View.GONE);
-            genericRvLayout.setVisibility(View.VISIBLE);
-
-            if (isNewConversationView) {
-                conversationPrivacyToogleLayout.setVisibility(View.VISIBLE);
-                joinConversationViaLinkLayout.setVisibility(View.VISIBLE);
-            }
-        }
-    }
-
-    private void dispose(@Nullable Disposable disposable) {
-        if (disposable != null && !disposable.isDisposed()) {
-            disposable.dispose();
-        } else if (disposable == null) {
-
-            if (contactsQueryDisposable != null && !contactsQueryDisposable.isDisposed()) {
-                contactsQueryDisposable.dispose();
-                contactsQueryDisposable = null;
-            }
-
-            if (cacheQueryDisposable != null && !cacheQueryDisposable.isDisposed()) {
-                cacheQueryDisposable.dispose();
-                cacheQueryDisposable = null;
-            }
-        }
-    }
-
-
-    @Override
-    public void onSaveViewState(@NonNull View view, @NonNull Bundle outState) {
-        adapter.onSaveInstanceState(outState);
-        super.onSaveViewState(view, outState);
-    }
-
-    @Override
-    public void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) {
-        super.onRestoreViewState(view, savedViewState);
-        if (adapter != null) {
-            adapter.onRestoreInstanceState(savedViewState);
-        }
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        dispose(null);
-    }
-
-    @Override
-    public boolean onQueryTextChange(String newText) {
-        if (!newText.equals("") && adapter.hasNewFilter(newText)) {
-            adapter.setFilter(newText);
-            fetchData();
-        } else if (newText.equals("")) {
-            adapter.setFilter("");
-            adapter.updateDataSet(contactItems);
-        }
-
-        if (swipeRefreshLayout != null) {
-            swipeRefreshLayout.setEnabled(!adapter.hasFilter());
-        }
-
-        return true;
-    }
-
-    @Override
-    public boolean onQueryTextSubmit(String query) {
-        return onQueryTextChange(query);
-    }
-
-    private void checkAndHandleDoneMenuItem() {
-        if (adapter != null && doneMenuItem != null) {
-            if ((selectedCircleIds.size() + selectedEmails.size() + selectedGroupIds.size() + selectedUserIds.size() > 0) || isPublicCall) {
-                doneMenuItem.setVisible(true);
-            } else {
-                doneMenuItem.setVisible(false);
-            }
-        } else if (doneMenuItem != null) {
-            doneMenuItem.setVisible(false);
-        }
-    }
-
-    @Override
-    protected String getTitle() {
-        if (isAddingParticipantsView) {
-            return getResources().getString(R.string.nc_add_participants);
-        } else if (isNewConversationView) {
-            return getResources().getString(R.string.nc_select_participants);
-        } else {
-            return getResources().getString(R.string.nc_app_product_name);
-        }
-    }
-
-    private void prepareAndShowBottomSheetWithBundle(Bundle bundle) {
-        // 11: create conversation-enter name for new conversation
-        // 10: get&join room when enter link
-        contactsBottomDialog = new ContactsBottomDialog(getActivity(), bundle);
-        contactsBottomDialog.show();
-    }
-
-
-    @Subscribe(threadMode = ThreadMode.MAIN)
-    public void onMessageEvent(OpenConversationEvent openConversationEvent) {
-        ConductorRemapping.INSTANCE.remapChatController(getRouter(), currentUser.getId(),
-                                                        openConversationEvent.getConversation().getToken(),
-                                                        openConversationEvent.getBundle(), true);
-        if (contactsBottomDialog != null) {
-            contactsBottomDialog.dismiss();
-        }
-    }
-
-    @Override
-    protected void onDetach(@NonNull View view) {
-        super.onDetach(view);
-        eventBus.unregister(this);
-    }
-
-    @Override
-    public boolean onItemClick(View view, int position) {
-        if (adapter.getItem(position) instanceof ContactItem) {
-            if (!isNewConversationView && !isAddingParticipantsView) {
-                ContactItem contactItem = (ContactItem) adapter.getItem(position);
-                String roomType = "1";
-
-                if ("groups".equals(contactItem.getModel().getSource())) {
-                    roomType = "2";
-                }
-
-                int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[]{ApiUtils.APIv4, 1});
-
-                RetrofitBucket retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(apiVersion,
-                                                                                        currentUser.getBaseUrl(),
-                                                                                        roomType,
-                                                                                        null,
-                                                                                        contactItem.getModel().getActorId(),
-                                                                                        null);
-
-                ncApi.createRoom(credentials,
-                                 retrofitBucket.getUrl(), retrofitBucket.getQueryMap())
-                    .subscribeOn(Schedulers.io())
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe(new Observer<RoomOverall>() {
-                        @Override
-                        public void onSubscribe(Disposable d) {
-
-                        }
-
-                        @Override
-                        public void onNext(RoomOverall roomOverall) {
-                            if (getActivity() != null) {
-                                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());
-                                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 {
-                Participant participant = ((ContactItem) adapter.getItem(position)).getModel();
-                participant.setSelected(!participant.isSelected());
-
-                if ("groups".equals(participant.getSource())) {
-                    if (participant.isSelected()) {
-                        selectedGroupIds.add(participant.getActorId());
-                    } else {
-                        selectedGroupIds.remove(participant.getActorId());
-                    }
-                } else if ("emails".equals(participant.getSource())) {
-                    if (participant.isSelected()) {
-                        selectedEmails.add(participant.getActorId());
-                    } else {
-                        selectedEmails.remove(participant.getActorId());
-                    }
-                } else if ("circles".equals(participant.getSource())) {
-                    if (participant.isSelected()) {
-                        selectedCircleIds.add(participant.getActorId());
-                    } else {
-                        selectedCircleIds.remove(participant.getActorId());
-                    }
-                } else {
-                    if (participant.isSelected()) {
-                        selectedUserIds.add(participant.getActorId());
-                    } else {
-                        selectedUserIds.remove(participant.getActorId());
-                    }
-                }
-
-                if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "last-room-activity")
-                    && !CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails") &&
-                    "groups".equals(((ContactItem) adapter.getItem(position)).getModel().getSource()) &&
-                    participant.isSelected() &&
-                    adapter.getSelectedItemCount() > 1) {
-                    List<ContactItem> currentItems = adapter.getCurrentItems();
-                    Participant internalParticipant;
-                    for (int i = 0; i < currentItems.size(); i++) {
-                        internalParticipant = currentItems.get(i).getModel();
-                        if (internalParticipant.getActorId().equals(participant.getActorId()) &&
-                            internalParticipant.getActorType() == Participant.ActorType.GROUPS &&
-                            internalParticipant.isSelected()) {
-                            internalParticipant.setSelected(false);
-                            selectedGroupIds.remove(internalParticipant.getActorId());
-                        }
-                    }
-
-                }
-
-                adapter.notifyDataSetChanged();
-                checkAndHandleDoneMenuItem();
-            }
-        }
-        return true;
-    }
-
-    @Optional
-    @OnClick(R.id.joinConversationViaLinkRelativeLayout)
-    void joinConversationViaLink() {
-        Bundle bundle = new Bundle();
-        bundle.putSerializable(BundleKeys.INSTANCE.getKEY_OPERATION_CODE(), OPS_CODE_GET_AND_JOIN_ROOM);
-
-        prepareAndShowBottomSheetWithBundle(bundle);
-    }
-
-    @Optional
-    @OnClick(R.id.call_header_layout)
-    void toggleCallHeader() {
-        toggleNewCallHeaderVisibility(isPublicCall);
-        isPublicCall = !isPublicCall;
-
-        if (isPublicCall) {
-            joinConversationViaLinkLayout.setVisibility(View.GONE);
-        } else {
-            joinConversationViaLinkLayout.setVisibility(View.VISIBLE);
-        }
-
-        if (isPublicCall) {
-            List<AbstractFlexibleItem> currentItems = adapter.getCurrentItems();
-            Participant internalParticipant;
-            for (int i = 0; i < currentItems.size(); i++) {
-                if (currentItems.get(i) instanceof ContactItem) {
-                    internalParticipant = ((ContactItem) currentItems.get(i)).getModel();
-                    if (internalParticipant.getActorType() == Participant.ActorType.GROUPS &&
-                        internalParticipant.isSelected()) {
-                        internalParticipant.setSelected(false);
-                        selectedGroupIds.remove(internalParticipant.getActorId());
-                    }
-                }
-            }
-        }
-
-        for (int i = 0; i < adapter.getItemCount(); i++) {
-            if (adapter.getItem(i) instanceof ContactItem) {
-                ContactItem contactItem = (ContactItem) adapter.getItem(i);
-                if ("groups".equals(contactItem.getModel().getSource())) {
-                    contactItem.setEnabled(!isPublicCall);
-                }
-            }
-        }
-
-        checkAndHandleDoneMenuItem();
-        adapter.notifyDataSetChanged();
-    }
-
-    private void toggleNewCallHeaderVisibility(boolean showInitialLayout) {
-        if (showInitialLayout) {
-            if (initialRelativeLayout != null) {
-                initialRelativeLayout.setVisibility(View.VISIBLE);
-            }
-            if (secondaryRelativeLayout != null) {
-                secondaryRelativeLayout.setVisibility(View.GONE);
-            }
-        } else {
-            if (initialRelativeLayout != null) {
-                initialRelativeLayout.setVisibility(View.GONE);
-            }
-            if (secondaryRelativeLayout != null) {
-                secondaryRelativeLayout.setVisibility(View.VISIBLE);
-            }
-        }
-    }
-}

+ 958 - 0
app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt

@@ -0,0 +1,958 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * @author Andy Scherzinger
+ * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.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.controllers
+
+import android.app.SearchManager
+import android.content.Context
+import android.graphics.PorterDuff
+import android.os.Build
+import android.os.Bundle
+import android.text.InputType
+import android.util.Log
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import androidx.appcompat.widget.SearchView
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.view.MenuItemCompat
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import autodagger.AutoInjector
+import com.bluelinelabs.logansquare.LoganSquare
+import com.nextcloud.talk.R
+import com.nextcloud.talk.adapters.items.ContactItem
+import com.nextcloud.talk.adapters.items.GenericTextHeaderItem
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.controllers.base.NewBaseController
+import com.nextcloud.talk.controllers.bottomsheet.ConversationOperationEnum
+import com.nextcloud.talk.controllers.util.viewBinding
+import com.nextcloud.talk.databinding.ControllerContactsRvBinding
+import com.nextcloud.talk.events.OpenConversationEvent
+import com.nextcloud.talk.jobs.AddParticipantsToConversation
+import com.nextcloud.talk.models.RetrofitBucket
+import com.nextcloud.talk.models.database.CapabilitiesUtil
+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.conversations.Conversation
+import com.nextcloud.talk.models.json.conversations.RoomOverall
+import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter
+import com.nextcloud.talk.models.json.participants.Participant
+import com.nextcloud.talk.ui.dialog.ContactsBottomDialog
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.ConductorRemapping
+import com.nextcloud.talk.utils.bundle.BundleKeys
+import com.nextcloud.talk.utils.database.user.UserUtils
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.SelectableAdapter
+import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import io.reactivex.Observer
+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 java.io.IOException
+import java.util.ArrayList
+import java.util.Collections
+import java.util.HashMap
+import java.util.HashSet
+import java.util.Locale
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class ContactsController(args: Bundle) :
+    NewBaseController(R.layout.controller_contacts_rv),
+    SearchView.OnQueryTextListener,
+    FlexibleAdapter.OnItemClickListener {
+    private val binding: ControllerContactsRvBinding by viewBinding(ControllerContactsRvBinding::bind)
+
+    @Inject
+    lateinit var userUtils: UserUtils
+
+    @Inject
+    lateinit var eventBus: EventBus
+
+    @Inject
+    lateinit var ncApi: NcApi
+
+    private var credentials: String? = null
+    private var currentUser: UserEntity? = null
+    private var contactsQueryDisposable: Disposable? = null
+    private var cacheQueryDisposable: Disposable? = null
+    private var adapter: FlexibleAdapter<*>? = null
+    private var contactItems: MutableList<AbstractFlexibleItem<*>>? = null
+    private var layoutManager: SmoothScrollLinearLayoutManager? = null
+    private var searchItem: MenuItem? = null
+    private var searchView: SearchView? = null
+    private var isNewConversationView = false
+    private var isPublicCall = false
+    private var userHeaderItems: HashMap<String, GenericTextHeaderItem> = HashMap<String, GenericTextHeaderItem>()
+    private var alreadyFetching = false
+    private var doneMenuItem: MenuItem? = null
+    private var selectedUserIds: MutableSet<String> = HashSet()
+    private var selectedGroupIds: MutableSet<String> = HashSet()
+    private var selectedCircleIds: MutableSet<String> = HashSet()
+    private var selectedEmails: MutableSet<String> = HashSet()
+    private var existingParticipants: List<String>? = null
+    private var isAddingParticipantsView = false
+    private var conversationToken: String? = null
+    private var contactsBottomDialog: ContactsBottomDialog? = null
+
+    init {
+        setHasOptionsMenu(true)
+        sharedApplication!!.componentApplication.inject(this)
+
+        if (args.containsKey(BundleKeys.KEY_NEW_CONVERSATION)) {
+            isNewConversationView = true
+            existingParticipants = ArrayList()
+        } else if (args.containsKey(BundleKeys.KEY_ADD_PARTICIPANTS)) {
+            isAddingParticipantsView = true
+            conversationToken = args.getString(BundleKeys.KEY_TOKEN)
+            existingParticipants = ArrayList()
+            if (args.containsKey(BundleKeys.KEY_EXISTING_PARTICIPANTS)) {
+                existingParticipants = args.getStringArrayList(BundleKeys.KEY_EXISTING_PARTICIPANTS)
+            }
+        }
+        selectedUserIds = HashSet()
+        selectedGroupIds = HashSet()
+        selectedEmails = HashSet()
+        selectedCircleIds = HashSet()
+    }
+
+    override fun onAttach(view: View) {
+        super.onAttach(view)
+        eventBus.register(this)
+        if (isNewConversationView) {
+            toggleNewCallHeaderVisibility(!isPublicCall)
+        }
+        if (isAddingParticipantsView) {
+            binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.GONE
+            binding.conversationPrivacyToggle.callHeaderLayout.visibility = View.GONE
+        } else {
+            binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.setOnClickListener {
+                joinConversationViaLink()
+            }
+            binding.conversationPrivacyToggle.callHeaderLayout.setOnClickListener {
+                toggleCallHeader()
+            }
+        }
+    }
+
+    override fun onViewBound(view: View) {
+        super.onViewBound(view)
+        currentUser = userUtils.currentUser
+        if (currentUser != null) {
+            credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
+        }
+        if (adapter == null) {
+            contactItems = ArrayList<AbstractFlexibleItem<*>>()
+            adapter = FlexibleAdapter(contactItems, activity, false)
+            if (currentUser != null) {
+                fetchData()
+            }
+        }
+        setupAdapter()
+        prepareViews()
+    }
+
+    private fun setupAdapter() {
+        adapter?.setNotifyChangeOfUnfilteredItems(true)?.mode = SelectableAdapter.Mode.MULTI
+        adapter?.setStickyHeaderElevation(HEADER_ELEVATION)
+            ?.setUnlinkAllItemsOnRemoveHeaders(true)
+            ?.setDisplayHeadersAtStartUp(true)
+            ?.setStickyHeaders(true)
+        adapter?.addListener(this)
+    }
+
+    private fun selectionDone() {
+        if (!isAddingParticipantsView) {
+            if (!isPublicCall && selectedCircleIds.size + selectedGroupIds.size + selectedUserIds.size == 1) {
+                val userId: String
+                var sourceType: String? = null
+                var roomType = "1"
+                when {
+                    selectedGroupIds.size == 1 -> {
+                        roomType = "2"
+                        userId = selectedGroupIds.iterator().next()
+                    }
+                    selectedCircleIds.size == 1 -> {
+                        roomType = "2"
+                        sourceType = "circles"
+                        userId = selectedCircleIds.iterator().next()
+                    }
+                    else -> {
+                        userId = selectedUserIds.iterator().next()
+                    }
+                }
+                createRoom(roomType, sourceType, userId)
+            } else {
+                val bundle = Bundle()
+                val roomType: Conversation.ConversationType = if (isPublicCall) {
+                    Conversation.ConversationType.ROOM_PUBLIC_CALL
+                } else {
+                    Conversation.ConversationType.ROOM_GROUP_CALL
+                }
+                val userIdsArray = ArrayList(selectedUserIds)
+                val groupIdsArray = ArrayList(selectedGroupIds)
+                val emailsArray = ArrayList(selectedEmails)
+                val circleIdsArray = ArrayList(selectedCircleIds)
+                bundle.putParcelable(BundleKeys.KEY_CONVERSATION_TYPE, Parcels.wrap(roomType))
+                bundle.putStringArrayList(BundleKeys.KEY_INVITED_PARTICIPANTS, userIdsArray)
+                bundle.putStringArrayList(BundleKeys.KEY_INVITED_GROUP, groupIdsArray)
+                bundle.putStringArrayList(BundleKeys.KEY_INVITED_EMAIL, emailsArray)
+                bundle.putStringArrayList(BundleKeys.KEY_INVITED_CIRCLE, circleIdsArray)
+                bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_INVITE_USERS)
+                prepareAndShowBottomSheetWithBundle(bundle)
+            }
+        } else {
+            addParticipantsToConversation()
+        }
+    }
+
+    private fun createRoom(roomType: String, sourceType: String?, userId: String) {
+        val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
+        val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
+            apiVersion,
+            currentUser!!.baseUrl,
+            roomType,
+            sourceType,
+            userId,
+            null
+        )
+        ncApi.createRoom(
+            credentials,
+            retrofitBucket.getUrl(), retrofitBucket.getQueryMap()
+        )
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<RoomOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+                override fun onNext(roomOverall: RoomOverall) {
+                    val bundle = Bundle()
+                    bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, currentUser)
+                    bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken())
+                    bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId())
+
+                    // FIXME once APIv2 or later is used only, the createRoom already returns all the data
+                    ncApi.getRoom(
+                        credentials,
+                        ApiUtils.getUrlForRoom(
+                            apiVersion, currentUser!!.baseUrl,
+                            roomOverall.getOcs().getData().getToken()
+                        )
+                    )
+                        .subscribeOn(Schedulers.io())
+                        .observeOn(AndroidSchedulers.mainThread())
+                        .subscribe(object : Observer<RoomOverall> {
+                            override fun onSubscribe(d: Disposable) {
+                                // unused atm
+                            }
+                            override fun onNext(roomOverall: RoomOverall) {
+                                bundle.putParcelable(
+                                    BundleKeys.KEY_ACTIVE_CONVERSATION,
+                                    Parcels.wrap(roomOverall.getOcs().getData())
+                                )
+                                ConductorRemapping.remapChatController(
+                                    router, currentUser!!.id,
+                                    roomOverall.getOcs().getData().getToken(), bundle, true
+                                )
+                            }
+
+                            override fun onError(e: Throwable) {
+                                // unused atm
+                            }
+
+                            override fun onComplete() {
+                                // unused atm
+                            }
+                        })
+                }
+
+                override fun onError(e: Throwable) {
+                    // unused atm
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun addParticipantsToConversation() {
+        val userIdsArray: Array<String> = selectedUserIds.toTypedArray<String>()
+        val groupIdsArray: Array<String> = selectedGroupIds.toTypedArray<String>()
+        val emailsArray: Array<String> = selectedEmails.toTypedArray<String>()
+        val circleIdsArray: Array<String> = selectedCircleIds.toTypedArray<String>()
+        val data = Data.Builder()
+        data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, currentUser!!.id)
+        data.putString(BundleKeys.KEY_TOKEN, conversationToken)
+        data.putStringArray(BundleKeys.KEY_SELECTED_USERS, userIdsArray)
+        data.putStringArray(BundleKeys.KEY_SELECTED_GROUPS, groupIdsArray)
+        data.putStringArray(BundleKeys.KEY_SELECTED_EMAILS, emailsArray)
+        data.putStringArray(BundleKeys.KEY_SELECTED_CIRCLES, circleIdsArray)
+        val addParticipantsToConversationWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(
+            AddParticipantsToConversation::class.java
+        ).setInputData(data.build()).build()
+        WorkManager.getInstance().enqueue(addParticipantsToConversationWorker)
+        router.popCurrentController()
+    }
+
+    private fun initSearchView() {
+        if (activity != null) {
+            val searchManager: SearchManager? = activity?.getSystemService(Context.SEARCH_SERVICE) as SearchManager?
+            if (searchItem != null) {
+                searchView = MenuItemCompat.getActionView(searchItem) as SearchView
+                searchView!!.maxWidth = Int.MAX_VALUE
+                searchView!!.inputType = InputType.TYPE_TEXT_VARIATION_FILTER
+                var imeOptions: Int = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
+                    appPreferences?.isKeyboardIncognito == true
+                ) {
+                    imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
+                }
+                searchView!!.imeOptions = imeOptions
+                searchView!!.queryHint = resources!!.getString(R.string.nc_search)
+                if (searchManager != null) {
+                    searchView!!.setSearchableInfo(searchManager.getSearchableInfo(activity?.componentName))
+                }
+                searchView!!.setOnQueryTextListener(this)
+            }
+        }
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        val itemId = item.itemId
+        if (itemId == R.id.home) {
+            return router.popCurrentController()
+        } else if (itemId == R.id.contacts_selection_done) {
+            selectionDone()
+            return true
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        super.onCreateOptionsMenu(menu, inflater)
+        inflater.inflate(R.menu.menu_contacts, menu)
+        searchItem = menu.findItem(R.id.action_search)
+        doneMenuItem = menu.findItem(R.id.contacts_selection_done)
+        initSearchView()
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu) {
+        super.onPrepareOptionsMenu(menu)
+        checkAndHandleDoneMenuItem()
+        if (adapter?.hasFilter() == true) {
+            searchItem!!.expandActionView()
+            searchView!!.setQuery(adapter!!.getFilter(String::class.java) as CharSequence, false)
+        }
+    }
+
+    @Suppress("Detekt.TooGenericExceptionCaught")
+    private fun fetchData() {
+        dispose(null)
+        alreadyFetching = true
+        userHeaderItems = HashMap<String, GenericTextHeaderItem>()
+        val query = adapter!!.getFilter(String::class.java) as String?
+        val retrofitBucket: RetrofitBucket =
+            ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser!!.baseUrl, query)
+        val modifiedQueryMap: HashMap<String, Any?> = HashMap<String, Any?>(retrofitBucket.getQueryMap())
+        modifiedQueryMap.put("limit", CONTACTS_BATCH_SIZE)
+        if (isAddingParticipantsView) {
+            modifiedQueryMap.put("itemId", conversationToken)
+        }
+        val shareTypesList: ArrayList<String> = ArrayList()
+        // users
+        shareTypesList.add("0")
+        if (!isAddingParticipantsView) {
+            // groups
+            shareTypesList.add("1")
+        } else if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails")) {
+            // groups
+            shareTypesList.add("1")
+            // emails
+            shareTypesList.add("4")
+        }
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "circles-support")) {
+            // circles
+            shareTypesList.add("7")
+        }
+        modifiedQueryMap.put("shareTypes[]", shareTypesList)
+        ncApi.getContactsWithSearchParam(
+            credentials,
+            retrofitBucket.getUrl(), shareTypesList, modifiedQueryMap
+        )
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .retry(RETRIES)
+            .subscribe(object : Observer<ResponseBody> {
+                override fun onSubscribe(d: Disposable) {
+                    contactsQueryDisposable = d
+                }
+
+                override fun onNext(responseBody: ResponseBody) {
+                    val newUserItemList = processAutocompleteUserList(responseBody)
+
+                    userHeaderItems = HashMap<String, GenericTextHeaderItem>()
+                    contactItems!!.addAll(newUserItemList)
+
+                    sortUserItems(newUserItemList)
+
+                    if (newUserItemList.size > 0) {
+                        adapter?.updateDataSet(newUserItemList as List<Nothing>?)
+                    } else {
+                        adapter?.filterItems()
+                    }
+
+                    try {
+                        binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
+                    } catch (npe: NullPointerException) {
+                        // view binding can be null
+                        // since this is called asynchronously and UI might have been destroyed in the meantime
+                        Log.i(TAG, "UI destroyed - view binding already gone")
+                    }
+                }
+
+                override fun onError(e: Throwable) {
+                    try {
+                        binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
+                    } catch (npe: NullPointerException) {
+                        // view binding can be null
+                        // since this is called asynchronously and UI might have been destroyed in the meantime
+                        Log.i(TAG, "UI destroyed - view binding already gone")
+                    }
+                    dispose(contactsQueryDisposable)
+                }
+
+                override fun onComplete() {
+                    try {
+                        binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = false
+                    } catch (npe: NullPointerException) {
+                        // view binding can be null
+                        // since this is called asynchronously and UI might have been destroyed in the meantime
+                        Log.i(TAG, "UI destroyed - view binding already gone")
+                    }
+                    dispose(contactsQueryDisposable)
+                    alreadyFetching = false
+                    disengageProgressBar()
+                }
+            })
+    }
+
+    private fun processAutocompleteUserList(responseBody: ResponseBody) : MutableList<AbstractFlexibleItem<*>> {
+        try {
+            val autocompleteOverall: AutocompleteOverall = LoganSquare.parse<AutocompleteOverall>(
+                responseBody.string(),
+                AutocompleteOverall::class.java
+            )
+            val autocompleteUsersList: ArrayList<AutocompleteUser> = ArrayList<AutocompleteUser>()
+            autocompleteUsersList.addAll(autocompleteOverall.ocs!!.data!!)
+            return processAutocompleteUserList(autocompleteUsersList)
+        } catch (ioe: IOException) {
+            Log.e(TAG, "Parsing response body failed while getting contacts", ioe)
+        }
+
+        return ArrayList<AbstractFlexibleItem<*>>()
+    }
+
+    private fun processAutocompleteUserList(
+        autocompleteUsersList: ArrayList<AutocompleteUser>
+    ): MutableList<AbstractFlexibleItem<*>> {
+        var participant: Participant
+        val actorTypeConverter = EnumActorTypeConverter()
+        val newUserItemList: MutableList<AbstractFlexibleItem<*>> = ArrayList<AbstractFlexibleItem<*>>()
+        for (autocompleteUser in autocompleteUsersList) {
+            if (autocompleteUser.id != currentUser!!.userId &&
+                !existingParticipants!!.contains(autocompleteUser.id!!)
+            ) {
+                participant = createParticipant(autocompleteUser, actorTypeConverter)
+                val headerTitle = getHeaderTitle(participant)
+                var genericTextHeaderItem: GenericTextHeaderItem
+                if (!userHeaderItems.containsKey(headerTitle)) {
+                    genericTextHeaderItem = GenericTextHeaderItem(headerTitle)
+                    userHeaderItems.put(headerTitle, genericTextHeaderItem)
+                }
+                val newContactItem = ContactItem(
+                    participant,
+                    currentUser,
+                    userHeaderItems[headerTitle]
+                )
+                if (!contactItems!!.contains(newContactItem)) {
+                    newUserItemList.add(newContactItem)
+                }
+            }
+        }
+        return newUserItemList
+    }
+
+    private fun getHeaderTitle(participant: Participant): String {
+        return when {
+            participant.getActorType() == Participant.ActorType.GROUPS -> {
+                resources!!.getString(R.string.nc_groups)
+            }
+            participant.getActorType() == Participant.ActorType.CIRCLES -> {
+                resources!!.getString(R.string.nc_circles)
+            }
+            else -> {
+                participant.getDisplayName().substring(0, 1).toUpperCase(Locale.getDefault())
+            }
+        }
+    }
+
+    private fun createParticipant(
+        autocompleteUser: AutocompleteUser,
+        actorTypeConverter: EnumActorTypeConverter
+    ): Participant {
+        val participant = Participant()
+        participant.setActorId(autocompleteUser.id)
+        participant.setActorType(actorTypeConverter.getFromString(autocompleteUser.source))
+        participant.setDisplayName(autocompleteUser.label)
+        participant.setSource(autocompleteUser.source)
+
+        return participant
+    }
+
+    private fun sortUserItems(newUserItemList: MutableList<AbstractFlexibleItem<*>>) {
+        Collections.sort(
+            newUserItemList,
+            { o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
+                val firstName: String = if (o1 is ContactItem) {
+                    (o1 as ContactItem).model.getDisplayName()
+                } else {
+                    (o1 as GenericTextHeaderItem).model
+                }
+                val secondName: String = if (o2 is ContactItem) {
+                    (o2 as ContactItem).model.getDisplayName()
+                } else {
+                    (o2 as GenericTextHeaderItem).model
+                }
+                if (o1 is ContactItem && o2 is ContactItem) {
+                    val firstSource: String = (o1 as ContactItem).model.getSource()
+                    val secondSource: String = (o2 as ContactItem).model.getSource()
+                    if (firstSource == secondSource) {
+                        return@sort firstName.compareTo(secondName, ignoreCase = true)
+                    }
+
+                    // First users
+                    if ("users" == firstSource) {
+                        return@sort -1
+                    } else if ("users" == secondSource) {
+                        return@sort 1
+                    }
+
+                    // Then groups
+                    if ("groups" == firstSource) {
+                        return@sort -1
+                    } else if ("groups" == secondSource) {
+                        return@sort 1
+                    }
+
+                    // Then circles
+                    if ("circles" == firstSource) {
+                        return@sort -1
+                    } else if ("circles" == secondSource) {
+                        return@sort 1
+                    }
+
+                    // Otherwise fall back to name sorting
+                    return@sort firstName.compareTo(secondName, ignoreCase = true)
+                }
+                firstName.compareTo(secondName, ignoreCase = true)
+            }
+        )
+
+        Collections.sort(
+            contactItems
+        ) { o1: AbstractFlexibleItem<*>, o2: AbstractFlexibleItem<*> ->
+            val firstName: String = if (o1 is ContactItem) {
+                (o1 as ContactItem).model.getDisplayName()
+            } else {
+                (o1 as GenericTextHeaderItem).model
+            }
+            val secondName: String = if (o2 is ContactItem) {
+                (o2 as ContactItem).model.getDisplayName()
+            } else {
+                (o2 as GenericTextHeaderItem).model
+            }
+            if (o1 is ContactItem && o2 is ContactItem) {
+                if ("groups" == (o1 as ContactItem).model.getSource() &&
+                    "groups" == (o2 as ContactItem).model.getSource()
+                ) {
+                    return@sort firstName.compareTo(secondName, ignoreCase = true)
+                } else if ("groups" == (o1 as ContactItem).model.getSource()) {
+                    return@sort -1
+                } else if ("groups" == (o2 as ContactItem).model.getSource()) {
+                    return@sort 1
+                }
+            }
+            firstName.compareTo(secondName, ignoreCase = true)
+        }
+    }
+
+    private fun prepareViews() {
+        layoutManager = SmoothScrollLinearLayoutManager(activity)
+        binding.controllerGenericRv.recyclerView.layoutManager = layoutManager
+        binding.controllerGenericRv.recyclerView.setHasFixedSize(true)
+        binding.controllerGenericRv.recyclerView.adapter = adapter
+        binding.controllerGenericRv.swipeRefreshLayout.setOnRefreshListener { fetchData() }
+        binding.controllerGenericRv.swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary)
+        binding.controllerGenericRv.swipeRefreshLayout
+            .setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background)
+        binding.joinConversationViaLink.joinConversationViaLinkImageView
+            .background
+            .setColorFilter(
+                ResourcesCompat.getColor(resources!!, R.color.colorBackgroundDarker, null),
+                PorterDuff.Mode.SRC_IN
+            )
+        binding.conversationPrivacyToggle.publicCallLink
+            .background
+            .setColorFilter(
+                ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null),
+                PorterDuff.Mode.SRC_IN
+            )
+        disengageProgressBar()
+    }
+
+    private fun disengageProgressBar() {
+        if (!alreadyFetching) {
+            binding.loadingContent.visibility = View.GONE
+            binding.controllerGenericRv.root.visibility = View.VISIBLE
+            if (isNewConversationView) {
+                binding.conversationPrivacyToggle.callHeaderLayout.visibility = View.VISIBLE
+                binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.VISIBLE
+            }
+        }
+    }
+
+    private fun dispose(disposable: Disposable?) {
+        if (disposable != null && !disposable.isDisposed) {
+            disposable.dispose()
+        } else if (disposable == null) {
+            if (contactsQueryDisposable != null && !contactsQueryDisposable!!.isDisposed) {
+                contactsQueryDisposable!!.dispose()
+                contactsQueryDisposable = null
+            }
+            if (cacheQueryDisposable != null && !cacheQueryDisposable!!.isDisposed) {
+                cacheQueryDisposable!!.dispose()
+                cacheQueryDisposable = null
+            }
+        }
+    }
+
+    override fun onSaveViewState(view: View, outState: Bundle) {
+        adapter?.onSaveInstanceState(outState)
+        super.onSaveViewState(view, outState)
+    }
+
+    override fun onRestoreViewState(view: View, savedViewState: Bundle) {
+        super.onRestoreViewState(view, savedViewState)
+        if (adapter != null) {
+            adapter?.onRestoreInstanceState(savedViewState)
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        dispose(null)
+    }
+
+    override fun onQueryTextChange(newText: String): Boolean {
+        if (newText != "" && adapter?.hasNewFilter(newText) == true) {
+            adapter?.setFilter(newText)
+            fetchData()
+        } else if (newText == "") {
+            adapter?.setFilter("")
+            adapter?.updateDataSet(contactItems as List<Nothing>?)
+        }
+
+        try {
+            binding.controllerGenericRv.swipeRefreshLayout.isRefreshing = !adapter!!.hasFilter()
+        } catch (npe: NullPointerException) {
+            // view binding can be null
+            // since this is called asynchronously and UI might have been destroyed in the meantime
+            Log.i(TAG, "UI destroyed - view binding already gone")
+        }
+
+        return true
+    }
+
+    override fun onQueryTextSubmit(query: String): Boolean {
+        return onQueryTextChange(query)
+    }
+
+    private fun checkAndHandleDoneMenuItem() {
+        if (adapter != null && doneMenuItem != null) {
+            doneMenuItem!!.isVisible =
+                selectedCircleIds.size + selectedEmails.size + selectedGroupIds.size + selectedUserIds.size > 0 ||
+                isPublicCall
+        } else if (doneMenuItem != null) {
+            doneMenuItem!!.isVisible = false
+        }
+    }
+
+    override val title: String
+        get() = when {
+            isAddingParticipantsView -> {
+                resources!!.getString(R.string.nc_add_participants)
+            }
+            isNewConversationView -> {
+                resources!!.getString(R.string.nc_select_participants)
+            }
+            else -> {
+                resources!!.getString(R.string.nc_app_product_name)
+            }
+        }
+
+    private fun prepareAndShowBottomSheetWithBundle(bundle: Bundle) {
+        // 11: create conversation-enter name for new conversation
+        // 10: get&join room when enter link
+        contactsBottomDialog = ContactsBottomDialog(activity!!, bundle)
+        contactsBottomDialog?.show()
+    }
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    fun onMessageEvent(openConversationEvent: OpenConversationEvent) {
+        ConductorRemapping.remapChatController(
+            router, currentUser!!.id,
+            openConversationEvent.conversation!!.getToken(),
+            openConversationEvent.bundle!!, true
+        )
+        contactsBottomDialog?.dismiss()
+    }
+
+    override fun onDetach(view: View) {
+        super.onDetach(view)
+        eventBus.unregister(this)
+    }
+
+    override fun onItemClick(view: View, position: Int): Boolean {
+        if (adapter?.getItem(position) is ContactItem) {
+            if (!isNewConversationView && !isAddingParticipantsView) {
+                createRoom(adapter?.getItem(position) as ContactItem)
+            } else {
+                val participant: Participant = (adapter?.getItem(position) as ContactItem).model
+                updateSelection((adapter?.getItem(position) as ContactItem))
+            }
+        }
+        return true
+    }
+
+    private fun updateSelection(contactItem: ContactItem) {
+        contactItem.model.isSelected = !contactItem.model.isSelected
+        updateSelectionLists(contactItem.model)
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "last-room-activity") &&
+            !CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails") &&
+            isValidGroupSelection(contactItem, contactItem.model, adapter)
+        ) {
+            val currentItems: List<ContactItem> = adapter?.currentItems as List<ContactItem>
+            var internalParticipant: Participant
+            for (i in currentItems.indices) {
+                internalParticipant = currentItems[i].model
+                if (internalParticipant.getActorId() == contactItem.model.getActorId() &&
+                    internalParticipant.getActorType() == Participant.ActorType.GROUPS &&
+                    internalParticipant.isSelected
+                ) {
+                    internalParticipant.isSelected = false
+                    selectedGroupIds.remove(internalParticipant.getActorId())
+                }
+            }
+        }
+        adapter?.notifyDataSetChanged()
+        checkAndHandleDoneMenuItem()
+    }
+
+    private fun createRoom(contactItem: ContactItem) {
+        var roomType = "1"
+        if ("groups" == contactItem.model.getSource()) {
+            roomType = "2"
+        }
+        val apiVersion: Int = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
+        val retrofitBucket: RetrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
+            apiVersion,
+            currentUser!!.baseUrl,
+            roomType,
+            null,
+            contactItem.model.getActorId(),
+            null
+        )
+        ncApi.createRoom(
+            credentials,
+            retrofitBucket.getUrl(), retrofitBucket.getQueryMap()
+        )
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<RoomOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+                override fun onNext(roomOverall: RoomOverall) {
+                    if (activity != null) {
+                        val bundle = Bundle()
+                        bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, currentUser)
+                        bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken())
+                        bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId())
+                        bundle.putParcelable(
+                            BundleKeys.KEY_ACTIVE_CONVERSATION,
+                            Parcels.wrap(roomOverall.getOcs().getData())
+                        )
+                        ConductorRemapping.remapChatController(
+                            router,
+                            currentUser!!.id,
+                            roomOverall.getOcs().getData().getToken(),
+                            bundle,
+                            true
+                        )
+                    }
+                }
+
+                override fun onError(e: Throwable) {
+                    // unused atm
+                }
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun updateSelectionLists(participant: Participant) {
+        if ("groups" == participant.getSource()) {
+            if (participant.isSelected) {
+                selectedGroupIds.add(participant.getActorId())
+            } else {
+                selectedGroupIds.remove(participant.getActorId())
+            }
+        } else if ("emails" == participant.getSource()) {
+            if (participant.isSelected) {
+                selectedEmails.add(participant.getActorId())
+            } else {
+                selectedEmails.remove(participant.getActorId())
+            }
+        } else if ("circles" == participant.getSource()) {
+            if (participant.isSelected) {
+                selectedCircleIds.add(participant.getActorId())
+            } else {
+                selectedCircleIds.remove(participant.getActorId())
+            }
+        } else {
+            if (participant.isSelected) {
+                selectedUserIds.add(participant.getActorId())
+            } else {
+                selectedUserIds.remove(participant.getActorId())
+            }
+        }
+    }
+
+    private fun isValidGroupSelection(
+        contactItem: ContactItem,
+        participant: Participant,
+        adapter: FlexibleAdapter<*>?
+    ): Boolean {
+        return "groups" == contactItem.model.getSource() && participant.isSelected && adapter?.selectedItemCount!! > 1
+    }
+
+    private fun joinConversationViaLink() {
+        val bundle = Bundle()
+        bundle.putSerializable(BundleKeys.KEY_OPERATION_CODE, ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM)
+        prepareAndShowBottomSheetWithBundle(bundle)
+    }
+
+    private fun toggleCallHeader() {
+        toggleNewCallHeaderVisibility(isPublicCall)
+        isPublicCall = !isPublicCall
+
+        if (isPublicCall) {
+            binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.GONE
+            updateGroupParticipantSelection()
+        } else {
+            binding.joinConversationViaLink.joinConversationViaLinkRelativeLayout.visibility = View.VISIBLE
+        }
+
+        enableContactForNonPublicCall()
+        checkAndHandleDoneMenuItem()
+        adapter?.notifyDataSetChanged()
+    }
+
+    private fun updateGroupParticipantSelection() {
+        val currentItems: List<AbstractFlexibleItem<*>> = adapter?.currentItems as
+            List<AbstractFlexibleItem<*>>
+        var internalParticipant: Participant
+        for (i in currentItems.indices) {
+            if (currentItems[i] is ContactItem) {
+                internalParticipant = (currentItems[i] as ContactItem).model
+                if (internalParticipant.getActorType() == Participant.ActorType.GROUPS &&
+                    internalParticipant.isSelected
+                ) {
+                    internalParticipant.isSelected = false
+                    selectedGroupIds.remove(internalParticipant.getActorId())
+                }
+            }
+        }
+    }
+
+    private fun enableContactForNonPublicCall() {
+        for (i in 0 until adapter!!.itemCount) {
+            if (adapter?.getItem(i) is ContactItem) {
+                val contactItem: ContactItem = adapter?.getItem(i) as ContactItem
+                if ("groups" == contactItem.model.getSource()) {
+                    contactItem.isEnabled = !isPublicCall
+                }
+            }
+        }
+    }
+
+    private fun toggleNewCallHeaderVisibility(showInitialLayout: Boolean) {
+        try {
+            if (showInitialLayout) {
+                binding.conversationPrivacyToggle.initialRelativeLayout.visibility = View.VISIBLE
+                binding.conversationPrivacyToggle.secondaryRelativeLayout.visibility = View.GONE
+            } else {
+                binding.conversationPrivacyToggle.initialRelativeLayout.visibility = View.GONE
+                binding.conversationPrivacyToggle.secondaryRelativeLayout.visibility = View.VISIBLE
+            }
+        } catch (npe: NullPointerException) {
+            // view binding can be null
+            // since this is called asynchronously and UI might have been destroyed in the meantime
+            Log.i(TAG, "UI destroyed - view binding already gone")
+        }
+    }
+
+    companion object {
+        const val TAG = "ContactsController"
+        const val RETRIES: Long = 3
+        const val CONTACTS_BATCH_SIZE: Int = 50
+        const val HEADER_ELEVATION: Int = 5
+    }
+}

+ 1 - 0
app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt

@@ -357,6 +357,7 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
         }
     }
 
+    @Suppress("Detekt.LongMethod")
     private fun createUserInfoDetails(userInfo: UserProfileData?): List<UserInfoDetailsItem> {
         val result: MutableList<UserInfoDetailsItem> = LinkedList()
 

+ 1 - 1
app/src/main/java/com/nextcloud/talk/models/json/converters/EnumActorTypeConverter.kt

@@ -32,7 +32,7 @@ import com.nextcloud.talk.models.json.participants.Participant.ActorType.GUESTS
 import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS
 
 class EnumActorTypeConverter : StringBasedTypeConverter<Participant.ActorType>() {
-    override fun getFromString(string: String): Participant.ActorType {
+    override fun getFromString(string: String?): Participant.ActorType {
         return when (string) {
             "emails" -> EMAILS
             "groups" -> GROUPS

+ 3 - 0
app/src/main/res/layout/controller_contacts_rv.xml

@@ -62,14 +62,17 @@
     </LinearLayout>
 
     <include
+        android:id="@+id/conversation_privacy_toggle"
         layout="@layout/conversation_privacy_toggle"
         android:visibility="gone" />
 
     <include
+        android:id="@+id/join_conversation_via_link"
         layout="@layout/join_conversation_via_link"
         android:visibility="gone" />
 
     <include
+        android:id="@+id/controller_generic_rv"
         layout="@layout/controller_generic_rv"
         android:visibility="gone" />
 </LinearLayout>