Rework conversation menu & bug fixes and improvements

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2019-10-18 13:23:51 +02:00
parent 978be1afe1
commit d66134c663
26 changed files with 569 additions and 1131 deletions

View File

@ -28,6 +28,7 @@ import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.emoji.widget.EmojiTextView;
import butterknife.BindView;
@ -103,6 +104,12 @@ public class ConversationItem
holder.dialogAvatar.setController(null);
if (conversation.isUpdating()) {
holder.progressBar.setVisibility(View.VISIBLE);
} else {
holder.progressBar.setVisibility(View.GONE);
}
if (adapter.hasFilter()) {
FlexibleUtils.highlightText(holder.dialogName, conversation.getDisplayName(),
String.valueOf(adapter.getFilter(String.class)),
@ -274,6 +281,8 @@ public class ConversationItem
ImageView passwordProtectedRoomImageView;
@BindView(R.id.favoriteConversationImageView)
ImageView pinnedConversationImageView;
@BindView(R.id.actionProgressBar)
ProgressBar progressBar;
ConversationItemViewHolder(View view, FlexibleAdapter adapter) {
super(view, adapter);

View File

@ -323,13 +323,13 @@ public interface NcApi {
@Url String url, @Query("search") String query,
@Nullable @Query("limit") Integer limit);
// Url is: /api/{apiVersion}/room/{token}/pin
// Url is: /api/{apiVersion}/room/{token}/favorite
@POST
Observable<GenericOverall> addConversationToFavorites(
@Header("Authorization") String authorization,
@Url String url);
// Url is: /api/{apiVersion}/room/{token}/favorites
// Url is: /api/{apiVersion}/room/{token}/favorite
@DELETE
Observable<GenericOverall> removeConversationFromFavorites(
@Header("Authorization") String authorization,

View File

@ -1,728 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.InputType;
import android.text.TextUtils;
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 androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
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 com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.conductor.changehandler.TransitionChangeHandlerCompat;
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler;
import com.bluelinelabs.conductor.internal.NoOpControllerChangeHandler;
import com.facebook.common.executors.UiThreadImmediateExecutorService;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.kennyc.bottomsheet.BottomSheet;
import com.nextcloud.talk.R;
import com.nextcloud.talk.activities.MagicCallActivity;
import com.nextcloud.talk.adapters.items.CallItem;
import com.nextcloud.talk.adapters.items.ConversationItem;
import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.controllers.base.BaseController;
import com.nextcloud.talk.controllers.bottomsheet.CallMenuController;
import com.nextcloud.talk.controllers.bottomsheet.EntryMenuController;
import com.nextcloud.talk.events.BottomSheetLockEvent;
import com.nextcloud.talk.events.EventStatus;
import com.nextcloud.talk.events.MoreMenuClickEvent;
import com.nextcloud.talk.interfaces.ConversationMenuInterface;
import com.nextcloud.talk.jobs.DeleteConversationWorker;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.models.json.conversations.Conversation;
import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.ConductorRemapping;
import com.nextcloud.talk.utils.DisplayUtils;
import com.nextcloud.talk.utils.KeyboardUtils;
import com.nextcloud.talk.utils.animations.SharedElementTransition;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.database.user.UserUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import com.uber.autodispose.AutoDispose;
import com.yarolegovich.lovelydialog.LovelySaveStateHandler;
import com.yarolegovich.lovelydialog.LovelyStandardDialog;
import eu.davidea.fastscroller.FastScroller;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager;
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.parceler.Parcels;
import retrofit2.HttpException;
@AutoInjector(NextcloudTalkApplication.class)
public class ConversationsListController extends BaseController
implements SearchView.OnQueryTextListener,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, FastScroller
.OnScrollStateChangeListener, ConversationMenuInterface {
public static final String TAG = "ConversationsListController";
public static final int ID_DELETE_CONVERSATION_DIALOG = 0;
private static final String KEY_SEARCH_QUERY = "ContactsController.searchQuery";
@Inject
UserUtils userUtils;
@Inject
EventBus eventBus;
@Inject
NcApi ncApi;
@Inject
Context context;
@Inject
AppPreferences appPreferences;
@BindView(R.id.recyclerView)
RecyclerView recyclerView;
@BindView(R.id.swipeRefreshLayoutView)
SwipeRefreshLayout swipeRefreshLayout;
@BindView(R.id.fast_scroller)
FastScroller fastScroller;
@BindView(R.id.floatingActionButton)
FloatingActionButton floatingActionButton;
private UserEntity currentUser;
private FlexibleAdapter<AbstractFlexibleItem> adapter;
private List<AbstractFlexibleItem> callItems = new ArrayList<>();
private BottomSheet bottomSheet;
private MenuItem searchItem;
private SearchView searchView;
private String searchQuery;
private View view;
private boolean shouldUseLastMessageLayout;
private String credentials;
private boolean adapterWasNull = true;
private boolean isRefreshing;
private LovelySaveStateHandler saveStateHandler;
private Bundle conversationMenuBundle = null;
public ConversationsListController() {
super();
setHasOptionsMenu(true);
}
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_conversations_rv, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
NextcloudTalkApplication.Companion.getSharedApplication()
.getComponentApplication()
.inject(this);
if (getActionBar() != null) {
getActionBar().show();
}
if (saveStateHandler == null) {
saveStateHandler = new LovelySaveStateHandler();
}
if (adapter == null) {
adapter = new FlexibleAdapter<>(callItems, getActivity(), true);
} else {
//progressBarView.setVisibility(View.GONE);
}
adapter.addListener(this);
prepareViews();
}
private void loadUserAvatar(MenuItem menuItem) {
if (getActivity() != null) {
int avatarSize = (int) DisplayUtils.convertDpToPixel(menuItem.getIcon().getIntrinsicHeight(),
getActivity());
ImageRequest imageRequest = DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatarWithNameAndPixels(currentUser.getBaseUrl(),
currentUser.getUserId(), avatarSize), null);
ImagePipeline imagePipeline = Fresco.getImagePipeline();
DataSource<CloseableReference<CloseableImage>> dataSource =
imagePipeline.fetchDecodedImage(imageRequest, null);
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
protected void onNewResultImpl(Bitmap bitmap) {
if (bitmap != null && getResources() != null) {
RoundedBitmapDrawable roundedBitmapDrawable =
RoundedBitmapDrawableFactory.create(getResources(), bitmap);
roundedBitmapDrawable.setCircular(true);
roundedBitmapDrawable.setAntiAlias(true);
menuItem.setIcon(roundedBitmapDrawable);
}
}
@Override
protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
menuItem.setIcon(R.drawable.ic_settings_white_24dp);
}
}, UiThreadImmediateExecutorService.getInstance());
}
}
@Override
protected void onAttach(@NonNull View view) {
super.onAttach(view);
currentUser = userUtils.getCurrentUser();
if (currentUser != null) {
credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken());
shouldUseLastMessageLayout = currentUser.hasSpreedFeatureCapability("last-room-activity");
fetchData(false);
}
}
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) {
switch (item.getItemId()) {
case R.id.action_settings:
ArrayList<String> names = new ArrayList<>();
names.add("userAvatar.transitionTag");
getRouter().pushController((RouterTransaction.with(new SettingsController())
.pushChangeHandler(new TransitionChangeHandlerCompat(new SharedElementTransition(names),
new VerticalChangeHandler()))
.popChangeHandler(new TransitionChangeHandlerCompat(new SharedElementTransition(names),
new VerticalChangeHandler()))));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_conversation_plus_filter, menu);
searchItem = menu.findItem(R.id.action_search);
initSearchView();
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
searchItem.setVisible(callItems.size() > 0);
if (adapter.hasFilter()) {
searchItem.expandActionView();
searchView.setQuery(adapter.getFilter(String.class), false);
}
MenuItem menuItem = menu.findItem(R.id.action_settings);
loadUserAvatar(menuItem);
}
private void fetchData(boolean fromBottomSheet) {
isRefreshing = true;
callItems = new ArrayList<>();
ncApi.getRooms(credentials, ApiUtils.getUrlForGetRooms(currentUser.getBaseUrl()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.as(AutoDispose.autoDisposable(getScopeProvider()))
.subscribe(roomsOverall -> {
if (adapterWasNull) {
adapterWasNull = false;
//progressBarView.setVisibility(View.GONE);
}
/*if (roomsOverall.getOcs().getData().size() > 0) {
if (emptyLayoutView.getVisibility() != View.GONE) {
emptyLayoutView.setVisibility(View.GONE);
}
if (swipeRefreshLayout.getVisibility() != View.VISIBLE) {
swipeRefreshLayout.setVisibility(View.VISIBLE);
}
} else {
if (emptyLayoutView.getVisibility() != View.VISIBLE) {
emptyLayoutView.setVisibility(View.VISIBLE);
}
if (swipeRefreshLayout.getVisibility() != View.GONE) {
swipeRefreshLayout.setVisibility(View.GONE);
}
}*/
Conversation conversation;
for (int i = 0; i < roomsOverall.getOcs().getData().size(); i++) {
conversation = roomsOverall.getOcs().getData().get(i);
if (shouldUseLastMessageLayout) {
if (getActivity() != null) {
ConversationItem conversationItem = new ConversationItem(conversation
, currentUser, getActivity());
callItems.add(conversationItem);
}
} else {
CallItem callItem = new CallItem(conversation, currentUser);
callItems.add(callItem);
}
}
if (currentUser.hasSpreedFeatureCapability("last-room-activity")) {
Collections.sort(callItems, (o1, o2) -> {
Conversation conversation1 = ((ConversationItem) o1).getModel();
Conversation conversation2 = ((ConversationItem) o2).getModel();
return new CompareToBuilder()
.append(conversation2.isFavorite(), conversation1.isFavorite())
.append(conversation2.getLastActivity(), conversation1.getLastActivity())
.toComparison();
});
} else {
Collections.sort(callItems, (callItem, t1) ->
Long.compare(((CallItem) t1).getModel().getLastPing(),
((CallItem) callItem).getModel().getLastPing()));
}
adapter.updateDataSet(callItems, false);
if (searchItem != null) {
searchItem.setVisible(callItems.size() > 0);
}
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
}, throwable -> {
if (searchItem != null) {
searchItem.setVisible(false);
}
if (throwable instanceof HttpException) {
HttpException exception = (HttpException) throwable;
switch (exception.code()) {
case 401:
/*if (getParentController() != null &&
getParentController().getRouter() != null) {
getParentController().getRouter().pushController((RouterTransaction.with
(new WebViewLoginController(currentUser.getBaseUrl(),
true))
.pushChangeHandler(new VerticalChangeHandler())
.popChangeHandler(new VerticalChangeHandler())));
}*/
break;
default:
break;
}
}
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
}, () -> {
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
if (fromBottomSheet) {
new Handler().postDelayed(() -> {
bottomSheet.setCancelable(true);
if (bottomSheet.isShowing()) {
bottomSheet.cancel();
}
}, 2500);
}
isRefreshing = false;
});
}
private void prepareViews() {
SmoothScrollLinearLayoutManager layoutManager =
new SmoothScrollLinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
recyclerView.setHasFixedSize(true);
recyclerView.setAdapter(adapter);
swipeRefreshLayout.setOnRefreshListener(() -> fetchData(false));
swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary);
//emptyLayoutView.setOnClickListener(v -> showNewConversationsScreen());
floatingActionButton.setOnClickListener(v -> {
showNewConversationsScreen();
});
fastScroller.addOnScrollStateChangeListener(this);
adapter.setFastScroller(fastScroller);
fastScroller.setBubbleTextCreator(position -> {
String displayName;
if (shouldUseLastMessageLayout) {
displayName = ((ConversationItem) adapter.getItem(position)).getModel().getDisplayName();
} else {
displayName = ((CallItem) adapter.getItem(position)).getModel().getDisplayName();
}
if (displayName.length() > 8) {
displayName = displayName.substring(0, 4) + "...";
}
return displayName;
});
}
private void showNewConversationsScreen() {
Bundle bundle = new Bundle();
bundle.putBoolean(BundleKeys.INSTANCE.getKEY_NEW_CONVERSATION(), true);
getRouter().pushController((RouterTransaction.with(new ContactsController(bundle))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler())));
}
@Override
public void onSaveViewState(@NonNull View view, @NonNull Bundle outState) {
saveStateHandler.saveInstanceState(outState);
if (searchView != null && !TextUtils.isEmpty(searchView.getQuery())) {
outState.putString(KEY_SEARCH_QUERY, searchView.getQuery().toString());
}
super.onSaveViewState(view, outState);
}
@Override
public void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) {
super.onRestoreViewState(view, savedViewState);
searchQuery = savedViewState.getString(KEY_SEARCH_QUERY, "");
if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) {
//Dialog won't be restarted automatically, so we need to call this method.
//Each dialog knows how to restore its viewState
showLovelyDialog(LovelySaveStateHandler.getSavedDialogId(savedViewState), savedViewState);
}
}
@Override
public boolean onQueryTextChange(String newText) {
if (adapter.hasNewFilter(newText) || !TextUtils.isEmpty(searchQuery)) {
if (!TextUtils.isEmpty(searchQuery)) {
adapter.setFilter(searchQuery);
searchQuery = "";
adapter.filterItems();
} else {
adapter.setFilter(newText);
adapter.filterItems(300);
}
}
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setEnabled(!adapter.hasFilter());
}
return true;
}
@Override
public boolean onQueryTextSubmit(String query) {
return onQueryTextChange(query);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(BottomSheetLockEvent bottomSheetLockEvent) {
if (bottomSheet != null) {
if (!bottomSheetLockEvent.isCancelable()) {
bottomSheet.setCancelable(bottomSheetLockEvent.isCancelable());
} else {
if (bottomSheetLockEvent.getDelay() != 0 && bottomSheetLockEvent.isShouldRefreshData()) {
fetchData(true);
} else {
bottomSheet.setCancelable(bottomSheetLockEvent.isCancelable());
if (bottomSheet.isShowing() && bottomSheetLockEvent.isCancel()) {
bottomSheet.cancel();
}
}
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(MoreMenuClickEvent moreMenuClickEvent) {
Bundle bundle = new Bundle();
Conversation conversation = moreMenuClickEvent.getConversation();
bundle.putParcelable(BundleKeys.INSTANCE.getKEY_ROOM(), Parcels.wrap(conversation));
bundle.putParcelable(BundleKeys.INSTANCE.getKEY_MENU_TYPE(),
Parcels.wrap(CallMenuController.MenuType.REGULAR));
prepareAndShowBottomSheetWithBundle(bundle, true);
}
private void prepareAndShowBottomSheetWithBundle(Bundle bundle,
boolean shouldShowCallMenuController) {
if (view == null) {
view = getActivity().getLayoutInflater().inflate(R.layout.bottom_sheet, null, false);
}
if (shouldShowCallMenuController) {
getChildRouter((ViewGroup) view).setRoot(
RouterTransaction.with(new CallMenuController(bundle, this))
.popChangeHandler(new VerticalChangeHandler())
.pushChangeHandler(new VerticalChangeHandler()));
} else {
getChildRouter((ViewGroup) view).setRoot(
RouterTransaction.with(new EntryMenuController(bundle))
.popChangeHandler(new VerticalChangeHandler())
.pushChangeHandler(new VerticalChangeHandler()));
}
if (bottomSheet == null) {
bottomSheet = new BottomSheet.Builder(getActivity()).setView(view).create();
}
bottomSheet.setOnShowListener(
dialog -> new KeyboardUtils(getActivity(), bottomSheet.getLayout(), true));
bottomSheet.setOnDismissListener(
dialog -> getActionBar().setDisplayHomeAsUpEnabled(getRouter().getBackstackSize() > 1));
bottomSheet.show();
}
@Override
public String getTitle() {
return getResources().getString(R.string.nc_app_name);
}
@Override
public void onFastScrollerStateChange(boolean scrolling) {
swipeRefreshLayout.setEnabled(!scrolling);
}
@Override
public boolean onItemClick(View view, int position) {
Object clickedItem = adapter.getItem(position);
if (clickedItem != null && getActivity() != null) {
Conversation conversation;
if (shouldUseLastMessageLayout) {
conversation = ((ConversationItem) clickedItem).getModel();
} else {
conversation = ((CallItem) clickedItem).getModel();
}
Bundle bundle = new Bundle();
bundle.putParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY(), currentUser);
bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), conversation.getToken());
bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), conversation.getRoomId());
if (conversation.hasPassword && (conversation.getParticipantType()
.equals(Participant.ParticipantType.GUEST) ||
conversation.getParticipantType()
.equals(Participant.ParticipantType.USER_FOLLOWING_LINK))) {
bundle.putInt(BundleKeys.INSTANCE.getKEY_OPERATION_CODE(), 99);
prepareAndShowBottomSheetWithBundle(bundle, false);
} else {
currentUser = userUtils.getCurrentUser();
if (currentUser.hasSpreedFeatureCapability("chat-v2")) {
bundle.putParcelable(BundleKeys.INSTANCE.getKEY_ACTIVE_CONVERSATION(),
Parcels.wrap(conversation));
ConductorRemapping.INSTANCE.remapChatController(getRouter(), currentUser.getId(),
conversation.getToken(), bundle, false);
} else {
overridePushHandler(new NoOpControllerChangeHandler());
overridePopHandler(new NoOpControllerChangeHandler());
Intent callIntent = new Intent(getActivity(), MagicCallActivity.class);
callIntent.putExtras(bundle);
startActivity(callIntent);
}
}
}
return true;
}
@Override
public void onItemLongClick(int position) {
if (currentUser.hasSpreedFeatureCapability("last-room-activity")) {
Object clickedItem = adapter.getItem(position);
if (clickedItem != null) {
Conversation conversation;
if (shouldUseLastMessageLayout) {
conversation = ((ConversationItem) clickedItem).getModel();
} else {
conversation = ((CallItem) clickedItem).getModel();
}
MoreMenuClickEvent moreMenuClickEvent = new MoreMenuClickEvent(conversation);
onMessageEvent(moreMenuClickEvent);
}
}
}
@Subscribe(sticky = true, threadMode = ThreadMode.BACKGROUND)
public void onMessageEvent(EventStatus eventStatus) {
if (currentUser != null && eventStatus.getUserId() == currentUser.getId()) {
switch (eventStatus.getEventType()) {
case CONVERSATION_UPDATE:
if (eventStatus.isAllGood() && !isRefreshing) {
fetchData(false);
}
break;
default:
break;
}
}
}
private void showDeleteConversationDialog(Bundle savedInstanceState) {
if (getActivity() != null
&& conversationMenuBundle != null
&& currentUser != null
&& conversationMenuBundle.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID())
== currentUser.getId()) {
Conversation conversation =
Parcels.unwrap(conversationMenuBundle.getParcelable(BundleKeys.INSTANCE.getKEY_ROOM()));
if (conversation != null) {
new LovelyStandardDialog(getActivity(), LovelyStandardDialog.ButtonLayout.HORIZONTAL)
.setTopColorRes(R.color.nc_darkRed)
.setIcon(DisplayUtils.getTintedDrawable(context.getResources(),
R.drawable.ic_delete_black_24dp, R.color.bg_default))
.setPositiveButtonColor(context.getResources().getColor(R.color.nc_darkRed))
.setTitle(R.string.nc_delete_call)
.setMessage(conversation.getDeleteWarningMessage())
.setPositiveButton(R.string.nc_delete, new View.OnClickListener() {
@Override
public void onClick(View v) {
Data.Builder data = new Data.Builder();
data.putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(),
conversationMenuBundle.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID()));
data.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), conversation.getToken());
conversationMenuBundle = null;
deleteConversation(data.build());
}
})
.setNegativeButton(R.string.nc_cancel, new View.OnClickListener() {
@Override
public void onClick(View v) {
conversationMenuBundle = null;
}
})
.setInstanceStateHandler(ID_DELETE_CONVERSATION_DIALOG, saveStateHandler)
.setSavedInstanceState(savedInstanceState)
.show();
}
}
}
private void deleteConversation(Data data) {
OneTimeWorkRequest deleteConversationWorker =
new OneTimeWorkRequest.Builder(DeleteConversationWorker.class).setInputData(data).build();
WorkManager.getInstance().enqueue(deleteConversationWorker);
}
private void showLovelyDialog(int dialogId, Bundle savedInstanceState) {
switch (dialogId) {
case ID_DELETE_CONVERSATION_DIALOG:
showDeleteConversationDialog(savedInstanceState);
break;
default:
break;
}
}
@Override
public void openLovelyDialogWithIdAndBundle(int dialogId, Bundle bundle) {
conversationMenuBundle = bundle;
switch (dialogId) {
case ID_DELETE_CONVERSATION_DIALOG:
showLovelyDialog(dialogId, null);
break;
default:
break;
}
}
}

