Commit 349f73a3 authored by Tiago Cunha's avatar Tiago Cunha Committed by GitHub

Merge branch 'develop' into feature/2fa

parents 1a41bf56 825b0b9f
......@@ -33,8 +33,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 16
targetSdkVersion 25
versionCode 17
versionName "1.0.8"
versionCode 20
versionName "1.0.10"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
......
package chat.rocket.android;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Process;
......@@ -11,8 +10,8 @@ public class BackgroundLooper {
public static Looper get() {
if (handlerThread == null) {
handlerThread =
new HandlerThread("BackgroundHandlerThread", Process.THREAD_PRIORITY_BACKGROUND);
handlerThread = new HandlerThread(
"BackgroundHandlerThread", Process.THREAD_PRIORITY_BACKGROUND);
handlerThread.start();
}
......
......@@ -16,6 +16,8 @@ import chat.rocket.persistence.realm.models.ddp.RealmPublicSetting;
import chat.rocket.core.SyncState;
import chat.rocket.persistence.realm.models.ddp.RealmMessage;
import chat.rocket.persistence.realm.models.ddp.RealmRoom;
import chat.rocket.persistence.realm.models.ddp.RealmSpotlightRoom;
import chat.rocket.persistence.realm.models.ddp.RealmSpotlightUser;
import chat.rocket.persistence.realm.models.internal.MethodCall;
import chat.rocket.persistence.realm.models.internal.RealmSession;
import chat.rocket.persistence.realm.RealmHelper;
......@@ -372,6 +374,42 @@ public class MethodCallHelper {
});
}
public Task<Void> searchSpotlightUsers(String term) {
return searchSpotlight(
RealmSpotlightUser.class, "users", term
);
}
public Task<Void> searchSpotlightRooms(String term) {
return searchSpotlight(
RealmSpotlightRoom.class, "rooms", term
);
}
private Task<Void> searchSpotlight(Class clazz, String key, String term) {
return call("spotlight", TIMEOUT_MS, () -> new JSONArray()
.put(term)
.put(JSONObject.NULL)
.put(new JSONObject().put(key, true)))
.onSuccessTask(CONVERT_TO_JSON_OBJECT)
.onSuccessTask(task -> {
final JSONObject result = task.getResult();
if (!result.has(key)) {
return null;
}
Object items = result.get(key);
if (!(items instanceof JSONArray)) {
return null;
}
return realmHelper.executeTransaction(realm -> {
realm.delete(clazz);
realm.createOrUpdateAllFromJson(clazz, (JSONArray) items);
return null;
});
});
}
protected interface ParamBuilder {
JSONArray buildParam() throws JSONException;
......
package chat.rocket.android.fragment.chatroom;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
......@@ -13,6 +12,7 @@ import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.support.v4.app.DialogFragment;
import android.support.v4.os.BuildCompat;
import android.support.v4.util.Pair;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.SlidingPaneLayout;
......@@ -20,12 +20,20 @@ import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import com.fernandocejas.arrow.optional.Optional;
import com.jakewharton.rxbinding2.support.v4.widget.RxDrawerLayout;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import chat.rocket.android.BackgroundLooper;
import chat.rocket.android.R;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.fragment.chatroom.dialog.FileUploadProgressDialogFragment;
......@@ -49,6 +57,14 @@ import chat.rocket.android.layouthelper.extra_action.upload.AudioUploadActionIte
import chat.rocket.android.layouthelper.extra_action.upload.ImageUploadActionItem;
import chat.rocket.android.layouthelper.extra_action.upload.VideoUploadActionItem;
import chat.rocket.android.log.RCLog;
import chat.rocket.android.renderer.RocketChatUserStatusProvider;
import chat.rocket.android.service.temp.DeafultTempSpotlightRoomCaller;
import chat.rocket.android.service.temp.DefaultTempSpotlightUserCaller;
import chat.rocket.android.widget.message.autocomplete.AutocompleteManager;
import chat.rocket.android.widget.message.autocomplete.channel.ChannelSource;
import chat.rocket.android.widget.message.autocomplete.user.UserSource;
import chat.rocket.core.interactors.AutocompleteChannelInteractor;
import chat.rocket.core.interactors.AutocompleteUserInteractor;
import chat.rocket.core.interactors.MessageInteractor;
import chat.rocket.core.interactors.SessionInteractor;
import chat.rocket.core.models.Message;
......@@ -57,6 +73,8 @@ import chat.rocket.persistence.realm.repositories.RealmMessageRepository;
import chat.rocket.persistence.realm.repositories.RealmRoomRepository;
import chat.rocket.persistence.realm.repositories.RealmServerInfoRepository;
import chat.rocket.persistence.realm.repositories.RealmSessionRepository;
import chat.rocket.persistence.realm.repositories.RealmSpotlightRoomRepository;
import chat.rocket.persistence.realm.repositories.RealmSpotlightUserRepository;
import chat.rocket.persistence.realm.repositories.RealmUserRepository;
import chat.rocket.android.layouthelper.chatroom.ModelListAdapter;
import chat.rocket.persistence.realm.RealmStore;
......@@ -87,11 +105,19 @@ public class RoomFragment extends AbstractChatRoomFragment
protected Snackbar unreadIndicator;
private boolean previousUnreadMessageExists;
private MessageListAdapter adapter;
private AutocompleteManager autocompleteManager;
private List<AbstractExtraActionItem> extraActionItems;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
protected RoomContract.Presenter presenter;
private RealmRoomRepository roomRepository;
private RealmUserRepository userRepository;
private MethodCallHelper methodCallHelper;
private AbsoluteUrlHelper absoluteUrlHelper;
public RoomFragment() {
}
......@@ -117,29 +143,31 @@ public class RoomFragment extends AbstractChatRoomFragment
hostname = args.getString(HOSTNAME);
roomId = args.getString(ROOM_ID);
RealmRoomRepository roomRepository = new RealmRoomRepository(hostname);
roomRepository = new RealmRoomRepository(hostname);
MessageInteractor messageInteractor = new MessageInteractor(
new RealmMessageRepository(hostname),
roomRepository
);
RealmUserRepository userRepository = new RealmUserRepository(hostname);
userRepository = new RealmUserRepository(hostname);
AbsoluteUrlHelper absoluteUrlHelper = new AbsoluteUrlHelper(
absoluteUrlHelper = new AbsoluteUrlHelper(
hostname,
new RealmServerInfoRepository(),
userRepository,
new SessionInteractor(new RealmSessionRepository(hostname))
);
methodCallHelper = new MethodCallHelper(getContext(), hostname);
presenter = new RoomPresenter(
roomId,
userRepository,
messageInteractor,
roomRepository,
absoluteUrlHelper,
new MethodCallHelper(getContext(), hostname),
methodCallHelper,
ConnectivityManager.getInstance(getContext())
);
......@@ -238,6 +266,14 @@ public class RoomFragment extends AbstractChatRoomFragment
adapter.unregisterAdapterDataObserver(autoScrollManager);
}
}
compositeDisposable.clear();
if (autocompleteManager != null) {
autocompleteManager.dispose();
autocompleteManager = null;
}
super.onDestroyView();
}
......@@ -246,7 +282,6 @@ public class RoomFragment extends AbstractChatRoomFragment
presenter.onMessageSelected(pairedMessage.target);
}
@SuppressLint("RxLeakedSubscription")
private void setupSideMenu() {
View sideMenu = rootView.findViewById(R.id.room_side_menu);
sideMenu.findViewById(R.id.btn_users).setOnClickListener(view -> {
......@@ -258,7 +293,7 @@ public class RoomFragment extends AbstractChatRoomFragment
DrawerLayout drawerLayout = (DrawerLayout) rootView.findViewById(R.id.drawer_layout);
SlidingPaneLayout pane = (SlidingPaneLayout) getActivity().findViewById(R.id.sliding_pane);
if (drawerLayout != null && pane != null) {
RxDrawerLayout.drawerOpen(drawerLayout, GravityCompat.END)
compositeDisposable.add(RxDrawerLayout.drawerOpen(drawerLayout, GravityCompat.END)
.compose(bindToLifecycle())
.subscribe(
opened -> {
......@@ -271,7 +306,8 @@ public class RoomFragment extends AbstractChatRoomFragment
}
},
Logger::report
);
)
);
}
}
......@@ -290,7 +326,58 @@ public class RoomFragment extends AbstractChatRoomFragment
messageFormManager =
new MessageFormManager(messageFormLayout, this::showExtraActionSelectionDialog);
messageFormManager.setSendMessageCallback(this::sendMessage);
messageFormLayout.setEditTextContentListener(this::onCommitContent);
messageFormLayout.setEditTextCommitContentListener(this::onCommitContent);
autocompleteManager =
new AutocompleteManager((ViewGroup) rootView.findViewById(R.id.message_list_root));
autocompleteManager.registerSource(
new ChannelSource(
new AutocompleteChannelInteractor(
roomRepository,
new RealmSpotlightRoomRepository(hostname),
new DeafultTempSpotlightRoomCaller(methodCallHelper)
),
AndroidSchedulers.from(BackgroundLooper.get()),
AndroidSchedulers.mainThread()
)
);
Disposable disposable = Single.zip(
absoluteUrlHelper.getRocketChatAbsoluteUrl(),
roomRepository.getById(roomId).first(Optional.absent()),
Pair::create
)
.subscribe(
pair -> {
if (pair.first.isPresent() && pair.second.isPresent()) {
autocompleteManager.registerSource(
new UserSource(
new AutocompleteUserInteractor(
pair.second.get(),
userRepository,
new RealmMessageRepository(hostname),
new RealmSpotlightUserRepository(hostname),
new DefaultTempSpotlightUserCaller(methodCallHelper)
),
pair.first.get(),
RocketChatUserStatusProvider.getInstance(),
AndroidSchedulers.from(BackgroundLooper.get()),
AndroidSchedulers.mainThread()
)
);
}
},
throwable -> {
}
);
compositeDisposable.add(disposable);
autocompleteManager.bindTo(
messageFormLayout.getEditText(),
messageFormLayout
);
}
@Override
......
......@@ -38,8 +38,7 @@ public class AbsoluteUrlHelper {
.filter(Optional::isPresent)
.map(Optional::get),
(info, user, session) -> Optional.of(new RocketChatAbsoluteUrl(
info, user,
session
info, user, session
))
)
.first(Optional.absent());
......
package chat.rocket.android.renderer;
import android.support.annotation.DrawableRes;
import chat.rocket.android.R;
import chat.rocket.android.widget.helper.UserStatusProvider;
import chat.rocket.core.models.User;
public class RocketChatUserStatusProvider implements UserStatusProvider {
private static RocketChatUserStatusProvider instance;
private RocketChatUserStatusProvider() {
}
public static RocketChatUserStatusProvider getInstance() {
if (instance == null) {
instance = new RocketChatUserStatusProvider();
}
return instance;
}
@Override
@DrawableRes
public int getStatusResId(String status) {
if (User.STATUS_ONLINE.equals(status)) {
return R.drawable.userstatus_online;
} else if (User.STATUS_AWAY.equals(status)) {
return R.drawable.userstatus_away;
} else if (User.STATUS_BUSY.equals(status)) {
return R.drawable.userstatus_busy;
} else if (User.STATUS_OFFLINE.equals(status)) {
return R.drawable.userstatus_offline;
}
// unknown status is rendered as "offline" status.
return R.drawable.userstatus_offline;
}
}
......@@ -4,7 +4,6 @@ import android.content.Context;
import android.widget.ImageView;
import android.widget.TextView;
import chat.rocket.android.R;
import chat.rocket.android.helper.Avatar;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.widget.AbsoluteUrl;
......@@ -56,18 +55,7 @@ public class UserRenderer extends AbstractRenderer<User> {
}
String status = object.getStatus();
if (User.STATUS_ONLINE.equals(status)) {
imageView.setImageResource(R.drawable.userstatus_online);
} else if (User.STATUS_AWAY.equals(status)) {
imageView.setImageResource(R.drawable.userstatus_away);
} else if (User.STATUS_BUSY.equals(status)) {
imageView.setImageResource(R.drawable.userstatus_busy);
} else if (User.STATUS_OFFLINE.equals(status)) {
imageView.setImageResource(R.drawable.userstatus_offline);
} else {
// unknown status is rendered as "offline" status.
imageView.setImageResource(R.drawable.userstatus_offline);
}
imageView.setImageResource(RocketChatUserStatusProvider.getInstance().getStatusResId(status));
return this;
}
......
package chat.rocket.android.service.temp;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.core.temp.TempSpotlightRoomCaller;
public class DeafultTempSpotlightRoomCaller implements TempSpotlightRoomCaller {
private final MethodCallHelper methodCallHelper;
public DeafultTempSpotlightRoomCaller(MethodCallHelper methodCallHelper) {
this.methodCallHelper = methodCallHelper;
}
@Override
public void search(String term) {
methodCallHelper.searchSpotlightRooms(term);
}
}
package chat.rocket.android.service.temp;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.core.temp.TempSpotlightUserCaller;
public class DefaultTempSpotlightUserCaller implements TempSpotlightUserCaller {
private final MethodCallHelper methodCallHelper;
public DefaultTempSpotlightUserCaller(MethodCallHelper methodCallHelper) {
this.methodCallHelper = methodCallHelper;
}
@Override
public void search(String term) {
methodCallHelper.searchSpotlightUsers(term);
}
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/message_list_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
......
package chat.rocket.persistence.realm;
import io.realm.DynamicRealm;
import io.realm.FieldAttribute;
import io.realm.RealmMigration;
import io.realm.RealmObjectSchema;
import io.realm.RealmSchema;
import chat.rocket.persistence.realm.models.ddp.RealmSpotlightRoom;
import chat.rocket.persistence.realm.models.ddp.RealmSpotlightUser;
public class Migration implements RealmMigration {
@Override
public void migrate(DynamicRealm dynamicRealm, long oldVersion, long newVersion) {
......@@ -12,11 +16,28 @@ public class Migration implements RealmMigration {
RealmSchema schema = dynamicRealm.getSchema();
if (oldVersion == 0) {
RealmObjectSchema roomSchema = schema.get("RealmRoom");
// NOOP
oldVersion++;
}
if (oldVersion == 1) {
schema.create("RealmSpotlightUser")
.addField(RealmSpotlightUser.Columns.ID, String.class, FieldAttribute.PRIMARY_KEY)
.addField(RealmSpotlightUser.Columns.USERNAME, String.class)
.addField(RealmSpotlightUser.Columns.STATUS, String.class);
roomSchema.addField("f", boolean.class);
schema.create("RealmSpotlightRoom")
.addField(RealmSpotlightRoom.Columns.ID, String.class, FieldAttribute.PRIMARY_KEY)
.addField(RealmSpotlightRoom.Columns.NAME, String.class)
.addField(RealmSpotlightRoom.Columns.TYPE, String.class);
oldVersion++;
}
if (oldVersion == 2) {
RealmObjectSchema roomSchema = schema.get("RealmSpotlightUser");
roomSchema.addField(RealmSpotlightUser.Columns.NAME, String.class);
}
}
}
......@@ -14,7 +14,9 @@ public class RealmStore {
return new RealmConfiguration.Builder()
.name(name + ".realm")
.modules(new RocketChatLibraryModule())
.deleteRealmIfMigrationNeeded().build();
.migration(new Migration())
.schemaVersion(3)
.build();
}
public static void put(String name) {
......
......@@ -4,19 +4,11 @@ import android.content.Context;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import chat.rocket.persistence.realm.modules.RocketChatLibraryModule;
public class RocketChatPersistenceRealm {
public static void init(Context context) {
Realm.init(context);
Realm.setDefaultConfiguration(
new RealmConfiguration.Builder()
.name("rocket.chat.persistence.realm")
.modules(new RocketChatLibraryModule())
.schemaVersion(1)
.migration(new Migration())
.build());
Realm.setDefaultConfiguration(new RealmConfiguration.Builder().build());
}
}
package chat.rocket.persistence.realm.models.ddp;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
import chat.rocket.core.models.SpotlightRoom;
public class RealmSpotlightRoom extends RealmObject {
public interface Columns {
String ID = "_id";
String NAME = "name";
String TYPE = "t";
}
@PrimaryKey private String _id;
private String name;
private String t;
public String getId() {
return _id;
}
public void setId(String _id) {
this._id = _id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return t;
}
public void setType(String t) {
this.t = t;
}
public SpotlightRoom asSpotlightRoom() {
return SpotlightRoom.builder()
.setId(_id)
.setName(name)
.setType(t)
.build();
}
}
package chat.rocket.persistence.realm.models.ddp;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
import chat.rocket.core.models.SpotlightUser;
public class RealmSpotlightUser extends RealmObject {
public interface Columns {
String ID = "_id";
String USERNAME = "username";
String NAME = "name";
String STATUS = "status";
}
@PrimaryKey private String _id;
private String username;
private String name;
private String status;
public String getId() {
return _id;
}
public void setId(String _id) {
this._id = _id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public SpotlightUser asSpotlightUser() {
return SpotlightUser.builder()
.setId(_id)
.setUsername(username)
.setName(name)
.setStatus(status)
.build();
}
}
......@@ -3,6 +3,10 @@ package chat.rocket.persistence.realm.repositories;
import android.os.Handler;
import android.os.Looper;
import io.realm.Realm;
import io.realm.RealmObject;
import io.realm.RealmResults;
import java.util.List;
public class RealmRepository {
......@@ -12,4 +16,10 @@ public class RealmRepository {
}
new Handler(looper).post(realm::close);
}
protected <T extends RealmObject> List<T> safeSubList(RealmResults<T> realmObjects,
int fromIndex,
int toIndex) {
return realmObjects.subList(Math.max(0, fromIndex), Math.min(realmObjects.size(), toIndex));
}
}
......@@ -6,11 +6,14 @@ import com.fernandocejas.arrow.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.realm.Case;
import io.realm.Realm;
import io.realm.RealmResults;
import io.realm.Sort;
import java.util.ArrayList;
import java.util.List;
import chat.rocket.core.SortDirection;
import chat.rocket.core.models.Room;
import chat.rocket.core.models.RoomHistoryState;
import chat.rocket.core.repositories.RoomRepository;
......@@ -141,6 +144,50 @@ public class RealmRoomRepository extends RealmRepository implements RoomReposito
});
}
@Override
public Flowable<List<Room>> getSortedLikeName(String name, SortDirection direction, int limit) {
return Flowable.defer(() -> Flowable.using(
() -> new Pair<>(RealmStore.getRealm(hostname), Looper.myLooper()),
pair -> RxJavaInterop.toV2Flowable(
pair.first.where(RealmRoom.class)
.like(RealmRoom.NAME, "*" + name + "*", Case.INSENSITIVE)
.beginGroup()
.equalTo(RealmRoom.TYPE, RealmRoom.TYPE_CHANNEL)
.or()
.equalTo(RealmRoom.TYPE, RealmRoom.TYPE_PRIVATE)
.endGroup()
.findAllSorted(RealmRoom.NAME,
direction.equals(SortDirection.ASC) ? Sort.ASCENDING : Sort.DESCENDING)
.asObservable()),
pair -> close(pair.first, pair.second)
)
.unsubscribeOn(AndroidSchedulers.from(Looper.myLooper()))
.filter(roomSubscriptions -> roomSubscriptions != null && roomSubscriptions.isLoaded()
&& roomSubscriptions.isValid())
.map(realmRooms -> toList(safeSubList(realmRooms, 0, limit))));
}
@Override
public Flowable<List<Room>> getLatestSeen(int limit) {
return Flowable.defer(() -> Flowable.using(
() -> new Pair<>(RealmStore.getRealm(hostname), Looper.myLooper()),
pair -> RxJavaInterop.toV2Flowable(
pair.first.where(RealmRoom.class)
.beginGroup()
.equalTo(RealmRoom.TYPE, RealmRoom.TYPE_CHANNEL)
.or()
.equalTo(RealmRoom.TYPE, RealmRoom.TYPE_PRIVATE)
.endGroup()
.findAllSorted(RealmRoom.LAST_SEEN, Sort.ASCENDING)
.asObservable()),
pair -> close(pair.first, pair.second)
)
.unsubscribeOn(AndroidSchedulers.from(Looper.myLooper()))
.filter(roomSubscriptions -> roomSubscriptions != null && roomSubscriptions.isLoaded()
&& roomSubscriptions.isValid())
.map(realmRooms -> toList(safeSubList(realmRooms, 0, limit))));
}
private List<Room> toList(RealmResults<RealmRoom> realmRooms) {
int total = realmRooms.size();
......@@ -152,4 +199,16 @@ public class RealmRoomRepository extends RealmRepository implements RoomReposito
return roomList;
}
private List<Room> toList(List<RealmRoom> realmRooms) {
int total = realmRooms.size();
final List<Room> roomList = new ArrayList<>(total);
for (int i = 0; i < total; i++) {
roomList.add(realmRooms.get(i).asRoom());
}
return roomList;
}
}
package chat.rocket.persistence.realm.repositories;
import android.os.Looper;
import android.support.v4.util.Pair;
import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.realm.Case;
import io.realm.Sort;
import java.util.ArrayList;
import java.util.List;
import chat.rocket.core.SortDirection;
import chat.rocket.core.models.SpotlightRoom;
import chat.rocket.core.repositories.SpotlightRoomRepository;
import chat.rocket.persistence.realm.RealmStore;
import chat.rocket.persistence.realm.models.ddp.RealmRoom;
import chat.rocket.persistence.realm.models.ddp.RealmSpotlightRoom;
import hu.akarnokd.rxjava.interop.RxJavaInterop;
public class RealmSpotlightRoomRepository extends RealmRepository
implements SpotlightRoomRepository {
private final String hostname;
public RealmSpotlightRoomRepository(String hostname) {
this.hostname = hostname;
}
@Override
public Flowable<List<SpotlightRoom>> getSuggestionsFor(String name, SortDirection direction,
int limit) {
return Flowable.defer(() -> Flowable.using(
() -> new Pair<>(RealmStore.getRealm(hostname), Looper.myLooper()),
pair -> RxJavaInterop.toV2Flowable(
pair.first.where(RealmSpotlightRoom.class)
.like(RealmSpotlightRoom.Columns.NAME, "*" + name + "*", Case.INSENSITIVE)
.beginGroup()
.equalTo(RealmSpotlightRoom.Columns.TYPE, RealmRoom.TYPE_CHANNEL)
.or()
.equalTo(RealmSpotlightRoom.Columns.TYPE, RealmRoom.TYPE_PRIVATE)
.endGroup()
.findAllSorted(RealmSpotlightRoom.Columns.NAME,
direction.equals(SortDirection.ASC) ? Sort.ASCENDING : Sort.DESCENDING)
.asObservable()),
pair -> close(pair.first, pair.second)
)
.unsubscribeOn(AndroidSchedulers.from(Looper.myLooper()))
.filter(it -> it != null && it.isLoaded() && it.isValid())
.map(realmSpotlightRooms -> toList(safeSubList(realmSpotlightRooms, 0, limit))));
}
private List<SpotlightRoom> toList(List<RealmSpotlightRoom> realmSpotlightRooms) {
int total = realmSpotlightRooms.size();
final List<SpotlightRoom> spotlightRooms = new ArrayList<>(total);
for (int i = 0; i < total; i++) {
spotlightRooms.add(realmSpotlightRooms.get(i).asSpotlightRoom());
}
return spotlightRooms;
}
}
package chat.rocket.persistence.realm.repositories;
import android.os.Looper;
import android.support.v4.util.Pair;
import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.realm.Case;
import io.realm.Sort;
import java.util.ArrayList;
import java.util.List;
import chat.rocket.core.SortDirection;
import chat.rocket.core.models.SpotlightUser;
import chat.rocket.core.repositories.SpotlightUserRepository;
import chat.rocket.persistence.realm.RealmStore;
import chat.rocket.persistence.realm.models.ddp.RealmSpotlightUser;
import hu.akarnokd.rxjava.interop.RxJavaInterop;
public class RealmSpotlightUserRepository extends RealmRepository
implements SpotlightUserRepository {
private final String hostname;
public RealmSpotlightUserRepository(String hostname) {
this.hostname = hostname;
}
@Override
public Flowable<List<SpotlightUser>> getSuggestionsFor(String name, SortDirection direction,
int limit) {
return Flowable.defer(() -> Flowable.using(
() -> new Pair<>(RealmStore.getRealm(hostname), Looper.myLooper()),
pair -> RxJavaInterop.toV2Flowable(
pair.first.where(RealmSpotlightUser.class)
.beginGroup()
.like(RealmSpotlightUser.Columns.USERNAME, "*" + name + "*", Case.INSENSITIVE)
.isNull(RealmSpotlightUser.Columns.NAME)
.endGroup()
.or()
.beginGroup()
.like(RealmSpotlightUser.Columns.NAME, "*" + name + "*", Case.INSENSITIVE)
.isNotNull(RealmSpotlightUser.Columns.USERNAME)
.endGroup()
.findAllSorted(RealmSpotlightUser.Columns.USERNAME,
direction.equals(SortDirection.ASC) ? Sort.ASCENDING : Sort.DESCENDING)
.asObservable()),
pair -> close(pair.first, pair.second)
)
.unsubscribeOn(AndroidSchedulers.from(Looper.myLooper()))
.filter(it -> it != null && it.isLoaded() && it.isValid())
.map(realmSpotlightUsers -> toList(safeSubList(realmSpotlightUsers, 0, limit))));
}
private List<SpotlightUser> toList(List<RealmSpotlightUser> realmSpotlightUsers) {
int total = realmSpotlightUsers.size();
final List<SpotlightUser> spotlightUsers = new ArrayList<>(total);
for (int i = 0; i < total; i++) {
spotlightUsers.add(realmSpotlightUsers.get(i).asSpotlightUser());
}
return spotlightUsers;
}
}
......@@ -5,8 +5,13 @@ import android.support.v4.util.Pair;
import com.fernandocejas.arrow.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.realm.Case;
import io.realm.RealmResults;
import io.realm.Sort;
import java.util.ArrayList;
import java.util.List;
import chat.rocket.core.SortDirection;
import chat.rocket.core.models.User;
import chat.rocket.core.repositories.UserRepository;
import chat.rocket.persistence.realm.RealmStore;
......@@ -43,4 +48,46 @@ public class RealmUserRepository extends RealmRepository implements UserReposito
return Optional.<User>absent();
}));
}
@Override
public Flowable<List<User>> getSortedLikeName(String name, SortDirection direction, int limit) {
return Flowable.defer(() -> Flowable.using(
() -> new Pair<>(RealmStore.getRealm(hostname), Looper.myLooper()),
pair -> RxJavaInterop.toV2Flowable(
pair.first.where(RealmUser.class)
.like(RealmUser.USERNAME, "*" + name + "*", Case.INSENSITIVE)
.findAllSorted(RealmUser.USERNAME,
direction.equals(SortDirection.ASC) ? Sort.ASCENDING : Sort.DESCENDING)
.asObservable()),
pair -> close(pair.first, pair.second)
)
.unsubscribeOn(AndroidSchedulers.from(Looper.myLooper()))
.filter(realmUsers -> realmUsers != null && realmUsers.isLoaded()
&& realmUsers.isValid())
.map(realmUsers -> toList(safeSubList(realmUsers, 0, limit))));
}
private List<User> toList(RealmResults<RealmUser> realmUsers) {
int total = realmUsers.size();
final List<User> userList = new ArrayList<>(total);
for (int i = 0; i < total; i++) {
userList.add(realmUsers.get(i).asUser());
}
return userList;
}
private List<User> toList(List<RealmUser> realmUsers) {
int total = realmUsers.size();
final List<User> userList = new ArrayList<>(total);
for (int i = 0; i < total; i++) {
userList.add(realmUsers.get(i).asUser());
}
return userList;
}
}
......@@ -32,6 +32,7 @@ android {
ext {
supportVersion = '25.2.0'
frescoVersion = '1.1.0'
rxbindingVersion = '2.0.0'
}
dependencies {
......@@ -39,6 +40,8 @@ dependencies {
compile "com.android.support:support-annotations:$supportVersion"
compile "com.android.support:appcompat-v7:$supportVersion"
compile "com.android.support:recyclerview-v7:$supportVersion"
compile "com.android.support:cardview-v7:$supportVersion"
compile "com.android.support:support-v13:$supportVersion"
compile "com.android.support:design:$supportVersion"
......@@ -58,5 +61,9 @@ dependencies {
compile 'com.caverock:androidsvg:1.2.1'
compile "com.jakewharton.rxbinding2:rxbinding:$rxbindingVersion"
compile "com.jakewharton.rxbinding2:rxbinding-support-v4:$rxbindingVersion"
testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:2.7.19"
}
package chat.rocket.android.widget.helper;
import android.support.annotation.StringRes;
import java.util.HashMap;
import chat.rocket.android.widget.R;
public class IconProvider {
private static HashMap<String, Integer> ICON_TABLE = new HashMap<String, Integer>() {
{
put("c", R.string.fa_hashtag);
put("p", R.string.fa_lock);
put("d", R.string.fa_at);
}
};
@StringRes
public static int getIcon(String type) {
if (ICON_TABLE.containsKey(type)) {
return ICON_TABLE.get(type);
}
return ICON_TABLE.get("c");
}
}
package chat.rocket.android.widget.helper;
import android.support.annotation.DrawableRes;
public interface UserStatusProvider {
@DrawableRes
int getStatusResId(String status);
}
package chat.rocket.android.widget.message;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.v13.view.inputmethod.EditorInfoCompat;
import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.support.v7.widget.AppCompatEditText;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.EditText;
public class ImageKeyboardEditText extends EditText {
public class ImageKeyboardEditText extends AppCompatEditText {
private final String[] mimeTypes = {"image/gif"};
......@@ -43,12 +41,6 @@ public class ImageKeyboardEditText extends EditText {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ImageKeyboardEditText(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
final InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
......
......@@ -12,6 +12,7 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
......@@ -115,6 +116,10 @@ public class MessageFormLayout extends LinearLayout {
addView(composer);
}
public EditText getEditText() {
return (EditText) composer.findViewById(R.id.editor);
}
public void setExtraActionSelectionClickListener(
ExtraActionSelectionClickListener extraActionSelectionClickListener) {
this.extraActionSelectionClickListener = extraActionSelectionClickListener;
......@@ -153,7 +158,8 @@ public class MessageFormLayout extends LinearLayout {
composer.findViewById(R.id.btn_submit).setEnabled(enabled);
}
public void setEditTextContentListener(ImageKeyboardEditText.OnCommitContentListener listener) {
public void setEditTextCommitContentListener(
ImageKeyboardEditText.OnCommitContentListener listener) {
this.listener = listener;
}
......
package chat.rocket.android.widget.message.autocomplete;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
public abstract class AutocompleteAdapter<I extends AutocompleteItem, H extends AutocompleteViewHolder<I>>
extends RecyclerView.Adapter<H> {
private static final int TYPE_EMPTY = 0;
private static final int TYPE_ITEM = 1;
private List<I> autocompleteItems = new ArrayList<>();
protected AutocompleteViewHolder.OnClickListener<I> onClickListener;
public void setAutocompleteItems(List<I> autocompleteItems) {
this.autocompleteItems.clear();
this.autocompleteItems.addAll(autocompleteItems);
notifyDataSetChanged();
}
public H onCreateViewHolder(ViewGroup parent, int viewType) {
H holder = getViewHolder(parent);
if (viewType == TYPE_EMPTY) {
holder.showAsEmpty();
}
return holder;
}
@Override
public void onBindViewHolder(H holder, int position) {
if (getItemViewType(position) == TYPE_EMPTY) {
return;
}
holder.bind(autocompleteItems.get(position));
}
@Override
public int getItemCount() {
int count = autocompleteItems.size();
if (count == 0) {
return 1;
}
return count;
}
@Override
public int getItemViewType(int position) {
if (autocompleteItems.size() == 0) {
return TYPE_EMPTY;
}
return TYPE_ITEM;
}
public abstract H getViewHolder(ViewGroup parent);
public void setOnClickListener(AutocompleteViewHolder.OnClickListener<I> onClickListener) {
this.onClickListener = onClickListener;
}
}
package chat.rocket.android.widget.message.autocomplete;
import android.support.annotation.NonNull;
public interface AutocompleteItem {
@NonNull
String getSuggestion();
}
package chat.rocket.android.widget.message.autocomplete;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.EditText;
import android.widget.RelativeLayout;
import com.jakewharton.rxbinding2.widget.RxTextView;
import com.jakewharton.rxbinding2.widget.TextViewAfterTextChangeEvent;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.internal.util.AppendOnlyLinkedArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import chat.rocket.android.widget.R;
public class AutocompleteManager {
private final Map<String, AutocompleteSource> autocompleteSourceMap = new HashMap<>();
private AutocompleteSource currentSource;
private Disposable afterTextChangeDisposable;
private CompositeDisposable sourceDisposable = new CompositeDisposable();
private EditText editText;
private String text;
private int fromIndex;
private int toIndex;
private final View contentHolder;
private final RecyclerView recyclerView;
private float contentHolderInitialY;
private final float yOffset;
private final AutocompleteSource.OnAutocompleteSelected onAutocompleteSelected =
new AutocompleteSource.OnAutocompleteSelected() {
@Override
public void onSelected(String autocompleteSuggestion) {
replaceSelected(autocompleteSuggestion);
}
};
public AutocompleteManager(ViewGroup parent) {
contentHolder =
LayoutInflater.from(parent.getContext()).inflate(R.layout.autocomplete_box, parent, false);
contentHolder.setVisibility(View.GONE);
recyclerView = (RecyclerView) contentHolder.findViewById(R.id.autocomplete_list);
recyclerView.setLayoutManager(new LinearLayoutManager(parent.getContext()));
parent.addView(contentHolder);
yOffset = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
32,
contentHolder.getContext().getResources().getDisplayMetrics()
);
}
public void registerSource(AutocompleteSource autocompleteSource) {
autocompleteSourceMap.put(autocompleteSource.getTrigger(), autocompleteSource);
}
public void bindTo(EditText editText, View anchor) {
this.editText = editText;
if (contentHolder.getLayoutParams() instanceof RelativeLayout.LayoutParams) {
RelativeLayout.LayoutParams layoutParams =
(RelativeLayout.LayoutParams) contentHolder.getLayoutParams();
layoutParams.addRule(RelativeLayout.ABOVE, anchor.getId());
contentHolder.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
contentHolderInitialY = contentHolder.getY();
animateHide();
contentHolder.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
afterTextChangeDisposable = RxTextView.afterTextChangeEvents(editText)
.debounce(300, TimeUnit.MILLISECONDS)
.filter(new AppendOnlyLinkedArrayList.NonThrowingPredicate<TextViewAfterTextChangeEvent>() {
@Override
public boolean test(TextViewAfterTextChangeEvent textViewAfterTextChangeEvent) {
return textViewAfterTextChangeEvent.editable() != null;
}
})
.map(new Function<TextViewAfterTextChangeEvent, String>() {
@Override
public String apply(@NonNull TextViewAfterTextChangeEvent textViewAfterTextChangeEvent)
throws Exception {
//noinspection ConstantConditions
return textViewAfterTextChangeEvent.editable().toString();
}
})
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(new Consumer<String>() {
@Override
public void accept(@NonNull String s) throws Exception {
// let's do stateful things
tryToAutocomplete(s);
}
})
.subscribe();
}
public void dispose() {
if (afterTextChangeDisposable != null) {
afterTextChangeDisposable.dispose();
afterTextChangeDisposable = null;
}
editText = null;
cleanState();
}
private void tryToAutocomplete(String text) {
if (editText == null) {
cleanState();
return;
}
final int selectionStart = editText.getSelectionStart();
final int selectionEnd = editText.getSelectionEnd();
if (selectionStart != selectionEnd) {
// selecting text
cleanState();
return;
}
final String toCompleteText = getToCompleteText(text, selectionStart);
final AutocompleteSource source = getSource(toCompleteText);
if (source == null) {
cleanState();
return;
}
// render and stuff
if (source != currentSource) {
cleanState();
currentSource = source;
// set adapter on something
recyclerView.setAdapter(currentSource.getAdapter());
currentSource.setOnAutocompleteSelected(onAutocompleteSelected);
}
this.text = text;
animateShow();
sourceDisposable.clear();
sourceDisposable.add(currentSource.loadList(toCompleteText));
}
private void cleanState() {
animateHide();
sourceDisposable.clear();
text = null;
if (currentSource != null) {
currentSource.dispose();
currentSource = null;
}
}
private String getToCompleteText(String text, int cursorPosition) {
if (text == null || text.length() == 0 || cursorPosition < 0
|| cursorPosition > text.length()) {
return "";
}
final String[] textParts = text.split(" ");
int currentPos = 0;
for (String textPart : textParts) {
int currentLength = currentPos + textPart.length();
if (cursorPosition >= currentPos && cursorPosition <= currentLength) {
fromIndex = currentPos;
toIndex = cursorPosition;
return textPart.substring(0, cursorPosition - currentPos);
}
currentPos = currentLength + 1;
}
return "";
}
private AutocompleteSource getSource(String toCompleteText) {
if (toCompleteText == null || toCompleteText.length() == 0) {
return null;
}
final String trigger = toCompleteText.substring(0, 1);
return autocompleteSourceMap.get(trigger);
}
private void replaceSelected(String autocompleteSuggestion) {
final String preText = text.substring(0, fromIndex);
final String postText = text.substring(toIndex);
StringBuilder stringBuilder =
new StringBuilder(text.length() + autocompleteSuggestion.length());
stringBuilder.append(preText)
.append(autocompleteSuggestion)
.append(' ');
final int selectionPos = stringBuilder.length();
stringBuilder.append(postText);
editText.setText(stringBuilder.toString());
editText.setSelection(selectionPos);
}
private void animateHide() {
contentHolder.animate().cancel();
contentHolder.animate()
.alpha(0)
.translationY(contentHolderInitialY + yOffset)
.setDuration(150)
.withEndAction(new Runnable() {
@Override
public void run() {
contentHolder.setVisibility(View.GONE);
}
});
}
private void animateShow() {
contentHolder.animate().cancel();
contentHolder.animate()
.alpha(1)
.translationY(contentHolderInitialY)
.setDuration(150)
.withStartAction(new Runnable() {
@Override
public void run() {
contentHolder.setVisibility(View.VISIBLE);
}
});
}
}
package chat.rocket.android.widget.message.autocomplete;
import android.support.annotation.NonNull;
import io.reactivex.disposables.Disposable;
public abstract class AutocompleteSource<A extends AutocompleteAdapter, I extends AutocompleteItem> {
protected A adapter;
private AutocompleteSource.OnAutocompleteSelected autocompleteSelected;
private AutocompleteViewHolder.OnClickListener autocompleteListener =
new AutocompleteViewHolder.OnClickListener<I>() {
@Override
public void onClick(I autocompleteItem) {
if (autocompleteSelected != null) {
autocompleteSelected.onSelected(getAutocompleteSuggestion(autocompleteItem));
}
}
};
@NonNull
public abstract String getTrigger();
@NonNull
public A getAdapter() {
if (adapter == null) {
adapter = createAdapter();
adapter.setOnClickListener(autocompleteListener);
}
return adapter;
}
@NonNull
public abstract Disposable loadList(String text);
public abstract void dispose();
public void setOnAutocompleteSelected(
AutocompleteSource.OnAutocompleteSelected autocompleteSelected) {
this.autocompleteSelected = autocompleteSelected;
}
protected abstract A createAdapter();
protected abstract String getAutocompleteSuggestion(I autocompleteItem);
public interface OnAutocompleteSelected {
void onSelected(String autocompleteSuggestion);
}
}
package chat.rocket.android.widget.message.autocomplete;
import android.support.v7.widget.RecyclerView;
import android.view.View;
public abstract class AutocompleteViewHolder<I extends AutocompleteItem>
extends RecyclerView.ViewHolder {
public AutocompleteViewHolder(View itemView) {
super(itemView);
}
public abstract void bind(I autocompleteItem);
public abstract void showAsEmpty();
public interface OnClickListener<I extends AutocompleteItem> {
void onClick(I autocompleteItem);
}
}
package chat.rocket.android.widget.message.autocomplete.channel;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import chat.rocket.android.widget.R;
import chat.rocket.android.widget.message.autocomplete.AutocompleteAdapter;
public class ChannelAdapter extends AutocompleteAdapter<ChannelItem, ChannelViewHolder> {
@Override
public ChannelViewHolder getViewHolder(ViewGroup parent) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.autocomplete_channel_view, parent, false);
return new ChannelViewHolder(view, onClickListener);
}
}
package chat.rocket.android.widget.message.autocomplete.channel;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import chat.rocket.android.widget.helper.IconProvider;
import chat.rocket.android.widget.message.autocomplete.AutocompleteItem;
import chat.rocket.core.models.SpotlightRoom;
public class ChannelItem implements AutocompleteItem {
private final SpotlightRoom spotlightRoom;
public ChannelItem(@NonNull SpotlightRoom spotlightRoom) {
this.spotlightRoom = spotlightRoom;
}
@NonNull
@Override
public String getSuggestion() {
return spotlightRoom.getName();
}
@StringRes
public int getIcon() {
return IconProvider.getIcon(spotlightRoom.getType());
}
}
package chat.rocket.android.widget.message.autocomplete.channel;
import android.support.annotation.NonNull;
import io.reactivex.Flowable;
import io.reactivex.Scheduler;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import org.reactivestreams.Publisher;
import java.util.ArrayList;
import java.util.List;
import chat.rocket.android.widget.message.autocomplete.AutocompleteSource;
import chat.rocket.core.interactors.AutocompleteChannelInteractor;
import chat.rocket.core.models.SpotlightRoom;
public class ChannelSource extends AutocompleteSource<ChannelAdapter, ChannelItem> {
private final AutocompleteChannelInteractor autocompleteChannelInteractor;
private final Scheduler bgScheduler;
private final Scheduler fgScheduler;
public ChannelSource(AutocompleteChannelInteractor autocompleteChannelInteractor,
Scheduler bgScheduler,
Scheduler fgScheduler) {
this.autocompleteChannelInteractor = autocompleteChannelInteractor;
this.bgScheduler = bgScheduler;
this.fgScheduler = fgScheduler;
}
@NonNull
@Override
public String getTrigger() {
return "#";
}
@NonNull
@Override
public Disposable loadList(String text) {
return Flowable.just(text)
.map(new Function<String, String>() {
@Override
public String apply(@io.reactivex.annotations.NonNull String s) throws Exception {
return s.substring(1);
}
})
.flatMap(new Function<String, Publisher<List<SpotlightRoom>>>() {
@Override
public Publisher<List<SpotlightRoom>> apply(@io.reactivex.annotations.NonNull String s)
throws Exception {
return autocompleteChannelInteractor.getSuggestionsFor(s);
}
})
.distinctUntilChanged()
.map(new Function<List<SpotlightRoom>, List<ChannelItem>>() {
@Override
public List<ChannelItem> apply(
@io.reactivex.annotations.NonNull List<SpotlightRoom> spotlightRooms)
throws Exception {
return toChannelItemList(spotlightRooms);
}
})
.subscribeOn(bgScheduler)
.observeOn(fgScheduler)
.subscribe(
new Consumer<List<ChannelItem>>() {
@Override
public void accept(@io.reactivex.annotations.NonNull List<ChannelItem> channelItems)
throws Exception {
if (adapter != null) {
adapter.setAutocompleteItems(channelItems);
}
}
},
new Consumer<Throwable>() {
@Override
public void accept(@io.reactivex.annotations.NonNull Throwable throwable)
throws Exception {
}
}
);
}
@Override
public void dispose() {
adapter = null;
}
@Override
protected ChannelAdapter createAdapter() {
return new ChannelAdapter();
}
@Override
protected String getAutocompleteSuggestion(ChannelItem autocompleteItem) {
return getTrigger() + autocompleteItem.getSuggestion();
}
private List<ChannelItem> toChannelItemList(List<SpotlightRoom> spotlightRooms) {
int size = spotlightRooms.size();
List<ChannelItem> channelItems = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
channelItems.add(new ChannelItem(spotlightRooms.get(i)));
}
return channelItems;
}
}
package chat.rocket.android.widget.message.autocomplete.channel;
import android.view.View;
import android.widget.TextView;
import chat.rocket.android.widget.R;
import chat.rocket.android.widget.message.autocomplete.AutocompleteViewHolder;
public class ChannelViewHolder extends AutocompleteViewHolder<ChannelItem> {
private final TextView titleTextView;
private final TextView iconTextView;
public ChannelViewHolder(View itemView,
final AutocompleteViewHolder.OnClickListener<ChannelItem> onClickListener) {
super(itemView);
titleTextView = (TextView) itemView.findViewById(R.id.title);
iconTextView = (TextView) itemView.findViewById(R.id.icon);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onClickListener != null) {
onClickListener.onClick((ChannelItem) v.getTag());
}
}
});
}
@Override
public void bind(ChannelItem channelItem) {
itemView.setTag(channelItem);
if (titleTextView != null) {
titleTextView.setText(channelItem.getSuggestion());
}
if (iconTextView != null) {
iconTextView.setText(channelItem.getIcon());
}
}
@Override
public void showAsEmpty() {
iconTextView.setVisibility(View.GONE);
titleTextView.setText(R.string.no_channel_found);
}
}
package chat.rocket.android.widget.message.autocomplete.user;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import chat.rocket.android.widget.R;
import chat.rocket.android.widget.message.autocomplete.AutocompleteAdapter;
public class UserAdapter extends AutocompleteAdapter<UserItem, UserViewHolder> {
@Override
public UserViewHolder getViewHolder(ViewGroup parent) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.autocomplete_user_view, parent, false);
return new UserViewHolder(view, onClickListener);
}
}
package chat.rocket.android.widget.message.autocomplete.user;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import chat.rocket.android.widget.AbsoluteUrl;
import chat.rocket.android.widget.helper.UserStatusProvider;
import chat.rocket.android.widget.message.autocomplete.AutocompleteItem;
import chat.rocket.core.models.SpotlightUser;
public class UserItem implements AutocompleteItem {
private final SpotlightUser user;
private final AbsoluteUrl absoluteUrl;
private final UserStatusProvider userStatusProvider;
public UserItem(@NonNull SpotlightUser user, AbsoluteUrl absoluteUrl,
UserStatusProvider userStatusProvider) {
this.user = user;
this.absoluteUrl = absoluteUrl;
this.userStatusProvider = userStatusProvider;
}
@NonNull
@Override
public String getSuggestion() {
//noinspection ConstantConditions
return user.getUsername();
}
public AbsoluteUrl getAbsoluteUrl() {
return absoluteUrl;
}
@DrawableRes
public int getStatusResId() {
return userStatusProvider.getStatusResId(user.getStatus());
}
}
package chat.rocket.android.widget.message.autocomplete.user;
import android.support.annotation.NonNull;
import io.reactivex.Flowable;
import io.reactivex.Scheduler;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import org.reactivestreams.Publisher;
import java.util.ArrayList;
import java.util.List;
import chat.rocket.android.widget.AbsoluteUrl;
import chat.rocket.android.widget.helper.UserStatusProvider;
import chat.rocket.android.widget.message.autocomplete.AutocompleteSource;
import chat.rocket.core.interactors.AutocompleteUserInteractor;
import chat.rocket.core.models.SpotlightUser;
public class UserSource extends AutocompleteSource<UserAdapter, UserItem> {
private final AutocompleteUserInteractor autocompleteUserInteractor;
private final AbsoluteUrl absoluteUrl;
private final UserStatusProvider userStatusProvider;
private final Scheduler bgScheduler;
private final Scheduler fgScheduler;
public UserSource(AutocompleteUserInteractor autocompleteUserInteractor,
AbsoluteUrl absoluteUrl, UserStatusProvider userStatusProvider,
Scheduler bgScheduler, Scheduler fgScheduler) {
this.autocompleteUserInteractor = autocompleteUserInteractor;
this.absoluteUrl = absoluteUrl;
this.userStatusProvider = userStatusProvider;
this.bgScheduler = bgScheduler;
this.fgScheduler = fgScheduler;
}
@NonNull
@Override
public String getTrigger() {
return "@";
}
@NonNull
@Override
public Disposable loadList(String text) {
return Flowable.just(text)
.map(new Function<String, String>() {
@Override
public String apply(@io.reactivex.annotations.NonNull String s) throws Exception {
return s.substring(1);
}
})
.flatMap(new Function<String, Publisher<List<SpotlightUser>>>() {
@Override
public Publisher<List<SpotlightUser>> apply(@io.reactivex.annotations.NonNull String s)
throws Exception {
return autocompleteUserInteractor.getSuggestionsFor(s);
}
})
.distinctUntilChanged()
.map(new Function<List<SpotlightUser>, List<UserItem>>() {
@Override
public List<UserItem> apply(@io.reactivex.annotations.NonNull List<SpotlightUser> users)
throws Exception {
return toUserItemList(users);
}
})
.subscribeOn(bgScheduler)
.observeOn(fgScheduler)
.subscribe(
new Consumer<List<UserItem>>() {
@Override
public void accept(@io.reactivex.annotations.NonNull List<UserItem> userItems)
throws Exception {
if (adapter != null) {
adapter.setAutocompleteItems(userItems);
}
}
},
new Consumer<Throwable>() {
@Override
public void accept(@io.reactivex.annotations.NonNull Throwable throwable)
throws Exception {
}
}
);
}
@Override
public void dispose() {
adapter = null;
}
@Override
protected UserAdapter createAdapter() {
return new UserAdapter();
}
@Override
protected String getAutocompleteSuggestion(UserItem autocompleteItem) {
return getTrigger() + autocompleteItem.getSuggestion();
}
private List<UserItem> toUserItemList(List<SpotlightUser> users) {
int size = users.size();
List<UserItem> userItems = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
userItems.add(new UserItem(users.get(i), absoluteUrl, userStatusProvider));
}
return userItems;
}
}
package chat.rocket.android.widget.message.autocomplete.user;
import android.content.Context;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.amulyakhare.textdrawable.TextDrawable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import chat.rocket.android.widget.AbsoluteUrl;
import chat.rocket.android.widget.R;
import chat.rocket.android.widget.RocketChatAvatar;
import chat.rocket.android.widget.message.autocomplete.AutocompleteViewHolder;
public class UserViewHolder extends AutocompleteViewHolder<UserItem> {
private static final int[] COLORS = new int[]{
0xFFF44336, 0xFFE91E63, 0xFF9C27B0, 0xFF673AB7, 0xFF3F51B5, 0xFF2196F3,
0xFF03A9F4, 0xFF00BCD4, 0xFF009688, 0xFF4CAF50, 0xFF8BC34A, 0xFFCDDC39,
0xFFFFC107, 0xFFFF9800, 0xFFFF5722, 0xFF795548, 0xFF9E9E9E, 0xFF607D8B
};
private final TextView titleTextView;
private final RocketChatAvatar avatar;
private final ImageView status;
public UserViewHolder(View itemView,
final AutocompleteViewHolder.OnClickListener<UserItem> onClickListener) {
super(itemView);
titleTextView = (TextView) itemView.findViewById(R.id.title);
avatar = (RocketChatAvatar) itemView.findViewById(R.id.avatar);
status = (ImageView) itemView.findViewById(R.id.status);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onClickListener != null) {
onClickListener.onClick((UserItem) v.getTag());
}
}
});
}
@Override
public void bind(UserItem userItem) {
itemView.setTag(userItem);
final String suggestion = userItem.getSuggestion();
if (titleTextView != null) {
titleTextView.setText(suggestion);
}
if (avatar != null) {
avatar.loadImage(
getImageUrl(suggestion, userItem.getAbsoluteUrl()),
getTextDrawable(itemView.getContext(), suggestion)
);
}
if (status != null) {
status.setImageResource(userItem.getStatusResId());
}
}
@Override
public void showAsEmpty() {
status.setVisibility(View.GONE);
avatar.setVisibility(View.GONE);
titleTextView.setText(R.string.no_user_found);
}
private String getImageUrl(String username, AbsoluteUrl absoluteUrl) {
//from Rocket.Chat:packages/rocketchat-ui/lib/avatar.coffee
//REMARK! this is often SVG image! (see: Rocket.Chat:server/startup/avatar.coffee)
try {
final String avatarUrl = "/avatar/" + URLEncoder.encode(username, "UTF-8") + ".jpg";
if (absoluteUrl == null) {
return avatarUrl;
}
return absoluteUrl.from(avatarUrl);
} catch (UnsupportedEncodingException exception) {
return null;
}
}
private Drawable getTextDrawable(Context context, String username) {
if (username == null) {
return null;
}
int round = (int) (4 * context.getResources().getDisplayMetrics().density);
return TextDrawable.builder()
.beginConfig()
.useFont(Typeface.SANS_SERIF)
.endConfig()
.buildRoundRect(getInitialsForUser(username), getColorForUser(username), round);
}
private int getColorForUser(String username) {
return COLORS[username.length() % COLORS.length];
}
private String getInitialsForUser(String username) {
String name = username
.replaceAll("[^A-Za-z0-9]", ".")
.replaceAll("\\.+", ".")
.replaceAll("(^\\.)|(\\.$)", "");
String[] initials = name.split("\\.");
if (initials.length >= 2) {
return (firstChar(initials[0]) + firstChar(initials[initials.length - 1])).toUpperCase();
} else {
String name2 = name.replaceAll("[^A-Za-z0-9]", "");
return (name2.length() < 2) ? name2 : name2.substring(0, 2).toUpperCase();
}
}
private static String firstChar(String str) {
return TextUtils.isEmpty(str) ? "" : str.substring(0, 1);
}
}
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/darker_gray" />
</shape>
</item>
<item android:bottom="1dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
</shape>
</item>
</layer-list>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginLeft="8dp"
android:layout_marginBottom="-8dp"
app:cardCornerRadius="4dp"
app:contentPadding="4dp">
<android.support.v7.widget.RecyclerView
android:id="@+id/autocomplete_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.v7.widget.CardView>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<io.github.yusukeiwaki.android.widget.FontAwesomeTextView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
tools:text="#" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="YOLO" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/status"
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp" />
<chat.rocket.android.widget.RocketChatAvatar
android:id="@+id/avatar"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="YOLO" />
</LinearLayout>
\ No newline at end of file
......@@ -2,4 +2,6 @@
<resources>
<string name="message_composer_message_hint">Message</string>
<string name="click_to_load">Click to load</string>
<string name="no_channel_found">No channel found</string>
<string name="no_user_found">No user found</string>
</resources>
\ No newline at end of file
plugins {
id "net.ltgt.apt" version "0.9"
id "me.tatarka.retrolambda" version "3.5.0"
id "org.jetbrains.kotlin.jvm" version "1.1.1"
}
apply plugin: 'idea'
......@@ -9,6 +8,8 @@ apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.1'
compile 'com.google.code.findbugs:jsr305:3.0.1'
compile 'io.reactivex.rxjava2:rxjava:2.0.6'
......@@ -16,9 +17,12 @@ dependencies {
compile 'com.fernandocejas:arrow:1.0.0'
compile 'com.google.auto.value:auto-value:1.3'
apt 'com.google.auto.value:auto-value:1.3'
apt 'com.gabrielittner.auto.value:auto-value-with:1.0.0'
kapt 'com.google.auto.value:auto-value:1.3'
kapt 'com.gabrielittner.auto.value:auto-value-with:1.0.0'
testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:2.7.19"
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
package chat.rocket.core;
public enum SortDirection {
ASC,
DESC
}
package chat.rocket.core.interactors
import io.reactivex.Flowable
import java.util.ArrayList
import chat.rocket.core.SortDirection
import chat.rocket.core.models.Room
import chat.rocket.core.models.SpotlightRoom
import chat.rocket.core.repositories.RoomRepository
import chat.rocket.core.repositories.SpotlightRoomRepository
import chat.rocket.core.temp.TempSpotlightRoomCaller
import chat.rocket.core.utils.Pair
import chat.rocket.core.utils.Triple
import io.reactivex.functions.BiFunction
import io.reactivex.functions.Function3
class AutocompleteChannelInteractor(private val roomRepository: RoomRepository,
private val spotlightRoomRepository: SpotlightRoomRepository,
private val tempSpotlightRoomCaller: TempSpotlightRoomCaller) {
fun getSuggestionsFor(name: String): Flowable<List<SpotlightRoom>> {
return Flowable.zip<String, List<SpotlightRoom>, List<SpotlightRoom>, Triple<String, List<SpotlightRoom>, List<SpotlightRoom>>>(
Flowable.just(name),
roomRepository.getLatestSeen(5).map { toSpotlightRooms(it) },
roomRepository.getSortedLikeName(name, SortDirection.DESC, 5).map { toSpotlightRooms(it) },
Function3 { a, b, c -> Triple.create(a, b, c) }
)
.flatMap { triple ->
if (triple.first.isEmpty()) {
return@flatMap Flowable.just(triple.second)
}
val spotlightRooms = ArrayList<SpotlightRoom>()
spotlightRooms.addAll(triple.second.filter { it.name.contains(triple.first, true) })
spotlightRooms.addAll(triple.third)
val workedList = spotlightRooms.distinct().take(5)
if (workedList.size == 5) {
return@flatMap Flowable.just<List<SpotlightRoom>>(workedList)
}
tempSpotlightRoomCaller.search(triple.first)
spotlightRoomRepository.getSuggestionsFor(triple.first, SortDirection.DESC, 5)
.withLatestFrom<List<SpotlightRoom>, Pair<List<SpotlightRoom>, List<SpotlightRoom>>>(
Flowable.just(workedList),
BiFunction { a, b -> Pair.create(a, b) }
)
.map { pair ->
val spotlightRooms1 = ArrayList<SpotlightRoom>()
spotlightRooms1.addAll(pair.second)
spotlightRooms1.addAll(pair.first)
return@map spotlightRooms1.distinct().take(5)
}
}
}
private fun toSpotlightRooms(rooms: List<Room>): List<SpotlightRoom> {
val size = rooms.size
val spotlightRooms = ArrayList<SpotlightRoom>(size)
for (i in 0..size - 1) {
val room = rooms[i]
spotlightRooms.add(SpotlightRoom.builder()
.setId(room.id)
.setName(room.name)
.setType(room.type)
.build())
}
return spotlightRooms
}
}
package chat.rocket.core.interactors
import chat.rocket.core.SortDirection
import chat.rocket.core.models.Message
import chat.rocket.core.models.Room
import chat.rocket.core.models.SpotlightUser
import chat.rocket.core.models.User
import chat.rocket.core.repositories.MessageRepository
import chat.rocket.core.repositories.SpotlightUserRepository
import chat.rocket.core.repositories.UserRepository
import chat.rocket.core.temp.TempSpotlightUserCaller
import chat.rocket.core.utils.Triple
import io.reactivex.Flowable
import io.reactivex.functions.Function3
import java.util.ArrayList
class AutocompleteUserInteractor(private val room: Room,
private val userRepository: UserRepository,
private val messageRepository: MessageRepository,
private val spotlightUserRepository: SpotlightUserRepository,
private val tempSpotlightUserCaller: TempSpotlightUserCaller) {
private val groupMentions = listOf(
SpotlightUser.builder().setId("all").setUsername("all").setStatus("online").build(),
SpotlightUser.builder().setId("here").setUsername("here").setStatus("online").build()
)
fun getSuggestionsFor(name: String): Flowable<List<SpotlightUser>> {
return Flowable.zip<String, List<Message>, List<SpotlightUser>, Triple<String, List<Message>, List<SpotlightUser>>>(
Flowable.just(name),
messageRepository.getAllFrom(room),
userRepository.getSortedLikeName(name, SortDirection.DESC, 5).map { it.toSpotlightUsers() },
Function3 { a, b, c -> Triple.create(a, b, c) }
)
.flatMap { triple ->
val recentUsers = triple.second.takeUsers(5).toSpotlightUsers()
if (triple.first.isEmpty()) {
return@flatMap Flowable.just(recentUsers + groupMentions)
}
val workedUsers = (recentUsers.filter { it.username.contains(triple.first, true) } + triple.third).distinct().take(5)
if (workedUsers.size == 5) {
return@flatMap Flowable.just(workedUsers + groupMentions.filter { it.username.contains(triple.first, true) })
}
tempSpotlightUserCaller.search(triple.first)
spotlightUserRepository.getSuggestionsFor(triple.first, SortDirection.DESC, 5)
.withLatestFrom<List<SpotlightUser>, List<SpotlightUser>, Triple<List<SpotlightUser>, List<SpotlightUser>, List<SpotlightUser>>>(
Flowable.just(workedUsers),
Flowable.just(groupMentions.filter { it.username.contains(triple.first, true) }),
Function3 { a, b, c -> Triple.create(a, b, c) }
)
.map { triple ->
val spotlightUsers = triple.first + triple.second
return@map spotlightUsers.distinct().take(5) + triple.third
}
}
}
}
fun List<User>.toSpotlightUsers(): List<SpotlightUser> {
val size = this.size
val spotlightUsers = ArrayList<SpotlightUser>(size)
for (i in 0..size - 1) {
val user = this[i]
spotlightUsers.add(SpotlightUser.builder()
.setId(user.id)
.setUsername(user.username)
.setStatus(user.status)
.build())
}
return spotlightUsers
}
fun List<Message>.takeUsers(n: Int): List<User> {
val users = ArrayList<User>()
this.forEach {
if (it.user != null && !users.contains(it.user!!)) {
users.add(it.user!!)
if (users.size == n) {
return@forEach
}
}
}
return users
}
package chat.rocket.core.interactors;
import io.reactivex.Flowable;
import io.reactivex.Single;
import chat.rocket.core.repositories.UserRepository;
public class CanCreateRoomInteractor {
private final UserRepository userRepository;
private final SessionInteractor sessionInteractor;
public CanCreateRoomInteractor(UserRepository userRepository,
SessionInteractor sessionInteractor) {
this.userRepository = userRepository;
this.sessionInteractor = sessionInteractor;
}
public Single<Boolean> canCreate(String roomId) {
return Flowable.zip(
userRepository.getCurrent(),
sessionInteractor.getDefault(),
Flowable.just(roomId),
(user, session, room) -> user != null && session != null && room != null
)
.first(false);
}
}
package chat.rocket.core.interactors
import chat.rocket.core.models.Session
import chat.rocket.core.models.User
import io.reactivex.Flowable
import io.reactivex.Single
import chat.rocket.core.repositories.UserRepository
import com.fernandocejas.arrow.optional.Optional
import io.reactivex.functions.Function3
class CanCreateRoomInteractor(private val userRepository: UserRepository,
private val sessionInteractor: SessionInteractor) {
fun canCreate(roomId: String): Single<Boolean> {
return Flowable.zip<Optional<User>, Optional<Session>, String, Boolean>(
userRepository.current,
sessionInteractor.getDefault(),
Flowable.just(roomId),
Function3 { user, session, room -> user.isPresent && session.isPresent && room != null }
)
.first(false)
}
}
package chat.rocket.core.interactors;
import com.fernandocejas.arrow.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
import java.util.List;
import java.util.UUID;
import chat.rocket.core.SyncState;
import chat.rocket.core.models.Message;
import chat.rocket.core.models.Room;
import chat.rocket.core.models.RoomHistoryState;
import chat.rocket.core.models.User;
import chat.rocket.core.repositories.MessageRepository;
import chat.rocket.core.repositories.RoomRepository;
public class MessageInteractor {
private final MessageRepository messageRepository;
private final RoomRepository roomRepository;
public MessageInteractor(MessageRepository messageRepository, RoomRepository roomRepository) {
this.messageRepository = messageRepository;
this.roomRepository = roomRepository;
}
public Single<Boolean> loadMessages(Room room) {
final RoomHistoryState roomHistoryState = RoomHistoryState.builder()
.setRoomId(room.getRoomId())
.setSyncState(SyncState.NOT_SYNCED)
.setCount(100)
.setReset(true)
.setComplete(false)
.setTimestamp(0)
.build();
return roomRepository.setHistoryState(roomHistoryState);
}
public Single<Boolean> loadMoreMessages(Room room) {
return roomRepository.getHistoryStateByRoomId(room.getRoomId())
.filter(Optional::isPresent)
.map(Optional::get)
.filter(roomHistoryState -> {
int syncState = roomHistoryState.getSyncState();
return !roomHistoryState.isComplete()
&& (syncState == SyncState.SYNCED || syncState == SyncState.FAILED);
})
.map(Optional::of)
.first(Optional.absent())
.flatMap(historyStateOptional -> {
if (!historyStateOptional.isPresent()) {
return Single.just(false);
}
return roomRepository
.setHistoryState(historyStateOptional.get().withSyncState(SyncState.NOT_SYNCED));
});
}
public Single<Boolean> send(Room destination, User sender, String messageText) {
final Message message = Message.builder()
.setId(UUID.randomUUID().toString())
.setSyncState(SyncState.NOT_SYNCED)
.setTimestamp(System.currentTimeMillis())
.setRoomId(destination.getRoomId())
.setMessage(messageText)
.setGroupable(false)
.setUser(sender)
.build();
return messageRepository.save(message);
}
public Single<Boolean> resend(Message message, User sender) {
return messageRepository.save(
message.withSyncState(SyncState.NOT_SYNCED)
.withUser(sender));
}
public Single<Boolean> delete(Message message) {
return messageRepository.delete(message);
}
public Single<Integer> unreadCountFor(Room room, User user) {
return messageRepository.unreadCountFor(room, user);
}
public Flowable<List<Message>> getAllFrom(Room room) {
return messageRepository.getAllFrom(room);
}
}
package chat.rocket.core.interactors
import com.fernandocejas.arrow.optional.Optional
import io.reactivex.Flowable
import io.reactivex.Single
import java.util.UUID
import chat.rocket.core.SyncState
import chat.rocket.core.models.Message
import chat.rocket.core.models.Room
import chat.rocket.core.models.RoomHistoryState
import chat.rocket.core.models.User
import chat.rocket.core.repositories.MessageRepository
import chat.rocket.core.repositories.RoomRepository
class MessageInteractor(private val messageRepository: MessageRepository, private val roomRepository: RoomRepository) {
fun loadMessages(room: Room): Single<Boolean> {
val roomHistoryState = RoomHistoryState.builder()
.setRoomId(room.roomId)
.setSyncState(SyncState.NOT_SYNCED)
.setCount(100)
.setReset(true)
.setComplete(false)
.setTimestamp(0)
.build()
return roomRepository.setHistoryState(roomHistoryState)
}
fun loadMoreMessages(room: Room): Single<Boolean> {
return roomRepository.getHistoryStateByRoomId(room.roomId)
.filter { it.isPresent }
.map { it.get() }
.filter { roomHistoryState ->
val syncState = roomHistoryState.syncState
!roomHistoryState.isComplete && (syncState == SyncState.SYNCED || syncState == SyncState.FAILED)
}
.map { Optional.of(it) }
.first(Optional.absent())
.flatMap { historyStateOptional ->
if (!historyStateOptional.isPresent) {
return@flatMap Single.just(false)
}
roomRepository
.setHistoryState(historyStateOptional.get().withSyncState(SyncState.NOT_SYNCED))
}
}
fun send(destination: Room, sender: User, messageText: String): Single<Boolean> {
val message = Message.builder()
.setId(UUID.randomUUID().toString())
.setSyncState(SyncState.NOT_SYNCED)
.setTimestamp(System.currentTimeMillis())
.setRoomId(destination.roomId)
.setMessage(messageText)
.setGroupable(false)
.setUser(sender)
.build()
return messageRepository.save(message)
}
fun resend(message: Message, sender: User): Single<Boolean> {
return messageRepository.save(
message.withSyncState(SyncState.NOT_SYNCED).withUser(sender))
}
fun delete(message: Message): Single<Boolean> {
return messageRepository.delete(message)
}
fun unreadCountFor(room: Room, user: User): Single<Int> {
return messageRepository.unreadCountFor(room, user)
}
fun getAllFrom(room: Room): Flowable<List<Message>> {
return messageRepository.getAllFrom(room)
}
}
package chat.rocket.core.interactors;
import io.reactivex.Flowable;
import java.util.List;
import chat.rocket.core.models.Room;
import chat.rocket.core.repositories.RoomRepository;
public class RoomInteractor {
private final RoomRepository roomRepository;
public RoomInteractor(RoomRepository roomRepository) {
this.roomRepository = roomRepository;
}
public Flowable<Integer> getTotalUnreadMentionsCount() {
return roomRepository.getAll()
.flatMap(rooms -> Flowable.fromIterable(rooms)
.filter(room -> room.isOpen() && room.isAlert())
.map(Room::getUnread)
.defaultIfEmpty(0)
.reduce((unreadCount, unreadCount2) -> unreadCount + unreadCount2)
.toFlowable());
}
public Flowable<Long> getTotalUnreadRoomsCount() {
return roomRepository.getAll()
.flatMap(rooms -> Flowable.fromIterable(rooms)
.filter(room -> room.isOpen() && room.isAlert())
.count()
.toFlowable());
}
public Flowable<List<Room>> getOpenRooms() {
return roomRepository.getAll()
.flatMap(rooms -> Flowable.fromIterable(rooms)
.filter(Room::isOpen)
.toList()
.toFlowable());
}
}
package chat.rocket.core.interactors
import io.reactivex.Flowable
import chat.rocket.core.SortDirection
import chat.rocket.core.models.Room
import chat.rocket.core.repositories.RoomRepository
class RoomInteractor(private val roomRepository: RoomRepository) {
fun getTotalUnreadMentionsCount(): Flowable<Int> {
return roomRepository.all
.flatMap { rooms ->
Flowable.fromIterable(rooms)
.filter { room -> room.isOpen && room.isAlert }
.map { it.unread }
.defaultIfEmpty(0)
.reduce { unreadCount, unreadCount2 -> unreadCount + unreadCount2 }
.toFlowable()
}
}
fun getTotalUnreadRoomsCount(): Flowable<Long> {
return roomRepository.all
.flatMap { rooms ->
Flowable.fromIterable(rooms)
.filter { room -> room.isOpen && room.isAlert }
.count()
.toFlowable()
}
}
fun getOpenRooms(): Flowable<List<Room>> {
return roomRepository.all
.flatMap { rooms ->
Flowable.fromIterable(rooms)
.filter { it.isOpen }
.toList()
.toFlowable()
}
}
fun getRoomsWithNameLike(name: String): Flowable<List<Room>> {
return roomRepository.getSortedLikeName(name, SortDirection.DESC, 5)
}
}
package chat.rocket.core.interactors;
import com.fernandocejas.arrow.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
import chat.rocket.core.models.Session;
import chat.rocket.core.repositories.SessionRepository;
public class SessionInteractor {
private static final int DEFAULT_ID = 0;
private final SessionRepository sessionRepository;
public SessionInteractor(SessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
public Flowable<Optional<Session>> getDefault() {
return sessionRepository.getById(DEFAULT_ID);
}
public Flowable<Session.State> getSessionState() {
return getDefault()
.map(sessionOptional -> getStateFrom(sessionOptional.orNull()));
}
public Single<Boolean> retryLogin() {
return getDefault()
.filter(Optional::isPresent)
.map(Optional::get)
.filter(session -> session.getToken() != null
&& (!session.isTokenVerified() || session.getError() != null))
.map(session -> Optional.of(session.withTokenVerified(false).withError(null)))
.first(Optional.absent())
.flatMap(sessionOptional -> {
if (!sessionOptional.isPresent()) {
return Single.just(false);
}
return sessionRepository.save(sessionOptional.get());
});
}
private Session.State getStateFrom(Session session) {
if (session == null) {
return Session.State.UNAVAILABLE;
}
final String token = session.getToken();
if (token == null || token.length() == 0) {
return Session.State.UNAVAILABLE;
}
final String error = session.getError();
if (error == null || error.length() == 0) {
return Session.State.VALID;
}
return Session.State.INVALID;
}
}
package chat.rocket.core.interactors
import com.fernandocejas.arrow.optional.Optional
import io.reactivex.Flowable
import io.reactivex.Single
import chat.rocket.core.models.Session
import chat.rocket.core.repositories.SessionRepository
class SessionInteractor(private val sessionRepository: SessionRepository) {
companion object {
private val DEFAULT_ID = 0
}
fun getDefault(): Flowable<Optional<Session>> {
return sessionRepository.getById(DEFAULT_ID)
}
fun getSessionState(): Flowable<Session.State> {
return getDefault()
.map { sessionOptional -> getStateFrom(sessionOptional.orNull()) }
}
fun retryLogin(): Single<Boolean> {
return getDefault()
.filter { it.isPresent }
.map { it.get() }
.filter { session -> session.token != null && (!session.isTokenVerified || session.error != null) }
.map { session -> Optional.of(session.withTokenVerified(false).withError(null)) }
.first(Optional.absent())
.flatMap { sessionOptional ->
if (!sessionOptional.isPresent) {
return@flatMap Single.just(false)
}
sessionRepository.save(sessionOptional.get())
}
}
private fun getStateFrom(session: Session?): Session.State {
if (session == null) {
return Session.State.UNAVAILABLE
}
val token = session.token
if (token == null || token.isEmpty()) {
return Session.State.UNAVAILABLE
}
val error = session.error
if (error == null || error.isEmpty()) {
return Session.State.VALID
}
return Session.State.INVALID
}
}
package chat.rocket.core.interactors;
import io.reactivex.Flowable;
import java.util.List;
import chat.rocket.core.SortDirection;
import chat.rocket.core.models.User;
import chat.rocket.core.repositories.UserRepository;
public class UserInteractor {
private final UserRepository userRepository;
public UserInteractor(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Flowable<List<User>> getUserAutocompleteSuggestions(String name) {
return userRepository.getSortedLikeName(name, SortDirection.DESC, 5);
}
}
package chat.rocket.core.models;
import com.google.auto.value.AutoValue;
@AutoValue
public abstract class SpotlightRoom {
public abstract String getId();
public abstract String getName();
public abstract String getType();
public boolean isChannel() {
return Room.TYPE_CHANNEL.equals(getType());
}
public boolean isPrivate() {
return Room.TYPE_PRIVATE.equals(getType());
}
public boolean isDirectMessage() {
return Room.TYPE_DIRECT_MESSAGE.equals(getType());
}
public static Builder builder() {
return new AutoValue_SpotlightRoom.Builder();
}
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setId(String id);
public abstract Builder setName(String name);
public abstract Builder setType(String type);
public abstract SpotlightRoom build();
}
}
package chat.rocket.core.models;
import com.google.auto.value.AutoValue;
import javax.annotation.Nullable;
@AutoValue
public abstract class SpotlightUser {
public abstract String getId();
public abstract String getUsername();
@Nullable
public abstract String getName();
@Nullable
public abstract String getStatus();
public static Builder builder() {
return new AutoValue_SpotlightUser.Builder();
}
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setId(String id);
public abstract Builder setUsername(String username);
public abstract Builder setName(String name);
public abstract Builder setStatus(String status);
public abstract SpotlightUser build();
}
}
......@@ -5,6 +5,7 @@ import io.reactivex.Flowable;
import io.reactivex.Single;
import java.util.List;
import chat.rocket.core.SortDirection;
import chat.rocket.core.models.Room;
import chat.rocket.core.models.RoomHistoryState;
......@@ -17,4 +18,8 @@ public interface RoomRepository {
Flowable<Optional<RoomHistoryState>> getHistoryStateByRoomId(String roomId);
Single<Boolean> setHistoryState(RoomHistoryState roomHistoryState);
Flowable<List<Room>> getSortedLikeName(String name, SortDirection direction, int limit);
Flowable<List<Room>> getLatestSeen(int limit);
}
package chat.rocket.core.repositories;
import io.reactivex.Flowable;
import java.util.List;
import chat.rocket.core.SortDirection;
import chat.rocket.core.models.SpotlightRoom;
public interface SpotlightRoomRepository {
Flowable<List<SpotlightRoom>> getSuggestionsFor(String name, SortDirection direction, int limit);
}
package chat.rocket.core.repositories;
import io.reactivex.Flowable;
import java.util.List;
import chat.rocket.core.SortDirection;
import chat.rocket.core.models.SpotlightUser;
public interface SpotlightUserRepository {
Flowable<List<SpotlightUser>> getSuggestionsFor(String name, SortDirection direction, int limit);
}
......@@ -3,9 +3,13 @@ package chat.rocket.core.repositories;
import com.fernandocejas.arrow.optional.Optional;
import io.reactivex.Flowable;
import java.util.List;
import chat.rocket.core.SortDirection;
import chat.rocket.core.models.User;
public interface UserRepository {
Flowable<Optional<User>> getCurrent();
Flowable<List<User>> getSortedLikeName(String name, SortDirection direction, int limit);
}
package chat.rocket.core.temp;
public interface TempSpotlightRoomCaller {
void search(String term);
}
package chat.rocket.core.temp;
public interface TempSpotlightUserCaller {
void search(String term);
}
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/util/Pair.java
package chat.rocket.core.utils;
/**
* Container to ease passing around a tuple of two objects. This object provides a sensible
* implementation of equals(), returning true if equals() is true on each of the contained
* objects.
*/
public class Pair<F, S> {
public final F first;
public final S second;
/**
* Constructor for a Pair.
* @param first the first object in the Pair
* @param second the second object in the pair
*/
public Pair(F first, S second) {
this.first = first;
this.second = second;
}
/**
* Checks the two objects for equality by delegating to their respective
* {@link Object#equals(Object)} methods.
* @param o the {@link Pair} to which this one is to be checked for equality
* @return true if the underlying objects of the Pair are both considered equal
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof Pair)) {
return false;
}
Pair<?, ?> p = (Pair<?, ?>) o;
return equals(p.first, first) && equals(p.second, second);
}
/**
* Compute a hash code using the hash codes of the underlying objects
* @return a hashcode of the Pair
*/
@Override
public int hashCode() {
return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode());
}
@Override
public String toString() {
return "Pair{" + String.valueOf(first) + " " + String.valueOf(second) + "}";
}
private boolean equals(Object var0, Object var1) {
return var0 == var1 || var0 != null && var0.equals(var1);
}
/**
* Convenience method for creating an appropriately typed pair.
* @param a the first object in the Pair
* @param b the second object in the pair
* @return a Pair that is templatized with the types of a and b
*/
public static <A, B> Pair<A, B> create(A a, B b) {
return new Pair<A, B>(a, b);
}
}
\ No newline at end of file
package chat.rocket.core.utils;
public class Triple<F, S, T> {
public final F first;
public final S second;
public final T third;
public Triple(F first, S second, T third) {
this.first = first;
this.second = second;
this.third = third;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Triple)) {
return false;
}
Triple<?, ?, ?> t = (Triple<?, ?, ?>) o;
return equals(t.first, first) && equals(t.second, second)
&& equals(t.third, third);
}
@Override
public int hashCode() {
return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode())
^ (third == null ? 0 : third.hashCode());
}
private boolean equals(Object var0, Object var1) {
return var0 == var1 || var0 != null && var0.equals(var1);
}
public static <A, B, C> Triple<A, B, C> create(A a, B b, C c) {
return new Triple<A, B, C>(a, b, c);
}
}
package chat.rocket.core.interactors;
import static org.mockito.Mockito.*;
import io.reactivex.Flowable;
import io.reactivex.subscribers.TestSubscriber;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import chat.rocket.core.SortDirection;
import chat.rocket.core.models.Room;
import chat.rocket.core.models.SpotlightRoom;
import chat.rocket.core.repositories.RoomRepository;
import chat.rocket.core.repositories.SpotlightRoomRepository;
import chat.rocket.core.temp.TempSpotlightRoomCaller;
@RunWith(MockitoJUnitRunner.class)
public class AutocompleteChannelInteractorTest {
@Mock
RoomRepository roomRepository;
@Mock
SpotlightRoomRepository spotlightRoomRepository;
@Mock
TempSpotlightRoomCaller tempSpotlightRoomCaller;
private AutocompleteChannelInteractor autocompleteChannelInteractor;
@Before
public void setUp() {
autocompleteChannelInteractor = new AutocompleteChannelInteractor(
roomRepository, spotlightRoomRepository, tempSpotlightRoomCaller
);
}
@Test
public void getSuggestionsForEmptyStringReturnLatestSeenOnly() throws Exception {
List<Room> rooms = new ArrayList<>();
rooms.add(getRoom("id1", "Name1", "c"));
when(roomRepository.getLatestSeen(anyInt())).thenReturn(Flowable.just(rooms));
rooms = new ArrayList<>();
rooms.add(getRoom("id2", "Name2", "c"));
when(roomRepository.getSortedLikeName(anyString(), any(SortDirection.class), anyInt()))
.thenReturn(Flowable.just(rooms));
TestSubscriber<List<SpotlightRoom>> testSubscriber = new TestSubscriber<>();
autocompleteChannelInteractor.getSuggestionsFor("").subscribe(testSubscriber);
List<SpotlightRoom> spotlightRooms = new ArrayList<>();
spotlightRooms.add(getSpotlightRoom("id1", "Name1", "c"));
testSubscriber.assertResult(spotlightRooms);
}
@Test
public void getSuggestionsForNonEmptyStringReturnLatestSeenAndFromRooms() throws Exception {
List<Room> rooms = new ArrayList<>();
rooms.add(getRoom("id1", "Name1", "c"));
rooms.add(getRoom("id1.1", "Ame1.1", "c"));
when(roomRepository.getLatestSeen(anyInt())).thenReturn(Flowable.just(rooms));
rooms = new ArrayList<>();
rooms.add(getRoom("id1", "Name1", "c"));
rooms.add(getRoom("id2", "Name2", "c"));
rooms.add(getRoom("id3", "Name3", "c"));
rooms.add(getRoom("id4", "Name4", "c"));
rooms.add(getRoom("id5", "Name5", "c"));
when(roomRepository.getSortedLikeName(anyString(), any(SortDirection.class), anyInt()))
.thenReturn(Flowable.just(rooms));
TestSubscriber<List<SpotlightRoom>> testSubscriber = new TestSubscriber<>();
autocompleteChannelInteractor.getSuggestionsFor("N").subscribe(testSubscriber);
List<SpotlightRoom> spotlightRooms = new ArrayList<>();
spotlightRooms.add(getSpotlightRoom("id1", "Name1", "c"));
spotlightRooms.add(getSpotlightRoom("id2", "Name2", "c"));
spotlightRooms.add(getSpotlightRoom("id3", "Name3", "c"));
spotlightRooms.add(getSpotlightRoom("id4", "Name4", "c"));
spotlightRooms.add(getSpotlightRoom("id5", "Name5", "c"));
testSubscriber.assertResult(spotlightRooms);
}
@Test
public void getSuggestionsForMayGetFromNetwork() throws Exception {
List<Room> rooms = new ArrayList<>();
rooms.add(getRoom("id1", "Name1", "c"));
rooms.add(getRoom("id1.1", "Ame1.1", "c"));
when(roomRepository.getLatestSeen(anyInt())).thenReturn(Flowable.just(rooms));
rooms = new ArrayList<>();
rooms.add(getRoom("id1", "Name1", "c"));
rooms.add(getRoom("id2", "Name2", "c"));
when(roomRepository.getSortedLikeName(anyString(), any(SortDirection.class), anyInt()))
.thenReturn(Flowable.just(rooms));
List<SpotlightRoom> spotlightRooms = new ArrayList<>();
spotlightRooms.add(getSpotlightRoom("id3", "Name3", "c"));
when(spotlightRoomRepository.getSuggestionsFor(anyString(), any(SortDirection.class), anyInt()))
.thenReturn(Flowable.just(spotlightRooms));
TestSubscriber<List<SpotlightRoom>> testSubscriber = new TestSubscriber<>();
autocompleteChannelInteractor.getSuggestionsFor("N").subscribe(testSubscriber);
verify(tempSpotlightRoomCaller, times(1)).search(anyString());
spotlightRooms = new ArrayList<>();
spotlightRooms.add(getSpotlightRoom("id1", "Name1", "c"));
spotlightRooms.add(getSpotlightRoom("id2", "Name2", "c"));
spotlightRooms.add(getSpotlightRoom("id3", "Name3", "c"));
testSubscriber.assertResult(spotlightRooms);
}
private Room getRoom(String id, String name, String type) {
Room room = mock(Room.class);
when(room.getId()).thenReturn(id);
when(room.getName()).thenReturn(name);
when(room.getType()).thenReturn(type);
return room;
}
private SpotlightRoom getSpotlightRoom(String id, String name, String type) {
return SpotlightRoom.builder()
.setId(id).setName(name).setType(type).build();
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment