Commit 2ae875c8 authored by Tiago Cunha's avatar Tiago Cunha

WIP: autocomplete for channel done

parent 9ad4a67c
......@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'me.tatarka:gradle-retrolambda:3.5.0'
classpath 'me.tatarka.retrolambda.projectlombok:lombok.ast:0.2.3.a2'
}
......
......@@ -10,7 +10,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.android.tools.build:gradle:2.3.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
......
......@@ -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;
......@@ -366,6 +368,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;
......
......@@ -56,9 +56,11 @@ import chat.rocket.android.layouthelper.extra_action.upload.ImageUploadActionIte
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.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.MessageInteractor;
import chat.rocket.core.interactors.RoomInteractor;
import chat.rocket.core.interactors.SessionInteractor;
......@@ -70,6 +72,7 @@ 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.RealmUserRepository;
import chat.rocket.android.layouthelper.chatroom.ModelListAdapter;
import chat.rocket.persistence.realm.RealmStore;
......@@ -321,7 +324,11 @@ public class RoomFragment extends AbstractChatRoomFragment
autocompleteManager.registerSource(
new ChannelSource(
new RoomInteractor(new RealmRoomRepository(hostname)),
new AutocompleteChannelInteractor(
new RealmRoomRepository(hostname),
new RealmSpotlightRoomRepository(hostname),
new DeafultTempSpotlightRoomCaller(new MethodCallHelper(getContext(), hostname))
),
AndroidSchedulers.from(BackgroundLooper.get()),
AndroidSchedulers.mainThread()
)
......
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);
}
}
......@@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.android.tools.build:gradle:2.3.1'
}
}
......
......@@ -8,7 +8,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'io.realm:realm-gradle-plugin:2.3.2'
classpath 'me.tatarka:gradle-retrolambda:3.5.0'
classpath 'me.tatarka.retrolambda.projectlombok:lombok.ast:0.2.3.a2'
......
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) {
......@@ -18,5 +22,17 @@ public class Migration implements RealmMigration {
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);
schema.create("RealmSpotlightRoom")
.addField(RealmSpotlightRoom.Columns.ID, String.class, FieldAttribute.PRIMARY_KEY)
.addField(RealmSpotlightRoom.Columns.NAME, String.class)
.addField(RealmSpotlightRoom.Columns.TYPE, String.class);
}
}
}
......@@ -15,7 +15,7 @@ public class RocketChatPersistenceRealm {
new RealmConfiguration.Builder()
.name("rocket.chat.persistence.realm")
.modules(new RocketChatLibraryModule())
.schemaVersion(1)
.schemaVersion(2)
.migration(new Migration())
.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 STATUS = "status";
}
@PrimaryKey private String _id;
private String username;
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 getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public SpotlightUser asSpotlightUser() {
return SpotlightUser.builder()
.setId(_id)
.setUsername(username)
.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));
}
}
......@@ -150,6 +150,11 @@ public class RealmRoomRepository extends RealmRepository implements RoomReposito
pair -> RxJavaInterop.toV2Flowable(
pair.first.where(RealmRoom.class)
.like(RealmRoom.NAME, "*" + name + "*")
.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()),
......@@ -161,6 +166,27 @@ public class RealmRoomRepository extends RealmRepository implements RoomReposito
.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();
......@@ -184,10 +210,4 @@ public class RealmRoomRepository extends RealmRepository implements RoomReposito
return roomList;
}
private List<RealmRoom> safeSubList(RealmResults<RealmRoom> realmRooms,
int fromIndex,
int toIndex) {
return realmRooms.subList(Math.max(0, fromIndex), Math.min(realmRooms.size(), toIndex));
}
}
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.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 + "*")
.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;
}
}
......@@ -89,10 +89,4 @@ public class RealmUserRepository extends RealmRepository implements UserReposito
return userList;
}
private List<RealmUser> safeSubList(RealmResults<RealmUser> realmUsers,
int fromIndex,
int toIndex) {
return realmUsers.subList(Math.max(0, fromIndex), Math.min(realmUsers.size(), toIndex));
}
}
......@@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.android.tools.build:gradle:2.3.1'
}
}
......
......@@ -146,11 +146,6 @@ public class AutocompleteManager {
}
final String toCompleteText = getToCompleteText(text, selectionStart);
// trigger plus 2 letters
if (toCompleteText.length() < 3) {
cleanState();
return;
}
final AutocompleteSource source = getSource(toCompleteText);
if (source == null) {
......
......@@ -5,24 +5,24 @@ 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.Room;
import chat.rocket.core.models.SpotlightRoom;
public class ChannelItem implements AutocompleteItem {
private final Room room;
private final SpotlightRoom spotlightRoom;
public ChannelItem(@NonNull Room room) {
this.room = room;
public ChannelItem(@NonNull SpotlightRoom spotlightRoom) {
this.spotlightRoom = spotlightRoom;
}
@NonNull
@Override
public String getSuggestion() {
return room.getName();
return spotlightRoom.getName();
}
@StringRes
public int getIcon() {
return IconProvider.getIcon(room.getType());
return IconProvider.getIcon(spotlightRoom.getType());
}
}
......@@ -13,19 +13,19 @@ 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.RoomInteractor;
import chat.rocket.core.models.Room;
import chat.rocket.core.interactors.AutocompleteChannelInteractor;
import chat.rocket.core.models.SpotlightRoom;
public class ChannelSource extends AutocompleteSource<ChannelAdapter, ChannelItem> {
private final RoomInteractor roomInteractor;
private final AutocompleteChannelInteractor autocompleteChannelInteractor;
private final Scheduler bgScheduler;
private final Scheduler fgScheduler;
public ChannelSource(RoomInteractor roomInteractor,
public ChannelSource(AutocompleteChannelInteractor autocompleteChannelInteractor,
Scheduler bgScheduler,
Scheduler fgScheduler) {
this.roomInteractor = roomInteractor;
this.autocompleteChannelInteractor = autocompleteChannelInteractor;
this.bgScheduler = bgScheduler;
this.fgScheduler = fgScheduler;
}
......@@ -46,19 +46,20 @@ public class ChannelSource extends AutocompleteSource<ChannelAdapter, ChannelIte
return s.substring(1);
}
})
.flatMap(new Function<String, Publisher<List<Room>>>() {
.flatMap(new Function<String, Publisher<List<SpotlightRoom>>>() {
@Override
public Publisher<List<Room>> apply(@io.reactivex.annotations.NonNull String s)
public Publisher<List<SpotlightRoom>> apply(@io.reactivex.annotations.NonNull String s)
throws Exception {
return roomInteractor.getRoomsWithNameLike(s);
return autocompleteChannelInteractor.getSuggestionsFor(s);
}
})
.distinctUntilChanged()
.map(new Function<List<Room>, List<ChannelItem>>() {
.map(new Function<List<SpotlightRoom>, List<ChannelItem>>() {
@Override
public List<ChannelItem> apply(@io.reactivex.annotations.NonNull List<Room> rooms)
public List<ChannelItem> apply(
@io.reactivex.annotations.NonNull List<SpotlightRoom> spotlightRooms)
throws Exception {
return toChannelItemList(rooms);
return toChannelItemList(spotlightRooms);
}
})
.subscribeOn(bgScheduler)
......@@ -97,12 +98,12 @@ public class ChannelSource extends AutocompleteSource<ChannelAdapter, ChannelIte
return getTrigger() + autocompleteItem.getSuggestion();
}
private List<ChannelItem> toChannelItemList(List<Room> rooms) {
int size = rooms.size();
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(rooms.get(i)));
channelItems.add(new ChannelItem(spotlightRooms.get(i)));
}
return channelItems;
......
......@@ -4,7 +4,6 @@ import android.view.View;
import android.widget.TextView;
import chat.rocket.android.widget.R;
import chat.rocket.android.widget.message.autocomplete.AutocompleteItem;
import chat.rocket.android.widget.message.autocomplete.AutocompleteViewHolder;
public class ChannelViewHolder extends AutocompleteViewHolder<ChannelItem> {
......
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.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.models.SpotlightUser
import chat.rocket.core.models.User
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 java.util.ArrayList
class AutocompleteUserInteractor(private val userRepository: UserRepository,
private val spotlightUserRepository: SpotlightUserRepository,
private val tempSpotlightUserCaller: TempSpotlightUserCaller) {
fun getSuggestionsFor(name: String): Flowable<List<SpotlightUser>> {
return Flowable.zip<String, List<SpotlightUser>, List<SpotlightUser>, Triple<String, List<SpotlightUser>, List<SpotlightUser>>>(
Flowable.just(name),
userRepository.getSortedLikeName(name)
)
}
private fun toSpotlightRooms(users: List<User>): List<SpotlightUser> {
val size = users.size
val spotlightUsers = ArrayList<SpotlightUser>(size)
for (i in 0..size - 1) {
val user = users[i]
spotlightUsers.add(SpotlightUser.builder()
.setId(user.id)
.setUsername(user.username)
.setStatus(user.status)
.build())
}
return spotlightUsers
}
}
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.SortDirection;
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());
}
public Flowable<List<Room>> getRoomsWithNameLike(String name) {
return roomRepository.getSortedLikeName(name, SortDirection.DESC, 5);
}
}
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.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 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 setStatus(String status);
public abstract SpotlightUser build();
}
}
......@@ -20,4 +20,6 @@ public interface RoomRepository {
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);
}
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