View File

@ -1,347 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers.bottomsheet;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import autodagger.AutoInjector;
import butterknife.BindView;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.kennyc.bottomsheet.adapters.AppAdapter;
import com.nextcloud.talk.R;
import com.nextcloud.talk.adapters.items.AppItem;
import com.nextcloud.talk.adapters.items.MenuItem;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.controllers.ConversationsListController;
import com.nextcloud.talk.controllers.base.BaseController;
import com.nextcloud.talk.events.BottomSheetLockEvent;
import com.nextcloud.talk.interfaces.ConversationMenuInterface;
import com.nextcloud.talk.jobs.LeaveConversationWorker;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.models.json.conversations.Conversation;
import com.nextcloud.talk.utils.DisplayUtils;
import com.nextcloud.talk.utils.ShareUtils;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.database.user.UserUtils;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager;
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.greenrobot.eventbus.EventBus;
import org.parceler.Parcel;
import org.parceler.Parcels;
@AutoInjector(NextcloudTalkApplication.class)
public class CallMenuController extends BaseController
implements FlexibleAdapter.OnItemClickListener {
@BindView(R.id.recyclerView)
RecyclerView recyclerView;
@Inject
EventBus eventBus;
@Inject
UserUtils userUtils;
private Conversation conversation;
private List<AbstractFlexibleItem> menuItems;
private FlexibleAdapter<AbstractFlexibleItem> adapter;
private MenuType menuType;
private Intent shareIntent;
private UserEntity currentUser;
private ConversationMenuInterface conversationMenuInterface;
public CallMenuController(Bundle args) {
super();
this.conversation = Parcels.unwrap(args.getParcelable(BundleKeys.INSTANCE.getKEY_ROOM()));
if (args.containsKey(BundleKeys.INSTANCE.getKEY_MENU_TYPE())) {
this.menuType = Parcels.unwrap(args.getParcelable(BundleKeys.INSTANCE.getKEY_MENU_TYPE()));
}
}
public CallMenuController(Bundle args,
@Nullable ConversationMenuInterface conversationMenuInterface) {
super();
this.conversation = Parcels.unwrap(args.getParcelable(BundleKeys.INSTANCE.getKEY_ROOM()));
if (args.containsKey(BundleKeys.INSTANCE.getKEY_MENU_TYPE())) {
this.menuType = Parcels.unwrap(args.getParcelable(BundleKeys.INSTANCE.getKEY_MENU_TYPE()));
}
this.conversationMenuInterface = conversationMenuInterface;
}
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_call_menu, container, false);
}
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
NextcloudTalkApplication.Companion.getSharedApplication()
.getComponentApplication()
.inject(this);
prepareViews();
}
private void prepareViews() {
LinearLayoutManager layoutManager = new SmoothScrollLinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
recyclerView.setHasFixedSize(true);
prepareMenu();
if (adapter == null) {
adapter = new FlexibleAdapter<>(menuItems, getActivity(), false);
}
adapter.addListener(this);
recyclerView.setAdapter(adapter);
}
private void prepareIntent() {
shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT,
String.format(getResources().getString(R.string.nc_share_subject),
getResources().getString(R.string.nc_app_name)));
}
private void prepareMenu() {
menuItems = new ArrayList<>();
if (menuType.equals(MenuType.REGULAR)) {
if (!TextUtils.isEmpty(conversation.getDisplayName())) {
menuItems.add(new MenuItem(conversation.getDisplayName(), 0, null));
} else if (!TextUtils.isEmpty(conversation.getName())) {
menuItems.add(new MenuItem(conversation.getName(), 0, null));
} else {
menuItems.add(new MenuItem(getResources().getString(R.string.nc_configure_room), 0, null));
}
currentUser = userUtils.getCurrentUser();
if (conversation.isFavorite()) {
menuItems.add(new MenuItem(getResources().getString(R.string.nc_remove_from_favorites), 97,
DisplayUtils.getTintedDrawable(getResources(), R.drawable.ic_star_border_black_24dp,
R.color.grey_600)));
} else if (currentUser.hasSpreedFeatureCapability("favorites")) {
menuItems.add(new MenuItem(getResources().getString(R.string.nc_add_to_favorites)
, 98, DisplayUtils.getTintedDrawable(getResources(), R.drawable.ic_star_black_24dp,
R.color.grey_600)));
}
if (conversation.isNameEditable(currentUser)) {
menuItems.add(new MenuItem(getResources().getString(R.string.nc_rename), 2,
getResources().getDrawable(R.drawable
.ic_pencil_grey600_24dp)));
}
if (conversation.canModerate(currentUser)) {
if (!conversation.isPublic()) {
menuItems.add(new MenuItem(getResources().getString(R.string.nc_make_call_public), 3,
getResources().getDrawable(R.drawable
.ic_link_grey600_24px)));
} else {
if (conversation.isHasPassword()) {
menuItems.add(new MenuItem(getResources().getString(R.string.nc_change_password), 4,
getResources().getDrawable(R.drawable
.ic_lock_grey600_24px)));
menuItems.add(new MenuItem(getResources().getString(R.string.nc_clear_password), 5,
getResources().getDrawable(R.drawable
.ic_lock_open_grey600_24dp)));
} else {
menuItems.add(new MenuItem(getResources().getString(R.string.nc_set_password), 6,
getResources().getDrawable(R.drawable
.ic_lock_plus_grey600_24dp)));
}
}
menuItems.add(new MenuItem(getResources().getString(R.string.nc_delete_call), 9,
getResources().getDrawable(R.drawable
.ic_delete_grey600_24dp)));
}
if (conversation.isPublic()) {
menuItems.add(new MenuItem(getResources().getString(R.string.nc_share_link), 7,
getResources().getDrawable(R.drawable
.ic_link_grey600_24px)));
if (conversation.canModerate(currentUser)) {
menuItems.add(new MenuItem(getResources().getString(R.string.nc_make_call_private), 8,
getResources().getDrawable(R.drawable
.ic_group_grey600_24px)));
}
}
if (conversation.canLeave(currentUser)) {
menuItems.add(new MenuItem(getResources().getString(R.string.nc_leave), 1,
DisplayUtils.getTintedDrawable(getResources(),
R.drawable.ic_exit_to_app_black_24dp, R.color.grey_600)
));
}
} else if (menuType.equals(MenuType.SHARE)) {
prepareIntent();
List<AppAdapter.AppInfo> appInfoList =
ShareUtils.getShareApps(getActivity(), shareIntent, null,
null);
menuItems.add(new AppItem(getResources().getString(R.string.nc_share_link_via), "", "",
getResources().getDrawable(R.drawable.ic_link_grey600_24px)));
if (appInfoList != null) {
for (AppAdapter.AppInfo appInfo : appInfoList) {
menuItems.add(
new AppItem(appInfo.title, appInfo.packageName, appInfo.name, appInfo.drawable));
}
}
} else {
menuItems.add(
new MenuItem(getResources().getString(R.string.nc_start_conversation), 0, null));
menuItems.add(new MenuItem(getResources().getString(R.string.nc_new_conversation), 1,
getResources().getDrawable(R.drawable.ic_add_grey600_24px)));
menuItems.add(new MenuItem(getResources().getString(R.string.nc_join_via_link), 2,
getResources().getDrawable(R.drawable.ic_link_grey600_24px)));
}
}
@Override
public boolean onItemClick(View view, int position) {
Bundle bundle = new Bundle();
bundle.putParcelable(BundleKeys.INSTANCE.getKEY_ROOM(), Parcels.wrap(conversation));
if (menuType.equals(MenuType.REGULAR)) {
MenuItem menuItem = (MenuItem) adapter.getItem(position);
if (menuItem != null) {
int tag = menuItem.getTag();
if (tag == 5) {
conversation.setPassword("");
}
if (tag > 0) {
if (tag == 1 || tag == 9) {
if (tag == 1) {
Data data;
if ((data = getWorkerData()) != null) {
OneTimeWorkRequest leaveConversationWorker =
new OneTimeWorkRequest.Builder(LeaveConversationWorker.class).setInputData(data)
.build();
WorkManager.getInstance().enqueue(leaveConversationWorker);
}
} else {
Bundle deleteConversationBundle;
if ((deleteConversationBundle = getDeleteConversationBundle()) != null) {
conversationMenuInterface.openLovelyDialogWithIdAndBundle(
ConversationsListController.ID_DELETE_CONVERSATION_DIALOG,
deleteConversationBundle);
}
}
eventBus.post(new BottomSheetLockEvent(true, 0, false, true));
} else {
bundle.putInt(BundleKeys.INSTANCE.getKEY_OPERATION_CODE(), tag);
if (tag != 2 && tag != 4 && tag != 6 && tag != 7) {
eventBus.post(new BottomSheetLockEvent(false, 0, false, false));
getRouter().pushController(
RouterTransaction.with(new OperationsMenuController(bundle))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
} else if (tag != 7) {
getRouter().pushController(RouterTransaction.with(new EntryMenuController(bundle))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
} else {
bundle.putParcelable(BundleKeys.INSTANCE.getKEY_MENU_TYPE(),
Parcels.wrap(MenuType.SHARE));
getRouter().pushController(
RouterTransaction.with(new CallMenuController(bundle, null))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
}
}
}
} else if (menuType.equals(MenuType.SHARE) && position != 0) {
AppItem appItem = (AppItem) adapter.getItem(position);
if (appItem != null && getActivity() != null) {
if (!conversation.hasPassword) {
shareIntent.putExtra(Intent.EXTRA_TEXT, ShareUtils.getStringForIntent(getActivity(), null,
userUtils, conversation));
Intent intent = new Intent(shareIntent);
intent.setComponent(new ComponentName(appItem.getPackageName(), appItem.getName()));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
eventBus.post(new BottomSheetLockEvent(true, 0, false, true));
getActivity().startActivity(intent);
} else {
bundle.putInt(BundleKeys.INSTANCE.getKEY_OPERATION_CODE(), 7);
bundle.putParcelable(BundleKeys.INSTANCE.getKEY_SHARE_INTENT(),
Parcels.wrap(shareIntent));
bundle.putString(BundleKeys.INSTANCE.getKEY_APP_ITEM_PACKAGE_NAME(),
appItem.getPackageName());
bundle.putString(BundleKeys.INSTANCE.getKEY_APP_ITEM_NAME(), appItem.getName());
getRouter().pushController(RouterTransaction.with(new EntryMenuController(bundle))
.pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
}
}
}
return true;
}
private Data getWorkerData() {
if (!TextUtils.isEmpty(conversation.getToken())) {
Data.Builder data = new Data.Builder();
data.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), conversation.getToken());
data.putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), currentUser.getId());
return data.build();
}
return null;
}
private Bundle getDeleteConversationBundle() {
if (!TextUtils.isEmpty(conversation.getToken())) {
Bundle bundle = new Bundle();
bundle.putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), currentUser.getId());
bundle.putParcelable(BundleKeys.INSTANCE.getKEY_ROOM(), Parcels.wrap(conversation));
return bundle;
}
return null;
}
@Parcel
public enum MenuType {
REGULAR, SHARE
}
}

View File

@ -22,6 +22,7 @@ package com.nextcloud.talk.models.json.conversations;
import android.content.res.Resources;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonIgnore;
import com.bluelinelabs.logansquare.annotation.JsonObject;
import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication;
@ -90,6 +91,7 @@ public class Conversation {
public Long lobbyTimer;
@JsonField(name = "lastReadMessage")
public int lastReadMessage;
public boolean updating;
public boolean isPublic() {
return (ConversationType.ROOM_PUBLIC_CALL.equals(type));

View File

@ -22,16 +22,50 @@ package com.nextcloud.talk.newarch.data.repository
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.newarch.data.source.remote.ApiService
import com.nextcloud.talk.newarch.domain.repository.NextcloudTalkRepository
import com.nextcloud.talk.newarch.utils.getCredentials
import com.nextcloud.talk.utils.ApiUtils
class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : NextcloudTalkRepository {
override suspend fun deleteConversationForUser(
user: UserEntity,
conversation: Conversation
): GenericOverall {
return apiService.deleteConversation(user.getCredentials(), ApiUtils.getRoom(user.baseUrl, conversation.token))
}
override suspend fun leaveConversationForUser(
user: UserEntity,
conversation: Conversation
): GenericOverall {
return apiService.leaveConversation(user.getCredentials(), ApiUtils.getUrlForRemoveSelfFromRoom(user
.baseUrl, conversation.token))
}
override suspend fun setFavoriteValueForConversation(
user: UserEntity,
conversation: Conversation,
favorite: Boolean
): GenericOverall {
if (favorite) {
return apiService.addConversationToFavorites(
user.getCredentials(),
ApiUtils.getUrlForConversationFavorites(user.baseUrl, conversation.token)
)
} else {
return apiService.removeConversationFromFavorites(
user.getCredentials(),
ApiUtils.getUrlForConversationFavorites(user.baseUrl, conversation.token)
)
}
}
override suspend fun getConversationsForUser(user: UserEntity): List<Conversation> {
return apiService.getConversations(
ApiUtils.getCredentials(user.username, user.token),
user.getCredentials(),
ApiUtils.getUrlForGetRooms(user.baseUrl)
)
.ocs.data
).ocs.data
}
}

View File

@ -21,8 +21,12 @@
package com.nextcloud.talk.newarch.data.source.remote
import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.GenericOverall
import io.reactivex.Observable
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Url
interface ApiService {
@ -36,4 +40,29 @@ interface ApiService {
"Authorization"
) authorization: String, @Url url: String
): RoomsOverall
@POST
suspend fun addConversationToFavorites(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@DELETE
suspend fun removeConversationFromFavorites(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@DELETE
suspend fun leaveConversation(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
@DELETE
suspend fun deleteConversation(
@Header("Authorization") authorization: String,
@Url url: String
): GenericOverall
}

View File

@ -79,7 +79,7 @@ val NetworkModule = module {
single { createSslSocketFactory(get(), get()) }
single { createCache(androidApplication() as NextcloudTalkApplication) }
single { createOkHttpClient(androidContext(), get(), get(), get(), get(), get(), get(), get()) }
factory { createApiErrorHandler() }
single { createNextcloudTalkRepository(get()) }
}

View File

@ -22,7 +22,15 @@ package com.nextcloud.talk.newarch.domain.repository
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.generic.GenericOverall
interface NextcloudTalkRepository {
suspend fun getConversationsForUser(user: UserEntity): List<Conversation>
suspend fun setFavoriteValueForConversation(
user: UserEntity,
conversation: Conversation,
favorite: Boolean
) : GenericOverall
suspend fun deleteConversationForUser(user: UserEntity, conversation: Conversation) : GenericOverall
suspend fun leaveConversationForUser(userEntity: UserEntity, conversation: Conversation) : GenericOverall
}

View File

@ -0,0 +1,38 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.newarch.domain.usecases
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.NextcloudTalkRepository
import com.nextcloud.talk.newarch.domain.usecases.base.UseCase
import org.koin.core.parameter.DefinitionParameters
class DeleteConversationUseCase constructor(
private val nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler?
) : UseCase<GenericOverall, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): GenericOverall {
val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.deleteConversationForUser(user, definitionParameters.get(0))
}
}

View File

@ -0,0 +1,38 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.newarch.domain.usecases
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.NextcloudTalkRepository
import com.nextcloud.talk.newarch.domain.usecases.base.UseCase
import org.koin.core.parameter.DefinitionParameters
class LeaveConversationUseCase constructor(
private val nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler?
) : UseCase<GenericOverall, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): GenericOverall {
val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.leaveConversationForUser(user, definitionParameters.get(0))
}
}

View File

@ -0,0 +1,38 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.newarch.domain.usecases
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.NextcloudTalkRepository
import com.nextcloud.talk.newarch.domain.usecases.base.UseCase
import org.koin.core.parameter.DefinitionParameters
class SetConversationFavoriteValueUseCase constructor(
private val nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler?
) : UseCase<GenericOverall, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): GenericOverall {
val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.setFavoriteValueForConversation(user, definitionParameters.get(0), definitionParameters.get(1))
}
}

View File

@ -21,6 +21,7 @@
package com.nextcloud.talk.newarch.domain.usecases.base
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async

View File

@ -23,16 +23,24 @@ package com.nextcloud.talk.newarch.features.conversationsList
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.newarch.domain.usecases.DeleteConversationUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase
import com.nextcloud.talk.newarch.domain.usecases.LeaveConversationUseCase
import com.nextcloud.talk.newarch.domain.usecases.SetConversationFavoriteValueUseCase
import com.nextcloud.talk.utils.database.user.UserUtils
class ConversationListViewModelFactory constructor(
private val application: Application,
private val conversationsUseCase: GetConversationsUseCase,
private val setConversationFavoriteValueUseCase: SetConversationFavoriteValueUseCase,
private val leaveConversationUseCase: LeaveConversationUseCase,
private val deleteConversationUseCase: DeleteConversationUseCase,
private val userUtils: UserUtils
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ConversationsListViewModel(application, conversationsUseCase, userUtils) as T
return ConversationsListViewModel(application, conversationsUseCase,
setConversationFavoriteValueUseCase, leaveConversationUseCase, deleteConversationUseCase,
userUtils) as T
}
}

View File

@ -38,23 +38,25 @@ import androidx.core.view.MenuItemCompat
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import butterknife.OnClick
import com.afollestad.materialdialogs.LayoutMode.WRAP_CONTENT
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.changehandler.TransitionChangeHandlerCompat
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
import com.nextcloud.talk.R
import com.nextcloud.talk.R.drawable
import com.nextcloud.talk.adapters.items.ConversationItem
import com.nextcloud.talk.controllers.ContactsController
import com.nextcloud.talk.controllers.SettingsController
import com.nextcloud.talk.controllers.bottomsheet.CallMenuController.MenuType
import com.nextcloud.talk.controllers.bottomsheet.CallMenuController.MenuType.REGULAR
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.controllers.bottomsheet.items.listItemsWithImage
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseView
import com.nextcloud.talk.newarch.mvvm.ViewState.FAILED
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADED
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADED_EMPTY
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADING
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
import com.nextcloud.talk.newarch.utils.ViewState.FAILED
import com.nextcloud.talk.newarch.utils.ViewState.LOADED
import com.nextcloud.talk.newarch.utils.ViewState.LOADED_EMPTY
import com.nextcloud.talk.newarch.utils.ViewState.LOADING
import com.nextcloud.talk.utils.ConductorRemapping
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.animations.SharedElementTransition
@ -213,12 +215,17 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
if (value.equals(FAILED)) {
view?.stateWithMessageView?.errorStateTextView?.text = messageData
if (messageData.equals(context.resources.getString(R.string.nc_no_connection_error))) {
if (messageData.equals(
context.resources.getString(R.string.nc_no_connection_error)
)
) {
view?.stateWithMessageView?.errorStateImageView?.setImageResource(
R.drawable.ic_cloud_off_white_24dp)
R.drawable.ic_cloud_off_white_24dp
)
} else {
view?.stateWithMessageView?.errorStateImageView?.setImageResource(
R.drawable.ic_announcement_white_24dp)
R.drawable.ic_announcement_white_24dp
)
}
view?.floatingActionButton?.visibility = View.GONE
} else {
@ -236,7 +243,7 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
}
})
conversationsListData.observe(this@ConversationsListView, Observer {
conversationsLiveListData.observe(this@ConversationsListView, Observer {
val newConversations = mutableListOf<ConversationItem>()
for (conversation in it) {
newConversations.add(ConversationItem(conversation, viewModel.currentUser, activity))
@ -318,12 +325,64 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
override fun onItemLongClick(position: Int) {
val clickedItem = recyclerViewAdapter.getItem(position)
if (clickedItem != null) {
val conversation = (clickedItem as ConversationItem).model
val bundle = Bundle()
bundle.putParcelable(BundleKeys.KEY_ROOM, Parcels.wrap<Conversation>(conversation))
bundle.putParcelable(BundleKeys.KEY_MENU_TYPE, Parcels.wrap<MenuType>(REGULAR))
//prepareAndShowBottomSheetWithBundle(bundle, true)
clickedItem?.let {
val conversationItem = it as ConversationItem
val conversation = conversationItem.model
activity?.let { activity ->
MaterialDialog(activity, BottomSheet(WRAP_CONTENT)).show {
cornerRadius(res = R.dimen.corner_radius)
title(text = conversation.displayName)
listItemsWithImage(
viewModel.getConversationMenuItemsForConversation(conversation)
) { dialog,
index, item ->
when (item.iconRes) {
drawable.ic_star_border_black_24dp -> {
viewModel.changeFavoriteValueForConversation(conversation, false)
}
drawable.ic_star_black_24dp -> {
viewModel.changeFavoriteValueForConversation(conversation, true)
}
drawable.ic_link_grey600_24px -> {
startActivity(viewModel.getShareIntentForConversation(conversation))
}
drawable.ic_exit_to_app_black_24dp -> {
MaterialDialog(activity).show {
title(R.string.nc_leave)
message(R.string.nc_leave_message)
positiveButton(R.string.nc_simple_leave) { dialog ->
viewModel.leaveConversation(conversation)
}
negativeButton(R.string.nc_cancel)
icon(drawable.ic_exit_to_app_black_24dp)
}
}
drawable.ic_delete_grey600_24dp -> {
MaterialDialog(activity).show {
title(R.string.nc_delete)
message(text = conversation.deleteWarningMessage)
positiveButton(R.string.nc_delete_call) { dialog ->
viewModel.deleteConversation(conversation)
}
negativeButton(R.string.nc_cancel)
icon(
drawable = DisplayUtils.getTintedDrawable(
resources, drawable
.ic_delete_grey600_24dp, R.color.nc_darkRed
)
)
}
}
else -> {
}
}
}
}
}
}
}

View File

@ -21,6 +21,7 @@
package com.nextcloud.talk.newarch.features.conversationsList
import android.app.Application
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
@ -33,29 +34,42 @@ import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
import com.facebook.imagepipeline.image.CloseableImage
import com.nextcloud.talk.R
import com.nextcloud.talk.R.drawable
import com.nextcloud.talk.R.string
import com.nextcloud.talk.controllers.bottomsheet.items.BasicListItemWithImage
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel
import com.nextcloud.talk.newarch.data.model.ErrorModel
import com.nextcloud.talk.newarch.domain.usecases.DeleteConversationUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase
import com.nextcloud.talk.newarch.domain.usecases.LeaveConversationUseCase
import com.nextcloud.talk.newarch.domain.usecases.SetConversationFavoriteValueUseCase
import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse
import com.nextcloud.talk.newarch.mvvm.ViewState
import com.nextcloud.talk.newarch.mvvm.ViewState.FAILED
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADED
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADED_EMPTY
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADING
import com.nextcloud.talk.newarch.utils.ViewState
import com.nextcloud.talk.newarch.utils.ViewState.FAILED
import com.nextcloud.talk.newarch.utils.ViewState.LOADED
import com.nextcloud.talk.newarch.utils.ViewState.LOADED_EMPTY
import com.nextcloud.talk.newarch.utils.ViewState.LOADING
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.ShareUtils
import com.nextcloud.talk.utils.database.user.UserUtils
import org.apache.commons.lang3.builder.CompareToBuilder
import org.koin.core.parameter.parametersOf
class ConversationsListViewModel constructor(
application: Application,
private val conversationsUseCase: GetConversationsUseCase,
private val getConversationsUseCase: GetConversationsUseCase,
private val setConversationFavoriteValueUseCase: SetConversationFavoriteValueUseCase,
private val leaveConversationUseCase: LeaveConversationUseCase,
private val deleteConversationUseCase: DeleteConversationUseCase,
private val userUtils: UserUtils
) : BaseViewModel<ConversationsListView>(application) {
val conversationsListData = MutableLiveData<List<Conversation>>()
private var conversations: MutableList<Conversation> = mutableListOf()
val conversationsLiveListData = MutableLiveData<List<Conversation>>()
val viewState = MutableLiveData<ViewState>(LOADING)
var messageData: String? = null
val searchQuery = MutableLiveData<String>()
@ -63,23 +77,91 @@ class ConversationsListViewModel constructor(
var currentUserAvatar: MutableLiveData<Drawable> = MutableLiveData()
get() {
if (field.value == null) {
field.value = context.resources.getDrawable(R.drawable.ic_settings_white_24dp)
field.value = context.resources.getDrawable(drawable.ic_settings_white_24dp)
}
return field
}
fun leaveConversation(conversation: Conversation) {
leaveConversationUseCase.user = currentUser
setConversationUpdateStatus(conversation, true)
leaveConversationUseCase.invoke(viewModelScope, parametersOf(conversation),
object : UseCaseResponse<GenericOverall> {
override fun onSuccess(result: GenericOverall) {
// TODO: Use binary search to find the right room
conversations.find { it.roomId == conversation.roomId }?.let {
conversations.remove(it)
conversationsLiveListData.value = conversations
}
}
override fun onError(errorModel: ErrorModel?) {
setConversationUpdateStatus(conversation, false)
}
})
}
fun deleteConversation(conversation: Conversation) {
deleteConversationUseCase.user = currentUser
setConversationUpdateStatus(conversation, true)
deleteConversationUseCase.invoke(viewModelScope, parametersOf(conversation),
object : UseCaseResponse<GenericOverall> {
override fun onSuccess(result: GenericOverall) {
// TODO: Use binary search to find the right room
conversations.find { it.roomId == conversation.roomId }?.let {
conversations.remove(it)
conversationsLiveListData.value = conversations
}
}
override fun onError(errorModel: ErrorModel?) {
setConversationUpdateStatus(conversation, false)
}
})
}
fun changeFavoriteValueForConversation(
conversation: Conversation,
favorite: Boolean
) {
setConversationFavoriteValueUseCase.user = currentUser
setConversationUpdateStatus(conversation, true)
setConversationFavoriteValueUseCase.invoke(viewModelScope, parametersOf(conversation, favorite),
object : UseCaseResponse<GenericOverall> {
override fun onSuccess(result: GenericOverall) {
// TODO: Use binary search to find the right room
conversations.find { it.roomId == conversation.roomId }?.apply {
updating = false
isFavorite = favorite
conversationsLiveListData.value = conversations
}
}
override fun onError(errorModel: ErrorModel?) {
setConversationUpdateStatus(conversation, false)
}
})
}
fun loadConversations() {
currentUser = userUtils.currentUser
if (viewState.value?.equals(FAILED)!! || !conversationsUseCase.isUserInitialized() ||
conversationsUseCase.user != currentUser
if (viewState.value?.equals(FAILED)!! || !getConversationsUseCase.isUserInitialized() ||
getConversationsUseCase.user != currentUser
) {
conversationsUseCase.user = currentUser
getConversationsUseCase.user = currentUser
viewState.value = LOADING
}
conversationsUseCase.invoke(
getConversationsUseCase.invoke(
viewModelScope, null, object : UseCaseResponse<List<Conversation>> {
override fun onSuccess(result: List<Conversation>) {
val newConversations = result.toMutableList()
@ -91,7 +173,8 @@ class ConversationsListViewModel constructor(
.toComparison()
})
conversationsListData.value = newConversations
conversations = newConversations
conversationsLiveListData.value = conversations
viewState.value = if (newConversations.isNotEmpty()) LOADED else LOADED_EMPTY
messageData = ""
}
@ -133,4 +216,88 @@ class ConversationsListViewModel constructor(
}, UiThreadImmediateExecutorService.getInstance())
}
fun getShareIntentForConversation(conversation: Conversation): Intent {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(
Intent.EXTRA_SUBJECT,
String.format(
context.getString(R.string.nc_share_subject),
context.getString(R.string.nc_app_name)
)
)
// TODO, make sure we ask for password if needed
putExtra(
Intent.EXTRA_TEXT, ShareUtils.getStringForIntent(
context, null,
userUtils, conversation
)
)
type = "text/plain"
}
val intent = Intent.createChooser(sendIntent, context.getString(string.nc_share_link))
// TODO filter our own app once we're there
return intent
}
fun getConversationMenuItemsForConversation(conversation: Conversation): MutableList<BasicListItemWithImage> {
val items = mutableListOf<BasicListItemWithImage>()
if (conversation.isFavorite) {
items.add(
BasicListItemWithImage(
drawable.ic_star_border_black_24dp,
context.getString(string.nc_remove_from_favorites)
)
)
} else {
items.add(
BasicListItemWithImage(
drawable.ic_star_black_24dp,
context.getString(string.nc_add_to_favorites)
)
)
}
if (conversation.isPublic) {
items.add(
(BasicListItemWithImage(
drawable
.ic_link_grey600_24px, context.getString(string.nc_share_link)
))
)
}
if (conversation.canLeave(currentUser)) {
items.add(
BasicListItemWithImage(
drawable.ic_exit_to_app_black_24dp, context.getString
(string.nc_leave)
)
)
}
if (conversation.canModerate(currentUser)) {
items.add(
BasicListItemWithImage(
drawable.ic_delete_grey600_24dp, context.getString(
string.nc_delete_call
)
)
)
}
return items
}
private fun setConversationUpdateStatus(conversation: Conversation, value: Boolean) {
conversations.find { it.roomId == conversation.roomId }?.apply {
updating = value
conversationsLiveListData.value = conversations
}
}
}

View File

@ -22,18 +22,35 @@ package com.nextcloud.talk.newarch.features.conversationsList.di.module
import android.app.Application
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.di.module.createApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.NextcloudTalkRepository
import com.nextcloud.talk.newarch.domain.usecases.DeleteConversationUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase
import com.nextcloud.talk.newarch.domain.usecases.LeaveConversationUseCase
import com.nextcloud.talk.newarch.domain.usecases.SetConversationFavoriteValueUseCase
import com.nextcloud.talk.newarch.features.conversationsList.ConversationListViewModelFactory
import com.nextcloud.talk.utils.database.user.UserUtils
import org.koin.android.ext.koin.androidApplication
import org.koin.dsl.module
val ConversationsListModule = module {
single { createGetConversationsUseCase(get(), createApiErrorHandler()) }
single { createGetConversationsUseCase(get(), get()) }
single { createSetConversationFavoriteValueUseCase(get(), get()) }
single { createLeaveConversationUseCase(get(), get()) }
single { createDeleteConversationuseCase(get(), get()) }
//viewModel { ConversationsListViewModel(get(), get()) }
factory { createConversationListViewModelFactory(androidApplication(), get(), get()) }
factory {
createConversationListViewModelFactory(
androidApplication(), get(), get(), get(), get
(), get()
)
}
}
fun createSetConversationFavoriteValueUseCase(
nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler
): SetConversationFavoriteValueUseCase {
return SetConversationFavoriteValueUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun createGetConversationsUseCase(
@ -43,11 +60,32 @@ fun createGetConversationsUseCase(
return GetConversationsUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun createLeaveConversationUseCase(
nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler
): LeaveConversationUseCase {
return LeaveConversationUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun createDeleteConversationuseCase(
nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler
): DeleteConversationUseCase {
return DeleteConversationUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun createConversationListViewModelFactory(
application: Application,
conversationsUseCase:
getConversationsUseCase:
GetConversationsUseCase,
setConversationFavoriteValueUseCase: SetConversationFavoriteValueUseCase,
leaveConversationUseCase: LeaveConversationUseCase,
deleteConversationUseCase: DeleteConversationUseCase,
userUtils: UserUtils
): ConversationListViewModelFactory {
return ConversationListViewModelFactory(application, conversationsUseCase, userUtils)
return ConversationListViewModelFactory(
application, getConversationsUseCase,
setConversationFavoriteValueUseCase, leaveConversationUseCase, deleteConversationUseCase,
userUtils
)
}

View File

@ -0,0 +1,26 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.newarch.utils
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.utils.ApiUtils
fun UserEntity.getCredentials() = ApiUtils.getCredentials(username, token)

View File

@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.newarch.mvvm
package com.nextcloud.talk.newarch.utils
enum class ViewState {
LOADING,

View File

@ -246,9 +246,10 @@ public class DisplayUtils {
public static Drawable getTintedDrawable(Resources res, @DrawableRes int drawableResId,
@ColorRes int colorResId) {
Drawable drawable = res.getDrawable(drawableResId);
Drawable mutableDrawable = drawable.mutate();
int color = res.getColor(colorResId);
drawable.setTint(color);
return drawable;
mutableDrawable.setTint(color);
return mutableDrawable;
}
public static Drawable getDrawableForMentionChipSpan(Context context, String id,

View File

@ -42,8 +42,7 @@ import java.util.Set;
public class ShareUtils {
public static String getStringForIntent(Context context, @Nullable String password,
UserUtils userUtils, Conversation
conversation) {
UserUtils userUtils, Conversation conversation) {
UserEntity userEntity = userUtils.getCurrentUser();
String shareString = "";

View File

@ -21,5 +21,6 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
<path android:fillColor="@color/grey_600"
android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -21,5 +21,6 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
<path android:fillColor="@color/grey_600"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
</vector>

View File

@ -21,5 +21,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
<path android:fillColor="@color/grey_600" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
</vector>

View File

@ -27,7 +27,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/rv_item_view_height"
android:layout_margin="@dimen/double_margin_between_elements">
android:layout_margin="@dimen/double_margin_between_elements"
android:animateLayoutChanges="true">
<RelativeLayout
@ -78,7 +79,6 @@
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/dialogUnreadBubble"
android:layout_toEndOf="@id/dialogLastMessageUserAvatar"
android:ellipsize="end"
android:gravity="top"
android:lines="1"
@ -112,13 +112,25 @@
android:maxLines="1"
android:textColor="@color/conversation_date" />
<ProgressBar
android:layout_width="16sp"
android:layout_height="16sp"
android:id="@+id/actionProgressBar"
android:layout_alignBottom="@id/dialogName"
android:layout_marginEnd="8dp"
android:layout_toEndOf="@+id/dialogAvatarFrameLayout"
android:indeterminateTint="@color/colorPrimary"
android:visibility="gone"
/>
<androidx.emoji.widget.EmojiTextView
android:id="@id/dialogName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignTop="@id/dialogAvatarFrameLayout"
android:layout_toStartOf="@id/dialogDate"
android:layout_toEndOf="@id/dialogAvatarFrameLayout"
android:layout_toEndOf="@id/actionProgressBar"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"

View File

@ -147,6 +147,8 @@
<string name="nc_start_conversation">Start a conversation</string>
<string name="nc_configure_room">Configure conversation</string>
<string name="nc_leave">Leave conversation</string>
<string name="nc_leave_message">Please confirm your intent to leave this conversation.</string>
<string name="nc_simple_leave">Leave</string>
<string name="nc_rename">Rename conversation</string>
<string name="nc_set_password">Set a password</string>
<string name="nc_change_password">Change password</string>
@ -157,10 +159,12 @@
<string name="nc_make_call_private">Make conversation private</string>
<string name="nc_delete_call">Delete conversation</string>
<string name="nc_delete">Delete</string>
<string name="nc_delete_conversation_default">Please confirm your intent to remove the conversation.</string>
<string name="nc_delete_conversation_one2one">If you delete the conversation, it will also be
<string name="nc_delete_conversation_default">Please confirm your intent to delete this
conversation.</string>
<string name="nc_delete_conversation_one2one">If you delete the conversation it will also be
deleted for %1$s.</string>
<string name="nc_delete_conversation_more">If you delete the conversation, it will also be deleted for all other participants.</string>
<string name="nc_delete_conversation_more">If you delete this conversation it will also be
deleted for all other participants.</string>
<string name="nc_new_conversation">New conversation</string>
<string name="nc_join_via_link">Join with a link</string>