Commit d26e6210 authored by Tiago Cunha's avatar Tiago Cunha

Merge remote-tracking branch 'refs/remotes/origin/develop' into develop

parents 85d79171 b34ffdff
apply plugin: 'com.android.library'
apply plugin: 'me.tatarka.retrolambda'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath rootProject.ext.androidPlugin
classpath rootProject.ext.retroLambdaPlugin
classpath rootProject.ext.retroLambdaPatch
}
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.compileSdkVersion
versionCode 1
versionName "0.0.8"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile rootProject.ext.supportAnnotations
compile 'com.squareup.okhttp3:okhttp-ws:3.4.1'
compile rootProject.ext.rxJava
compile rootProject.ext.boltsTask
compile rootProject.ext.timber
}
# -*- coding:utf-8 -*-
a='''
@Override
public void onOpen(WebSocket webSocket, Response response) {
}
@Override
public void onFailure(IOException e, Response response) {
}
@Override
public void onMessage(ResponseBody responseBody) throws IOException {
}
@Override
public void onPong(Buffer payload) {
}
@Override
public void onClose(int code, String reason) {
}
'''.strip().split('@Override')
for m in a[1:]:
m= " @Override\n "+m.strip()
mn = m.split("\n")[1].strip().split(" ")[2].split("(")[0]
if mn.startswith("on"):
d=dict()
d["classname"]=mn[2:]
params = [p for p in " ".join(m.split("\n")[1].strip()[:-1].split(" throws ")[0].split(" ")[2:]).strip()[len(mn)+1:-1].split(", ") if p.split(" ")[0]!="WebSocket"]
d["params"]="".join([", "+p for p in params])
paramnames = [p.split(" ")[-1] for p in params]
d["paramdefs"]="\n".join([" public "+p+";" for p in params])
d["thisis"]="\n".join([" this.{param} = {param};".format(param=p) for p in paramnames])
# print '''
# public static class {classname} extends Base {{
# {paramdefs}
# public {classname}(WebSocket websocket{params}) {{
# super("{classname}", websocket);
# {thisis}
# }}
# }}'''.format(**d)
######################
x=m.split("\n")
x[2]=''' mSubscriber.onNext(new RxWebSocketCallback.{classname}(mWebSocket, {params}));'''.format(classname=mn[2:],params=", ".join(paramnames))
print "\n".join(x)
<lint>
<issue id="InvalidPackage">
<ignore regexp="okio.*jar"/>
</issue>
</lint>
\ No newline at end of file
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/yi01/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.rocket.android_ddp">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true">
</application>
</manifest>
package chat.rocket.android_ddp;
import android.support.annotation.Nullable;
import bolts.Task;
import bolts.TaskCompletionSource;
import chat.rocket.android_ddp.rx.RxWebSocketCallback;
import okhttp3.OkHttpClient;
import org.json.JSONArray;
import rx.Observable;
import timber.log.Timber;
public class DDPClient {
// reference: https://github.com/eddflrs/meteor-ddp/blob/master/meteor-ddp.js
private final DDPClientImpl impl;
public DDPClient(OkHttpClient client) {
impl = new DDPClientImpl(this, client);
Timber.plant(new Timber.DebugTree());
}
public Task<DDPClientCallback.Connect> connect(String url) {
return connect(url, null);
}
public Task<DDPClientCallback.Connect> connect(String url, String session) {
TaskCompletionSource<DDPClientCallback.Connect> task = new TaskCompletionSource<>();
impl.connect(task, url, session);
return task.getTask();
}
public Task<DDPClientCallback.Ping> ping(@Nullable String id) {
TaskCompletionSource<DDPClientCallback.Ping> task = new TaskCompletionSource<>();
impl.ping(task, id);
return task.getTask();
}
public Task<DDPClientCallback.RPC> rpc(String method, JSONArray params, String id,
long timeoutMs) {
TaskCompletionSource<DDPClientCallback.RPC> task = new TaskCompletionSource<>();
impl.rpc(task, method, params, id, timeoutMs);
return task.getTask();
}
public Task<DDPSubscription.Ready> sub(String id, String name, JSONArray params) {
TaskCompletionSource<DDPSubscription.Ready> task = new TaskCompletionSource<>();
impl.sub(task, name, params, id);
return task.getTask();
}
public Task<DDPSubscription.NoSub> unsub(String id) {
TaskCompletionSource<DDPSubscription.NoSub> task = new TaskCompletionSource<>();
impl.unsub(task, id);
return task.getTask();
}
public Observable<DDPSubscription.Event> getSubscriptionCallback() {
return impl.getDDPSubscription();
}
public Task<RxWebSocketCallback.Close> getOnCloseCallback() {
return impl.getOnCloseCallback();
}
public boolean isConnected() {
return impl.isConnected();
}
public void close() {
impl.close(1000, "closed by DDPClient#close()");
}
}
package chat.rocket.android_ddp;
import android.support.annotation.Nullable;
import org.json.JSONObject;
public class DDPClientCallback {
public static abstract class Base {
public DDPClient client;
public Base(DDPClient client) {
this.client = client;
}
}
public static abstract class BaseException extends Exception {
public DDPClient client;
public BaseException(DDPClient client) {
this.client = client;
}
}
public static class Connect extends Base {
public String session;
public Connect(DDPClient client, String session) {
super(client);
this.session = session;
}
public static class Failed extends BaseException {
public String version;
public Failed(DDPClient client, String version) {
super(client);
this.version = version;
}
}
}
public static class Ping extends Base {
@Nullable public String id;
public Ping(DDPClient client, @Nullable String id) {
super(client);
this.id = id;
}
public static class Timeout extends BaseException {
public Timeout(DDPClient client) {
super(client);
}
}
}
public static class RPC extends Base {
public String id;
public String result;
public RPC(DDPClient client, String id, String result) {
super(client);
this.id = id;
this.result = result;
}
public static class Error extends BaseException {
public String id;
public JSONObject error;
public Error(DDPClient client, String id, JSONObject error) {
super(client);
this.id = id;
this.error = error;
}
}
public static class Timeout extends BaseException {
public Timeout(DDPClient client) {
super(client);
}
}
}
}
package chat.rocket.android_ddp;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import bolts.Task;
import bolts.TaskCompletionSource;
import chat.rocket.android_ddp.rx.RxWebSocket;
import chat.rocket.android_ddp.rx.RxWebSocketCallback;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import okhttp3.OkHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import rx.Observable;
import rx.functions.Func1;
import rx.subscriptions.CompositeSubscription;
import timber.log.Timber;
public class DDPClientImpl {
private final DDPClient client;
private final RxWebSocket websocket;
private Observable<RxWebSocketCallback.Base> observable;
private CompositeSubscription subscriptions;
public DDPClientImpl(DDPClient self, OkHttpClient client) {
websocket = new RxWebSocket(client);
this.client = self;
}
private static JSONObject toJson(String s) {
if (TextUtils.isEmpty(s)) return null;
try {
return new JSONObject(s);
} catch (JSONException e) {
return null;
}
}
private static String extractMsg(JSONObject response) {
if (response == null || response.isNull("msg")) {
return null;
} else {
return response.optString("msg");
}
}
public void connect(final TaskCompletionSource<DDPClientCallback.Connect> task, final String url,
String session) {
try {
observable = websocket.connect(url).autoConnect();
CompositeSubscription subscriptions = new CompositeSubscription();
subscriptions.add(observable.filter(callback -> callback instanceof RxWebSocketCallback.Open)
.subscribe(callback -> {
sendMessage("connect",
json -> (TextUtils.isEmpty(session) ? json : json.put("session", session)).put(
"version", "pre2").put("support", new JSONArray().put("pre2").put("pre1")));
}, err -> {
}));
subscriptions.add(
observable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
.map(DDPClientImpl::toJson)
.subscribe(response -> {
String msg = extractMsg(response);
if ("connected".equals(msg) && !response.isNull("session")) {
task.setResult(
new DDPClientCallback.Connect(client, response.optString("session")));
subscriptions.unsubscribe();
} else if ("error".equals(msg) && "Already connected".equals(
response.optString("reason"))) {
task.setResult(new DDPClientCallback.Connect(client, null));
subscriptions.unsubscribe();
} else if ("failed".equals(msg)) {
task.setError(
new DDPClientCallback.Connect.Failed(client, response.optString("version")));
subscriptions.unsubscribe();
}
}, err -> {
}));
addErrorCallback(subscriptions, task);
subscribeBaseListeners();
} catch (Exception e) {
Timber.e(e);
}
}
public boolean isConnected() {
return websocket != null && websocket.isConnected();
}
public void ping(final TaskCompletionSource<DDPClientCallback.Ping> task,
@Nullable final String id) {
CompositeSubscription subscriptions = new CompositeSubscription();
subscriptions.add(
observable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
.map(DDPClientImpl::toJson)
.timeout(4, TimeUnit.SECONDS)
.subscribe(response -> {
String msg = extractMsg(response);
if ("pong".equals(msg)) {
if (response.isNull("id")) {
task.setResult(new DDPClientCallback.Ping(client, null));
subscriptions.unsubscribe();
} else {
String _id = response.optString("id");
if (id.equals(_id)) {
task.setResult(new DDPClientCallback.Ping(client, id));
subscriptions.unsubscribe();
}
}
}
}, err -> {
task.setError(new DDPClientCallback.Ping.Timeout(client));
}));
addErrorCallback(subscriptions, task);
if (TextUtils.isEmpty(id)) {
sendMessage("ping", null);
} else {
sendMessage("ping", json -> json.put("id", id));
}
}
public void sub(final TaskCompletionSource<DDPSubscription.Ready> task, String name,
JSONArray params, String id) {
CompositeSubscription subscriptions = new CompositeSubscription();
subscriptions.add(
observable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
.map(DDPClientImpl::toJson)
.subscribe(response -> {
String msg = extractMsg(response);
if ("ready".equals(msg) && !response.isNull("subs")) {
JSONArray ids = response.optJSONArray("subs");
for (int i = 0; i < ids.length(); i++) {
String _id = ids.optString(i);
if (id.equals(_id)) {
task.setResult(new DDPSubscription.Ready(client, id));
subscriptions.unsubscribe();
break;
}
}
} else if ("nosub".equals(msg) && !response.isNull("id") && !response.isNull(
"error")) {
String _id = response.optString("id");
if (id.equals(_id)) {
task.setError(new DDPSubscription.NoSub.Error(client, id,
response.optJSONObject("error")));
subscriptions.unsubscribe();
}
}
}, err -> {
}));
addErrorCallback(subscriptions, task);
sendMessage("sub", json -> json.put("id", id).put("name", name).put("params", params));
}
public void unsub(final TaskCompletionSource<DDPSubscription.NoSub> task,
@Nullable final String id) {
CompositeSubscription subscriptions = new CompositeSubscription();
subscriptions.add(
observable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
.map(DDPClientImpl::toJson)
.subscribe(response -> {
String msg = extractMsg(response);
if ("nosub".equals(msg) && response.isNull("error") && !response.isNull("id")) {
String _id = response.optString("id");
if (id.equals(_id)) {
task.setResult(new DDPSubscription.NoSub(client, id));
subscriptions.unsubscribe();
}
}
}, err -> {
}));
addErrorCallback(subscriptions, task);
sendMessage("unsub", json -> json.put("id", id));
}
public void rpc(final TaskCompletionSource<DDPClientCallback.RPC> task, String method,
JSONArray params, String id, long timeoutMs) {
CompositeSubscription subscriptions = new CompositeSubscription();
subscriptions.add(
observable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
.map(DDPClientImpl::toJson)
.timeout(timeoutMs, TimeUnit.MILLISECONDS)
.subscribe(response -> {
String msg = extractMsg(response);
if ("result".equals(msg)) {
String _id = response.optString("id");
if (id.equals(_id)) {
if (!response.isNull("error")) {
task.setError(new DDPClientCallback.RPC.Error(client, id,
response.optJSONObject("error")));
} else {
String result = response.optString("result");
task.setResult(new DDPClientCallback.RPC(client, id, result));
}
subscriptions.unsubscribe();
}
}
}, err -> {
if (err instanceof TimeoutException) {
task.setError(new DDPClientCallback.RPC.Timeout(client));
}
}));
addErrorCallback(subscriptions, task);
sendMessage("method", json -> json.put("method", method).put("params", params).put("id", id));
}
private void subscribeBaseListeners() {
if (subscriptions != null &&
subscriptions.hasSubscriptions() && !subscriptions.isUnsubscribed()) {
return;
}
subscriptions = new CompositeSubscription();
subscriptions.add(
observable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
.map(DDPClientImpl::toJson)
.subscribe(response -> {
String msg = extractMsg(response);
if ("ping".equals(msg)) {
if (response.isNull("id")) {
sendMessage("pong", null);
} else {
sendMessage("pong", json -> json.put("id", response.getString("id")));
}
}
}, err -> {
}));
}
public Observable<DDPSubscription.Event> getDDPSubscription() {
String[] targetMsgs = { "added", "changed", "removed", "addedBefore", "movedBefore" };
return observable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
.map(DDPClientImpl::toJson)
.filter(response -> {
String msg = extractMsg(response);
for (String m : targetMsgs) {
if (m.equals(msg)) return true;
}
return false;
})
.map((Func1<JSONObject, DDPSubscription.Event>) response -> {
String msg = extractMsg(response);
if ("added".equals(msg)) {
return new DDPSubscription.Added(client, response.optString("collection"),
response.optString("id"),
response.isNull("fields") ? null : response.optJSONObject("fields"));
} else if ("addedBefore".equals(msg)) {
return new DDPSubscription.Added.Before(client, response.optString("collection"),
response.optString("id"),
response.isNull("fields") ? null : response.optJSONObject("fields"),
response.isNull("before") ? null : response.optString("before"));
} else if ("changed".equals(msg)) {
return new DDPSubscription.Changed(client, response.optString("collection"),
response.optString("id"),
response.isNull("fields") ? null : response.optJSONObject("fields"),
response.isNull("cleared") ? new JSONArray() : response.optJSONArray("before"));
} else if ("removed".equals(msg)) {
return new DDPSubscription.Removed(client, response.optString("collection"),
response.optString("id"));
} else if ("movedBefore".equals(msg)) {
return new DDPSubscription.MovedBefore(client, response.optString("collection"),
response.optString("id"),
response.isNull("before") ? null : response.optString("before"));
}
return null;
})
.asObservable();
}
public void unsubscribeBaseListeners() {
if (subscriptions.hasSubscriptions() && !subscriptions.isUnsubscribed()) {
subscriptions.unsubscribe();
}
}
public Task<RxWebSocketCallback.Close> getOnCloseCallback() {
TaskCompletionSource<RxWebSocketCallback.Close> task = new TaskCompletionSource<>();
observable.filter(callback -> callback instanceof RxWebSocketCallback.Close)
.cast(RxWebSocketCallback.Close.class)
.subscribe(close -> {
task.setResult(close);
}, err -> {
if (err instanceof Exception) {
task.setError((Exception) err);
} else {
task.setError(new Exception(err));
}
});
return task.getTask().onSuccessTask(_task -> {
unsubscribeBaseListeners();
return _task;
});
}
private void sendMessage(String msg, @Nullable JSONBuilder json) {
try {
JSONObject origJson = new JSONObject().put("msg", msg);
String msg2 = (json == null ? origJson : json.create(origJson)).toString();
websocket.sendText(msg2);
} catch (Exception e) {
Timber.e(e);
}
}
private void addErrorCallback(CompositeSubscription subscriptions, TaskCompletionSource<?> task) {
subscriptions.add(observable.subscribe(base -> {
}, err -> {
task.setError(new Exception(err));
subscriptions.unsubscribe();
}));
}
public void close(int code, String reason) {
try {
websocket.close(code, reason);
} catch (Exception e) {
Timber.e(e);
}
}
private interface JSONBuilder {
@NonNull JSONObject create(JSONObject root) throws JSONException;
}
}
package chat.rocket.android_ddp;
import android.support.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONObject;
public class DDPSubscription {
public static abstract class Event {
public final DDPClient client;
public Event(DDPClient client) {
this.client = client;
}
}
public static abstract class BaseException extends Exception {
public final DDPClient client;
public BaseException(DDPClient client) {
this.client = client;
}
}
public static class NoSub extends Event {
public String id;
public NoSub(DDPClient client, String id) {
super(client);
this.id = id;
}
@Override public String toString() {
return "NoSub[id=" + id + "]";
}
public static class Error extends BaseException {
String id;
JSONObject error;
public Error(DDPClient client, String id, JSONObject error) {
super(client);
this.id = id;
this.error = error;
}
}
}
public static class Ready extends Event {
public String id;
public Ready(DDPClient client, String id) {
super(client);
this.id = id;
}
@Override public String toString() {
return "Ready[id=" + id + "]";
}
}
public static class DocEvent extends Event {
public String collection;
public String docID;
public DocEvent(DDPClient client, String collection, String docID) {
super(client);
this.collection = collection;
this.docID = docID;
}
@Override public String toString() {
return "DocEvent[id=" + docID + ", collection=" + collection + "]";
}
}
public static class Added extends DocEvent {
public JSONObject fields;
public Added(DDPClient client, String collection, String docID, JSONObject fields) {
super(client, collection, docID);
this.fields = fields;
}
public static class Before extends Added {
public String before;
public Before(DDPClient client, String collection, String docID, JSONObject fields,
String before) {
super(client, collection, docID, fields);
this.before = before;
}
}
}
public static class Changed extends DocEvent {
public JSONObject fields;
public JSONArray cleared;
public Changed(DDPClient client, String collection, String docID, JSONObject fields,
@NonNull JSONArray cleared) {
super(client, collection, docID);
this.fields = fields;
this.cleared = cleared;
}
}
public static class Removed extends DocEvent {
public Removed(DDPClient client, String collection, String docID) {
super(client, collection, docID);
}
}
public static class MovedBefore extends DocEvent {
public String before;
public MovedBefore(DDPClient client, String collection, String docID, String before) {
super(client, collection, docID);
this.before = before;
}
}
}
package chat.rocket.android_ddp.rx;
import java.io.IOException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.ws.WebSocket;
import okhttp3.ws.WebSocketCall;
import okhttp3.ws.WebSocketListener;
import okio.Buffer;
import rx.Observable;
import rx.Subscriber;
import rx.exceptions.OnErrorNotImplementedException;
import rx.observables.ConnectableObservable;
import timber.log.Timber;
public class RxWebSocket {
private OkHttpClient httpClient;
private WebSocket webSocket;
private boolean isConnected;
public RxWebSocket(OkHttpClient client) {
httpClient = client;
isConnected = false;
}
public ConnectableObservable<RxWebSocketCallback.Base> connect(String url) {
final Request request = new Request.Builder().url(url).build();
WebSocketCall call = WebSocketCall.create(httpClient, request);
return Observable.create(new Observable.OnSubscribe<RxWebSocketCallback.Base>() {
@Override public void call(Subscriber<? super RxWebSocketCallback.Base> subscriber) {
call.enqueue(new WebSocketListener() {
@Override public void onOpen(WebSocket webSocket, Response response) {
isConnected = true;
RxWebSocket.this.webSocket = webSocket;
subscriber.onNext(new RxWebSocketCallback.Open(RxWebSocket.this.webSocket, response));
}
@Override public void onFailure(IOException e, Response response) {
try {
isConnected = false;
subscriber.onError(new RxWebSocketCallback.Failure(webSocket, e, response));
} catch (OnErrorNotImplementedException ex) {
Timber.w(ex, "OnErrorNotImplementedException ignored");
}
}
@Override public void onMessage(ResponseBody responseBody) throws IOException {
isConnected = true;
subscriber.onNext(new RxWebSocketCallback.Message(webSocket, responseBody));
}
@Override public void onPong(Buffer payload) {
isConnected = true;
subscriber.onNext(new RxWebSocketCallback.Pong(webSocket, payload));
}
@Override public void onClose(int code, String reason) {
isConnected = false;
subscriber.onNext(new RxWebSocketCallback.Close(webSocket, code, reason));
subscriber.onCompleted();
}
});
}
}).publish();
}
public void sendText(String message) throws IOException {
webSocket.sendMessage(RequestBody.create(WebSocket.TEXT, message));
}
public boolean isConnected() {
return isConnected;
}
public void close(int code, String reason) throws IOException {
webSocket.close(code, reason);
}
}
package chat.rocket.android_ddp.rx;
import java.io.IOException;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.ws.WebSocket;
import okio.Buffer;
import timber.log.Timber;
import static android.R.attr.type;
public class RxWebSocketCallback {
public static abstract class Base {
public String type;
public WebSocket ws;
public Base(String type, WebSocket ws) {
this.type = type;
this.ws = ws;
}
@Override public String toString() {
return "[" + type + "]";
}
}
public static class Open extends Base {
public Response response;
public Open(WebSocket websocket, Response response) {
super("Open", websocket);
this.response = response;
}
}
public static class Failure extends Exception {
public WebSocket ws;
public Response response;
public Failure(WebSocket websocket, IOException e, Response response) {
super(e);
this.ws = websocket;
this.response = response;
}
@Override public String toString() {
if (response != null) {
return "[" + type + "] " + response.message();
} else {
return super.toString();
}
}
}
public static class Message extends Base {
public String responseBodyString;
public Message(WebSocket websocket, ResponseBody responseBody) {
super("Message", websocket);
try {
this.responseBodyString = responseBody.string();
} catch (Exception e) {
Timber.e(e, "error in reading response(Message)");
}
}
@Override public String toString() {
return "[" + type + "] " + responseBodyString;
}
}
public static class Pong extends Base {
public Buffer payload;
public Pong(WebSocket websocket, Buffer payload) {
super("Pong", websocket);
this.payload = payload;
}
}
public static class Close extends Base {
public int code;
public String reason;
public Close(WebSocket websocket, int code, String reason) {
super("Close", websocket);
this.code = code;
this.reason = reason;
}
@Override public String toString() {
return "[" + type + "] code=" + code + ", reason=" + reason;
}
}
}
<resources>
<string name="app_name">android-ddp</string>
</resources>
...@@ -14,8 +14,8 @@ buildscript { ...@@ -14,8 +14,8 @@ buildscript {
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
classpath 'me.tatarka:gradle-retrolambda:3.3.1' classpath rootProject.ext.retroLambdaPlugin
classpath 'me.tatarka.retrolambda.projectlombok:lombok.ast:0.2.3.a2' classpath rootProject.ext.retroLambdaPatch
classpath rootProject.ext.realmPlugin classpath rootProject.ext.realmPlugin
classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1' classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
classpath 'com.google.gms:google-services:3.0.0' classpath 'com.google.gms:google-services:3.0.0'
...@@ -78,6 +78,7 @@ repositories { ...@@ -78,6 +78,7 @@ repositories {
} }
dependencies { dependencies {
compile project(':android-ddp')
compile project(':rocket-chat-android-widgets') compile project(':rocket-chat-android-widgets')
compile project(':realm-helpers') compile project(':realm-helpers')
compile rootProject.ext.supportAppCompat compile rootProject.ext.supportAppCompat
...@@ -98,8 +99,6 @@ dependencies { ...@@ -98,8 +99,6 @@ dependencies {
compile 'com.facebook.stetho:stetho-okhttp3:1.4.1' compile 'com.facebook.stetho:stetho-okhttp3:1.4.1'
compile 'com.uphyca:stetho_realm:2.0.1' compile 'com.uphyca:stetho_realm:2.0.1'
compile 'chat.rocket:android-ddp:0.0.8'
compile rootProject.ext.timber compile rootProject.ext.timber
compile 'com.jakewharton.rxbinding:rxbinding:0.4.0' compile 'com.jakewharton.rxbinding:rxbinding:0.4.0'
compile 'com.jakewharton.rxbinding:rxbinding-support-v4:0.4.0' compile 'com.jakewharton.rxbinding:rxbinding-support-v4:0.4.0'
......
...@@ -4,6 +4,7 @@ import android.support.multidex.MultiDexApplication; ...@@ -4,6 +4,7 @@ import android.support.multidex.MultiDexApplication;
import chat.rocket.android.model.ServerConfig; import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.realm_helper.RealmStore; import chat.rocket.android.realm_helper.RealmStore;
import com.facebook.stetho.Stetho; import com.facebook.stetho.Stetho;
import com.instabug.library.Feature;
import com.instabug.library.Instabug; import com.instabug.library.Instabug;
import com.instabug.library.invocation.InstabugInvocationEvent; import com.instabug.library.invocation.InstabugInvocationEvent;
import com.uphyca.stetho_realm.RealmInspectorModulesProvider; import com.uphyca.stetho_realm.RealmInspectorModulesProvider;
...@@ -38,6 +39,7 @@ public class RocketChatApplication extends MultiDexApplication { ...@@ -38,6 +39,7 @@ public class RocketChatApplication extends MultiDexApplication {
new Instabug.Builder(this, getString(R.string.instabug_api_key)) new Instabug.Builder(this, getString(R.string.instabug_api_key))
.setInvocationEvent(InstabugInvocationEvent.FLOATING_BUTTON) .setInvocationEvent(InstabugInvocationEvent.FLOATING_BUTTON)
.setInAppMessagingState(Feature.State.DISABLED) //not available in Free plan...
.build(); .build();
//TODO: add periodic trigger for RocketChatService.keepalive(this) here! //TODO: add periodic trigger for RocketChatService.keepalive(this) here!
......
...@@ -10,6 +10,7 @@ import chat.rocket.android.helper.OnBackPressListener; ...@@ -10,6 +10,7 @@ import chat.rocket.android.helper.OnBackPressListener;
import com.instabug.library.InstabugTrackingDelegate; import com.instabug.library.InstabugTrackingDelegate;
import com.trello.rxlifecycle.components.support.RxAppCompatActivity; import com.trello.rxlifecycle.components.support.RxAppCompatActivity;
import icepick.Icepick; import icepick.Icepick;
import timber.log.Timber;
abstract class AbstractFragmentActivity extends RxAppCompatActivity { abstract class AbstractFragmentActivity extends RxAppCompatActivity {
...@@ -66,7 +67,11 @@ abstract class AbstractFragmentActivity extends RxAppCompatActivity { ...@@ -66,7 +67,11 @@ abstract class AbstractFragmentActivity extends RxAppCompatActivity {
} }
@Override public boolean dispatchTouchEvent(MotionEvent event) { @Override public boolean dispatchTouchEvent(MotionEvent event) {
try {
InstabugTrackingDelegate.notifyActivityGotTouchEvent(event, this); InstabugTrackingDelegate.notifyActivityGotTouchEvent(event, this);
} catch (IllegalStateException exception) {
Timber.w(exception, "Instabug error (ignored)");
}
return super.dispatchTouchEvent(event); return super.dispatchTouchEvent(event);
} }
} }
...@@ -36,7 +36,7 @@ public class AddServerActivity extends AbstractFragmentActivity { ...@@ -36,7 +36,7 @@ public class AddServerActivity extends AbstractFragmentActivity {
if (config == null || config.getState() == ServerConfig.STATE_CONNECTION_ERROR) { if (config == null || config.getState() == ServerConfig.STATE_CONNECTION_ERROR) {
showFragment(new InputHostnameFragment()); showFragment(new InputHostnameFragment());
} else { } else {
showFragment(WaitingFragment.create("Connecting to server...")); showFragment(WaitingFragment.create(getString(R.string.add_server_activity_waiting_server)));
} }
}); });
......
...@@ -123,13 +123,6 @@ public class MainActivity extends AbstractAuthedActivity { ...@@ -123,13 +123,6 @@ public class MainActivity extends AbstractAuthedActivity {
} }
} }
@Override protected void onResume() {
super.onResume();
if (sessionObserver != null) {
sessionObserver.keepalive();
}
}
@Override protected void onDestroy() { @Override protected void onDestroy() {
if (sessionObserver != null) { if (sessionObserver != null) {
sessionObserver.unsub(); sessionObserver.unsub();
......
...@@ -76,7 +76,7 @@ public class ServerConfigActivity extends AbstractFragmentActivity { ...@@ -76,7 +76,7 @@ public class ServerConfigActivity extends AbstractFragmentActivity {
final String token = session.getToken(); final String token = session.getToken();
if (!TextUtils.isEmpty(token)) { if (!TextUtils.isEmpty(token)) {
if (TextUtils.isEmpty(session.getError())) { if (TextUtils.isEmpty(session.getError())) {
showFragment(WaitingFragment.create("Authenticating...")); showFragment(WaitingFragment.create(getString(R.string.server_config_activity_authenticating)));
} else { } else {
showFragment(new RetryLoginFragment()); showFragment(new RetryLoginFragment());
} }
......
...@@ -11,6 +11,7 @@ import java.util.UUID; ...@@ -11,6 +11,7 @@ import java.util.UUID;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import rx.Observable; import rx.Observable;
import timber.log.Timber;
/** /**
* DDP client wrapper. * DDP client wrapper.
...@@ -82,12 +83,28 @@ public class DDPClientWraper { ...@@ -82,12 +83,28 @@ public class DDPClientWraper {
*/ */
public Task<DDPClientCallback.RPC> rpc(String methodCallId, String methodName, String params, public Task<DDPClientCallback.RPC> rpc(String methodCallId, String methodName, String params,
long timeoutMs) { long timeoutMs) {
Timber.d("rpc:[%s]> %s(%s) timeout=%d", methodCallId, methodName, params, timeoutMs);
if (TextUtils.isEmpty(params)) { if (TextUtils.isEmpty(params)) {
return ddpClient.rpc(methodName, null, methodCallId, timeoutMs); return ddpClient.rpc(methodName, null, methodCallId, timeoutMs).continueWithTask(task -> {
if (task.isFaulted()) {
Timber.d("rpc:[%s]< error = %s", methodCallId, task.getError());
} else {
Timber.d("rpc:[%s]< result = %s", methodCallId, task.getResult().result);
}
return task;
});
} }
try { try {
return ddpClient.rpc(methodName, new JSONArray(params), methodCallId, timeoutMs); return ddpClient.rpc(methodName, new JSONArray(params), methodCallId, timeoutMs)
.continueWithTask(task -> {
if (task.isFaulted()) {
Timber.d("rpc:[%s]< error = %s", methodCallId, task.getError());
} else {
Timber.d("rpc:[%s]< result = %s", methodCallId, task.getResult().result);
}
return task;
});
} catch (JSONException exception) { } catch (JSONException exception) {
return Task.forError(exception); return Task.forError(exception);
} }
......
...@@ -6,6 +6,7 @@ import bolts.Continuation; ...@@ -6,6 +6,7 @@ import bolts.Continuation;
import bolts.Task; import bolts.Task;
import chat.rocket.android.helper.CheckSum; import chat.rocket.android.helper.CheckSum;
import chat.rocket.android.helper.TextUtils; import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.SyncState;
import chat.rocket.android.model.ddp.Message; import chat.rocket.android.model.ddp.Message;
import chat.rocket.android.model.ddp.RoomSubscription; import chat.rocket.android.model.ddp.RoomSubscription;
import chat.rocket.android.model.internal.MethodCall; import chat.rocket.android.model.internal.MethodCall;
...@@ -170,9 +171,9 @@ public class MethodCallHelper { ...@@ -170,9 +171,9 @@ public class MethodCallHelper {
} }
/** /**
* Login with GitHub OAuth. * Login with OAuth.
*/ */
public Task<Void> loginWithGitHub(final String credentialToken, public Task<Void> loginWithOAuth(final String credentialToken,
final String credentialSecret) { final String credentialSecret) {
return call("login", TIMEOUT_MS, () -> new JSONArray().put(new JSONObject() return call("login", TIMEOUT_MS, () -> new JSONArray().put(new JSONObject()
.put("oauth", new JSONObject() .put("oauth", new JSONObject()
...@@ -214,7 +215,7 @@ public class MethodCallHelper { ...@@ -214,7 +215,7 @@ public class MethodCallHelper {
/** /**
* request "subscriptions/get". * request "subscriptions/get".
*/ */
public Task<Void> getRooms() { public Task<Void> getRoomSubscriptions() {
return call("subscriptions/get", TIMEOUT_MS).onSuccessTask(CONVERT_TO_JSON_ARRAY) return call("subscriptions/get", TIMEOUT_MS).onSuccessTask(CONVERT_TO_JSON_ARRAY)
.onSuccessTask(task -> { .onSuccessTask(task -> {
final JSONArray result = task.getResult(); final JSONArray result = task.getResult();
...@@ -255,7 +256,10 @@ public class MethodCallHelper { ...@@ -255,7 +256,10 @@ public class MethodCallHelper {
return realmHelper.executeTransaction(realm -> { return realmHelper.executeTransaction(realm -> {
if (timestamp == 0) { if (timestamp == 0) {
realm.where(Message.class).equalTo("rid", roomId).findAll().deleteAllFromRealm(); realm.where(Message.class)
.equalTo("rid", roomId)
.equalTo("syncstate", SyncState.SYNCED)
.findAll().deleteAllFromRealm();
} }
if (messages.length() > 0) { if (messages.length() > 0) {
realm.createOrUpdateAllFromJson(Message.class, messages); realm.createOrUpdateAllFromJson(Message.class, messages);
...@@ -278,4 +282,35 @@ public class MethodCallHelper { ...@@ -278,4 +282,35 @@ public class MethodCallHelper {
return call("getUsersOfRoom", TIMEOUT_MS, () -> new JSONArray().put(roomId).put(showAll)) return call("getUsersOfRoom", TIMEOUT_MS, () -> new JSONArray().put(roomId).put(showAll))
.onSuccessTask(CONVERT_TO_JSON_OBJECT); .onSuccessTask(CONVERT_TO_JSON_OBJECT);
} }
/**
* send message.
*/
public Task<JSONObject> sendMessage(String messageId, String roomId, String msg) {
try {
return sendMessage(new JSONObject()
.put("_id", messageId)
.put("rid", roomId)
.put("msg", msg));
} catch (JSONException exception) {
return Task.forError(exception);
}
}
/**
* Send message object.
*/
public Task<JSONObject> sendMessage(final JSONObject messageJson) {
return call("sendMessage", TIMEOUT_MS, () -> new JSONArray().put(messageJson))
.onSuccessTask(CONVERT_TO_JSON_OBJECT)
.onSuccessTask(task -> Task.forResult(Message.customizeJson(task.getResult())));
}
/**
* mark all messages are read in the room.
*/
public Task<Void> readMessages(final String roomId) {
return call("readMessages", TIMEOUT_MS, () -> new JSONArray().put(roomId))
.onSuccessTask(task -> Task.forResult(null));
}
} }
...@@ -11,12 +11,12 @@ public class HomeFragment extends AbstractChatRoomFragment { ...@@ -11,12 +11,12 @@ public class HomeFragment extends AbstractChatRoomFragment {
} }
@Override protected void onSetupView() { @Override protected void onSetupView() {
activityToolbar.setTitle("Rocket.Chat - Home"); activityToolbar.setTitle(R.string.home_fragment_title);
} }
@Override public void onResume() { @Override public void onResume() {
super.onResume(); super.onResume();
activityToolbar.setNavigationIcon(null); activityToolbar.setNavigationIcon(null);
activityToolbar.setTitle("Rocket.Chat - Home"); activityToolbar.setTitle(R.string.home_fragment_title);
} }
} }
...@@ -2,37 +2,46 @@ package chat.rocket.android.fragment.chatroom; ...@@ -2,37 +2,46 @@ package chat.rocket.android.fragment.chatroom;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.GravityCompat; import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout; import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.SlidingPaneLayout; import android.support.v4.widget.SlidingPaneLayout;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.View; import android.view.View;
import chat.rocket.android.R; import chat.rocket.android.R;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.fragment.chatroom.dialog.UsersOfRoomDialogFragment; import chat.rocket.android.fragment.chatroom.dialog.UsersOfRoomDialogFragment;
import chat.rocket.android.helper.LoadMoreScrollListener; import chat.rocket.android.helper.LoadMoreScrollListener;
import chat.rocket.android.helper.LogcatIfError; import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.OnBackPressListener; import chat.rocket.android.helper.OnBackPressListener;
import chat.rocket.android.layouthelper.chatroom.MessageComposerManager;
import chat.rocket.android.layouthelper.chatroom.MessageListAdapter; import chat.rocket.android.layouthelper.chatroom.MessageListAdapter;
import chat.rocket.android.layouthelper.chatroom.PairedMessage;
import chat.rocket.android.model.ServerConfig; import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.model.SyncState; import chat.rocket.android.model.SyncState;
import chat.rocket.android.model.ddp.Message; import chat.rocket.android.model.ddp.Message;
import chat.rocket.android.model.ddp.RoomSubscription; import chat.rocket.android.model.ddp.RoomSubscription;
import chat.rocket.android.model.internal.LoadMessageProcedure; import chat.rocket.android.model.internal.LoadMessageProcedure;
import chat.rocket.android.realm_helper.RealmHelper; import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.realm_helper.RealmModelListAdapter;
import chat.rocket.android.realm_helper.RealmObjectObserver; import chat.rocket.android.realm_helper.RealmObjectObserver;
import chat.rocket.android.realm_helper.RealmStore; import chat.rocket.android.realm_helper.RealmStore;
import chat.rocket.android.service.RocketChatService; import chat.rocket.android.service.RocketChatService;
import chat.rocket.android.widget.message.MessageComposer;
import com.jakewharton.rxbinding.support.v4.widget.RxDrawerLayout; import com.jakewharton.rxbinding.support.v4.widget.RxDrawerLayout;
import io.realm.Sort; import io.realm.Sort;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.UUID;
import org.json.JSONObject; import org.json.JSONObject;
import timber.log.Timber; import timber.log.Timber;
/** /**
* Chat room screen. * Chat room screen.
*/ */
public class RoomFragment extends AbstractChatRoomFragment implements OnBackPressListener { public class RoomFragment extends AbstractChatRoomFragment
implements OnBackPressListener, RealmModelListAdapter.OnItemClickListener<PairedMessage> {
private String serverConfigId; private String serverConfigId;
private RealmHelper realmHelper; private RealmHelper realmHelper;
...@@ -41,6 +50,7 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres ...@@ -41,6 +50,7 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres
private String hostname; private String hostname;
private LoadMoreScrollListener scrollListener; private LoadMoreScrollListener scrollListener;
private RealmObjectObserver<LoadMessageProcedure> procedureObserver; private RealmObjectObserver<LoadMessageProcedure> procedureObserver;
private MessageComposerManager messageComposerManager;
/** /**
* create fragment with roomId. * create fragment with roomId.
...@@ -88,12 +98,14 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres ...@@ -88,12 +98,14 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres
@Override protected void onSetupView() { @Override protected void onSetupView() {
RecyclerView listView = (RecyclerView) rootView.findViewById(R.id.recyclerview); RecyclerView listView = (RecyclerView) rootView.findViewById(R.id.recyclerview);
listView.setAdapter(realmHelper.createListAdapter(getContext(), MessageListAdapter adapter = (MessageListAdapter) realmHelper.createListAdapter(getContext(),
realm -> realm.where(Message.class) realm -> realm.where(Message.class)
.equalTo("rid", roomId) .equalTo("rid", roomId)
.findAllSorted("ts", Sort.DESCENDING), .findAllSorted("ts", Sort.DESCENDING),
context -> new MessageListAdapter(context, hostname) context -> new MessageListAdapter(context, hostname)
)); );
listView.setAdapter(adapter);
adapter.setOnItemClickListener(this);
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(),
LinearLayoutManager.VERTICAL, true); LinearLayoutManager.VERTICAL, true);
...@@ -107,6 +119,33 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres ...@@ -107,6 +119,33 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres
listView.addOnScrollListener(scrollListener); listView.addOnScrollListener(scrollListener);
setupSideMenu(); setupSideMenu();
setupMessageComposer();
}
@Override public void onItemClick(PairedMessage pairedMessage) {
if (pairedMessage.target != null) {
final int syncstate = pairedMessage.target.getSyncstate();
if (syncstate == SyncState.FAILED) {
final String messageId = pairedMessage.target.get_id();
new AlertDialog.Builder(getContext())
.setPositiveButton(R.string.resend, (dialog, which) -> {
realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(Message.class, new JSONObject()
.put("_id", messageId)
.put("syncstate", SyncState.NOT_SYNCED))
).continueWith(new LogcatIfError());
})
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.discard, (dialog, which) -> {
realmHelper.executeTransaction(realm ->
realm.where(Message.class)
.equalTo("_id", messageId).findAll().deleteAllFromRealm()
).continueWith(new LogcatIfError());;
})
.show();
}
}
} }
private void setupSideMenu() { private void setupSideMenu() {
...@@ -128,7 +167,7 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres ...@@ -128,7 +167,7 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres
fieldSlidable.setAccessible(true); fieldSlidable.setAccessible(true);
fieldSlidable.setBoolean(pane, !opened); fieldSlidable.setBoolean(pane, !opened);
} catch (Exception exception) { } catch (Exception exception) {
Timber.w(exception); Timber.w(exception, "failed to set CanSlide.");
} }
}); });
} }
...@@ -143,7 +182,27 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres ...@@ -143,7 +182,27 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres
return false; return false;
} }
private void setupMessageComposer() {
final FloatingActionButton fabCompose =
(FloatingActionButton) rootView.findViewById(R.id.fab_compose);
final MessageComposer messageComposer =
(MessageComposer) rootView.findViewById(R.id.message_composer);
messageComposerManager = new MessageComposerManager(fabCompose, messageComposer);
messageComposerManager.setCallback(messageText ->
realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(Message.class, new JSONObject()
.put("_id", UUID.randomUUID().toString())
.put("syncstate", SyncState.NOT_SYNCED)
.put("ts", System.currentTimeMillis())
.put("rid", roomId)
.put("msg", messageText))));
}
private void onRenderRoom(RoomSubscription roomSubscription) { private void onRenderRoom(RoomSubscription roomSubscription) {
if (roomSubscription == null) {
return;
}
String type = roomSubscription.getT(); String type = roomSubscription.getT();
if (RoomSubscription.TYPE_CHANNEL.equals(type)) { if (RoomSubscription.TYPE_CHANNEL.equals(type)) {
activityToolbar.setNavigationIcon(R.drawable.ic_hashtag_white_24dp); activityToolbar.setNavigationIcon(R.drawable.ic_hashtag_white_24dp);
...@@ -211,11 +270,21 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres ...@@ -211,11 +270,21 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres
}).continueWith(new LogcatIfError()); }).continueWith(new LogcatIfError());
} }
private void markAsReadIfNeeded() {
RoomSubscription room = realmHelper.executeTransactionForRead(realm ->
realm.where(RoomSubscription.class).equalTo("rid", roomId).findFirst());
if (room != null && room.isAlert()) {
new MethodCallHelper(getContext(), serverConfigId).readMessages(roomId)
.continueWith(new LogcatIfError());
}
}
@Override public void onResume() { @Override public void onResume() {
super.onResume(); super.onResume();
roomObserver.sub(); roomObserver.sub();
procedureObserver.sub(); procedureObserver.sub();
closeSideMenuIfNeeded(); closeSideMenuIfNeeded();
markAsReadIfNeeded();
} }
@Override public void onPause() { @Override public void onPause() {
...@@ -225,6 +294,6 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres ...@@ -225,6 +294,6 @@ public class RoomFragment extends AbstractChatRoomFragment implements OnBackPres
} }
@Override public boolean onBackPressed() { @Override public boolean onBackPressed() {
return closeSideMenuIfNeeded(); return closeSideMenuIfNeeded() || messageComposerManager.hideMessageComposerIfNeeded();
} }
} }
package chat.rocket.android.fragment.oauth;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Base64;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.fragment.AbstractWebViewFragment;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.model.ddp.MeteorLoginServiceConfiguration;
import chat.rocket.android.realm_helper.RealmStore;
import java.nio.charset.Charset;
import org.json.JSONException;
import org.json.JSONObject;
import timber.log.Timber;
public abstract class AbstractOAuthFragment extends AbstractWebViewFragment {
protected String serverConfigId;
protected String hostname;
private String url;
private boolean resultOK;
protected abstract String getOAuthServiceName();
protected abstract String generateURL(MeteorLoginServiceConfiguration oauthConfig);
private boolean hasValidArgs(Bundle args) {
return args != null
&& args.containsKey("serverConfigId");
}
protected final String getStateString() {
try {
return Base64.encodeToString(new JSONObject().put("loginStyle", "popup")
.put("credentialToken", getOAuthServiceName() + System.currentTimeMillis())
.put("isCordova", true)
.toString()
.getBytes(Charset.forName("UTF-8")), Base64.NO_WRAP);
} catch (JSONException exception) {
throw new RuntimeException(exception);
}
}
@Override public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (!hasValidArgs(args)) {
throw new IllegalArgumentException(
"serverConfigId required");
}
serverConfigId = args.getString("serverConfigId");
ServerConfig serverConfig = RealmStore.getDefault().executeTransactionForRead(realm ->
realm.where(ServerConfig.class).equalTo("serverConfigId", serverConfigId).findFirst());
MeteorLoginServiceConfiguration oauthConfig =
RealmStore.get(serverConfigId).executeTransactionForRead(realm ->
realm.where(MeteorLoginServiceConfiguration.class)
.equalTo("service", getOAuthServiceName())
.findFirst());
if (serverConfig == null || oauthConfig == null) {
throw new IllegalArgumentException(
"Invalid serverConfigId given,");
}
hostname = serverConfig.getHostname();
url = generateURL(oauthConfig);
}
@Override protected void navigateToInitialPage(WebView webview) {
if (TextUtils.isEmpty(url)) {
finish();
return;
}
resultOK = false;
webview.loadUrl(url);
webview.addJavascriptInterface(new JSInterface(result -> {
// onPageFinish is called twice... Should ignore latter one.
if (resultOK) {
return;
}
if (result != null && result.optBoolean("setCredentialToken", false)) {
try {
final String credentialToken = result.getString("credentialToken");
final String credentialSecret = result.getString("credentialSecret");
handleOAuthCallback(credentialToken, credentialSecret);
resultOK = true;
} catch (JSONException exception) {
Timber.e(exception, "failed to parse OAuth result.");
}
}
onOAuthCompleted();
}), "_rocketchet_hook");
}
@Override protected void onPageLoaded(WebView webview, String url) {
super.onPageLoaded(webview, url);
if (url.contains(hostname) && url.contains("_oauth/" + getOAuthServiceName() + "?close")) {
final String jsHookUrl = "javascript:"
+ "window._rocketchet_hook.handleConfig(document.getElementById('config').innerText);";
webview.loadUrl(jsHookUrl);
}
}
private interface JSInterfaceCallback {
void hanldeResult(@Nullable JSONObject result);
}
private static final class JSInterface {
private final JSInterfaceCallback jsInterfaceCallback;
JSInterface(JSInterfaceCallback callback) {
jsInterfaceCallback = callback;
}
@JavascriptInterface public void handleConfig(String config) {
try {
jsInterfaceCallback.hanldeResult(new JSONObject(config));
} catch (Exception exception) {
jsInterfaceCallback.hanldeResult(null);
}
}
}
private void handleOAuthCallback(final String credentialToken, final String credentialSecret) {
new MethodCallHelper(getContext(), serverConfigId)
.loginWithOAuth(credentialToken, credentialSecret)
.continueWith(new LogcatIfError());
}
protected void onOAuthCompleted() {
}
}
package chat.rocket.android.fragment.oauth;
import chat.rocket.android.model.ddp.MeteorLoginServiceConfiguration;
import okhttp3.HttpUrl;
public class FacebookOAuthFragment extends AbstractOAuthFragment {
@Override protected String getOAuthServiceName() {
return "facebook";
}
@Override protected String generateURL(MeteorLoginServiceConfiguration oauthConfig) {
return new HttpUrl.Builder().scheme("https")
.host("www.facebook.com")
.addPathSegment("v2.2")
.addPathSegment("dialog")
.addPathSegment("oauth")
.addQueryParameter("client_id", oauthConfig.getAppId())
.addQueryParameter("redirect_uri", "https://" + hostname + "/_oauth/facebook?close")
.addQueryParameter("display", "popup")
.addQueryParameter("scope", "email")
.addQueryParameter("state", getStateString())
.build()
.toString();
}
}
package chat.rocket.android.fragment.oauth; package chat.rocket.android.fragment.oauth;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Base64;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import chat.rocket.android.fragment.AbstractWebViewFragment;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.model.ddp.MeteorLoginServiceConfiguration; import chat.rocket.android.model.ddp.MeteorLoginServiceConfiguration;
import chat.rocket.android.realm_helper.RealmStore;
import java.nio.charset.Charset;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import org.json.JSONException;
import org.json.JSONObject;
import timber.log.Timber;
public class GitHubOAuthFragment extends AbstractWebViewFragment { public class GitHubOAuthFragment extends AbstractOAuthFragment {
private String serverConfigId; @Override protected String getOAuthServiceName() {
private String hostname; return "github";
private String url;
private boolean resultOK;
/**
* create new Fragment with ServerConfig-ID.
*/
public static GitHubOAuthFragment create(final String serverConfigId) {
Bundle args = new Bundle();
args.putString("serverConfigId", serverConfigId);
GitHubOAuthFragment fragment = new GitHubOAuthFragment();
fragment.setArguments(args);
return fragment;
}
private boolean hasValidArgs(Bundle args) {
return args != null
&& args.containsKey("serverConfigId");
}
@Override public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (!hasValidArgs(args)) {
throw new IllegalArgumentException(
"serverConfigId required");
} }
serverConfigId = args.getString("serverConfigId"); @Override protected String generateURL(MeteorLoginServiceConfiguration oauthConfig) {
ServerConfig serverConfig = RealmStore.getDefault().executeTransactionForRead(realm ->
realm.where(ServerConfig.class).equalTo("serverConfigId", serverConfigId).findFirst());
MeteorLoginServiceConfiguration oauthConfig =
RealmStore.get(serverConfigId).executeTransactionForRead(realm ->
realm.where(MeteorLoginServiceConfiguration.class)
.equalTo("service", "github")
.findFirst());
if (serverConfig == null || oauthConfig == null) {
throw new IllegalArgumentException(
"Invalid serverConfigId given,");
}
hostname = serverConfig.getHostname();
url = generateURL(oauthConfig.getClientId());
}
private String generateURL(String clientId) {
try {
String state = Base64.encodeToString(new JSONObject().put("loginStyle", "popup")
.put("credentialToken", "github" + System.currentTimeMillis())
.put("isCordova", true)
.toString()
.getBytes(Charset.forName("UTF-8")), Base64.NO_WRAP);
return new HttpUrl.Builder().scheme("https") return new HttpUrl.Builder().scheme("https")
.host("github.com") .host("github.com")
.addPathSegment("login") .addPathSegment("login")
.addPathSegment("oauth") .addPathSegment("oauth")
.addPathSegment("authorize") .addPathSegment("authorize")
.addQueryParameter("client_id", clientId) .addQueryParameter("client_id", oauthConfig.getClientId())
.addQueryParameter("scope", "user:email") .addQueryParameter("scope", "user:email")
.addQueryParameter("state", state) .addQueryParameter("state", getStateString())
.build() .build()
.toString(); .toString();
} catch (Exception exception) {
Timber.e(exception, "failed to generate GitHub OAUth URL");
}
return null;
}
@Override protected void navigateToInitialPage(WebView webview) {
if (TextUtils.isEmpty(url)) {
finish();
return;
}
resultOK = false;
webview.loadUrl(url);
webview.addJavascriptInterface(new JSInterface(result -> {
// onPageFinish is called twice... Should ignore latter one.
if (resultOK) {
return;
}
if (result != null && result.optBoolean("setCredentialToken", false)) {
try {
final String credentialToken = result.getString("credentialToken");
final String credentialSecret = result.getString("credentialSecret");
handleOAuthCallback(credentialToken, credentialSecret);
resultOK = true;
} catch (JSONException exception) {
Timber.e(exception, "failed to parse OAuth result.");
}
}
onOAuthCompleted();
}), "_rocketchet_hook");
}
@Override protected void onPageLoaded(WebView webview, String url) {
super.onPageLoaded(webview, url);
if (url.contains(hostname) && url.contains("_oauth/github?close")) {
final String jsHookUrl = "javascript:"
+ "window._rocketchet_hook.handleConfig(document.getElementById('config').innerText);";
webview.loadUrl(jsHookUrl);
}
}
private interface JSInterfaceCallback {
void hanldeResult(@Nullable JSONObject result);
}
private static final class JSInterface {
private final JSInterfaceCallback jsInterfaceCallback;
JSInterface(JSInterfaceCallback callback) {
jsInterfaceCallback = callback;
}
@JavascriptInterface public void handleConfig(String config) {
try {
jsInterfaceCallback.hanldeResult(new JSONObject(config));
} catch (Exception exception) {
jsInterfaceCallback.hanldeResult(null);
}
}
}
private void handleOAuthCallback(final String credentialToken, final String credentialSecret) {
new MethodCallHelper(getContext(), serverConfigId)
.loginWithGitHub(credentialToken, credentialSecret)
.continueWith(new LogcatIfError());
}
private void onOAuthCompleted() {
} }
} }
package chat.rocket.android.fragment.oauth;
import chat.rocket.android.model.ddp.MeteorLoginServiceConfiguration;
import okhttp3.HttpUrl;
public class GoogleOAuthFragment extends AbstractOAuthFragment {
@Override protected String getOAuthServiceName() {
return "google";
}
@Override protected String generateURL(MeteorLoginServiceConfiguration oauthConfig) {
return new HttpUrl.Builder().scheme("https")
.host("accounts.google.com")
.addPathSegment("o")
.addPathSegment("oauth2")
.addPathSegment("auth")
.addQueryParameter("response_type", "code")
.addQueryParameter("client_id", oauthConfig.getClientId())
.addQueryParameter("scope", "profile email")
.addQueryParameter("redirect_uri", "https://" + hostname + "/_oauth/google?close")
.addQueryParameter("state", getStateString())
.build()
.toString();
}
}
package chat.rocket.android.fragment.oauth;
import chat.rocket.android.model.ddp.MeteorLoginServiceConfiguration;
public class TwitterOAuthFragment extends AbstractOAuthFragment {
@Override protected String getOAuthServiceName() {
return "twitter";
}
@Override protected String generateURL(MeteorLoginServiceConfiguration oauthConfig) {
return "https://" + hostname + "/_oauth/twitter/"
+ "?requestTokenAndRedirect=true&state=" + getStateString();
}
}
...@@ -49,7 +49,6 @@ public class InputHostnameFragment extends AbstractServerConfigFragment { ...@@ -49,7 +49,6 @@ public class InputHostnameFragment extends AbstractServerConfigFragment {
@Override public void onResume() { @Override public void onResume() {
super.onResume(); super.onResume();
serverConfigObserver.keepalive();
} }
@Override public void onDestroyView() { @Override public void onDestroyView() {
......
...@@ -3,16 +3,19 @@ package chat.rocket.android.fragment.server_config; ...@@ -3,16 +3,19 @@ package chat.rocket.android.fragment.server_config;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import chat.rocket.android.R; import chat.rocket.android.R;
import chat.rocket.android.fragment.oauth.GitHubOAuthFragment;
import chat.rocket.android.api.MethodCallHelper; import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.helper.TextUtils; import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.layouthelper.oauth.OAuthProviderInfo;
import chat.rocket.android.model.ddp.MeteorLoginServiceConfiguration; import chat.rocket.android.model.ddp.MeteorLoginServiceConfiguration;
import chat.rocket.android.realm_helper.RealmListObserver; import chat.rocket.android.realm_helper.RealmListObserver;
import chat.rocket.android.realm_helper.RealmStore; import chat.rocket.android.realm_helper.RealmStore;
import java.util.HashMap;
import java.util.List; import java.util.List;
import timber.log.Timber;
/** /**
* Login screen. * Login screen.
...@@ -67,30 +70,42 @@ public class LoginFragment extends AbstractServerConfigFragment { ...@@ -67,30 +70,42 @@ public class LoginFragment extends AbstractServerConfigFragment {
} }
private void onRenderAuthProviders(List<MeteorLoginServiceConfiguration> authProviders) { private void onRenderAuthProviders(List<MeteorLoginServiceConfiguration> authProviders) {
final View btnTwitter = rootView.findViewById(R.id.btn_login_with_twitter); HashMap<String, View> viewMap = new HashMap<>();
final View btnGitHub = rootView.findViewById(R.id.btn_login_with_github); HashMap<String, Boolean> supportedMap = new HashMap<>();
for (OAuthProviderInfo info : OAuthProviderInfo.LIST) {
viewMap.put(info.serviceName, rootView.findViewById(info.buttonId));
supportedMap.put(info.serviceName, false);
}
boolean hasTwitter = false;
boolean hasGitHub = false;
for (MeteorLoginServiceConfiguration authProvider : authProviders) { for (MeteorLoginServiceConfiguration authProvider : authProviders) {
if (!hasTwitter for (OAuthProviderInfo info : OAuthProviderInfo.LIST) {
&& "twitter".equals(authProvider.getService())) { if (!supportedMap.get(info.serviceName)
hasTwitter = true; && info.serviceName.equals(authProvider.getService())) {
btnTwitter.setOnClickListener(view -> { supportedMap.put(info.serviceName, true);
viewMap.get(info.serviceName).setOnClickListener(view -> {
}); Fragment fragment = null;
try {
fragment = info.fragmentClass.newInstance();
} catch (java.lang.InstantiationException | IllegalAccessException exception) {
Timber.w(exception, "failed to create new Fragment");
}
if (fragment != null) {
Bundle args = new Bundle();
args.putString("serverConfigId", serverConfigId);
fragment.setArguments(args);
showFragmentWithBackStack(fragment);
} }
if (!hasGitHub
&& "github".equals(authProvider.getService())) {
hasGitHub = true;
btnGitHub.setOnClickListener(view -> {
showFragmentWithBackStack(GitHubOAuthFragment.create(serverConfigId));
}); });
viewMap.get(info.serviceName).setVisibility(View.VISIBLE);
}
} }
} }
btnTwitter.setVisibility(hasTwitter ? View.VISIBLE : View.GONE); for (OAuthProviderInfo info : OAuthProviderInfo.LIST) {
btnGitHub.setVisibility(hasGitHub ? View.VISIBLE : View.GONE); if (!supportedMap.get(info.serviceName)) {
viewMap.get(info.serviceName).setVisibility(View.GONE);
}
}
} }
@Override public void onResume() { @Override public void onResume() {
......
...@@ -67,7 +67,7 @@ public class SidebarMainFragment extends AbstractFragment { ...@@ -67,7 +67,7 @@ public class SidebarMainFragment extends AbstractFragment {
.setOnUpdateListener(list -> roomListManager.setRooms(list)); .setOnUpdateListener(list -> roomListManager.setRooms(list));
currentUserObserver = realmHelper currentUserObserver = realmHelper
.createObjectObserver(realm -> realm.where(User.class).isNotEmpty("emails")) .createObjectObserver(User::queryCurrentUser)
.setOnUpdateListener(this::onRenderCurrentUser); .setOnUpdateListener(this::onRenderCurrentUser);
methodCallHelper = new MethodCallHelper(getContext(), serverConfigId); methodCallHelper = new MethodCallHelper(getContext(), serverConfigId);
......
package chat.rocket.android.layouthelper.chatroom;
import android.support.design.widget.FloatingActionButton;
import bolts.Task;
import chat.rocket.android.widget.message.MessageComposer;
/**
* handling visibility of FAB-compose and MessageComposer.
*/
public class MessageComposerManager {
public interface Callback {
Task<Void> onSubmit(String messageText);
}
private final FloatingActionButton fabCompose;
private final MessageComposer messageComposer;
private Callback callback;
public MessageComposerManager(FloatingActionButton fabCompose, MessageComposer messageComposer) {
this.fabCompose = fabCompose;
this.messageComposer = messageComposer;
init();
}
private void init() {
fabCompose.setOnClickListener(view -> {
setMessageComposerVisibility(true);
});
messageComposer.setOnActionListener(new MessageComposer.ActionListener() {
@Override public void onSubmit(String message) {
if (callback != null) {
messageComposer.setEnabled(false);
callback.onSubmit(message).onSuccess(task -> {
clearComposingText();
return null;
}).continueWith(task -> {
messageComposer.setEnabled(true);
return null;
});
}
}
@Override public void onCancel() {
setMessageComposerVisibility(false);
}
});
setMessageComposerVisibility(false);
}
public void setCallback(Callback callback) {
this.callback = callback;
}
public void clearComposingText() {
messageComposer.setText("");
}
private void setMessageComposerVisibility(boolean show) {
if (show) {
fabCompose.hide();
messageComposer.show(null);
} else {
messageComposer.hide(fabCompose::show);
}
}
public boolean hideMessageComposerIfNeeded() {
if (messageComposer.isShown()) {
setMessageComposerVisibility(false);
return true;
}
return false;
}
}
...@@ -7,6 +7,7 @@ import android.widget.TextView; ...@@ -7,6 +7,7 @@ import android.widget.TextView;
import chat.rocket.android.R; import chat.rocket.android.R;
import chat.rocket.android.helper.DateTime; import chat.rocket.android.helper.DateTime;
import chat.rocket.android.helper.TextUtils; import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.SyncState;
import chat.rocket.android.realm_helper.RealmModelViewHolder; import chat.rocket.android.realm_helper.RealmModelViewHolder;
import chat.rocket.android.renderer.MessageRenderer; import chat.rocket.android.renderer.MessageRenderer;
import chat.rocket.android.widget.message.RocketChatMessageLayout; import chat.rocket.android.widget.message.RocketChatMessageLayout;
...@@ -49,6 +50,15 @@ public class MessageViewHolder extends RealmModelViewHolder<PairedMessage> { ...@@ -49,6 +50,15 @@ public class MessageViewHolder extends RealmModelViewHolder<PairedMessage> {
.timestampInto(timestamp) .timestampInto(timestamp)
.bodyInto(body); .bodyInto(body);
if (pairedMessage.target != null) {
int syncstate = pairedMessage.target.getSyncstate();
if (syncstate == SyncState.NOT_SYNCED || syncstate == SyncState.SYNCING) {
itemView.setAlpha(0.6f);
} else {
itemView.setAlpha(1.0f);
}
}
renderNewDayAndSequential(pairedMessage); renderNewDayAndSequential(pairedMessage);
} }
......
...@@ -7,7 +7,7 @@ import chat.rocket.android.model.ddp.Message; ...@@ -7,7 +7,7 @@ import chat.rocket.android.model.ddp.Message;
* View Model for messages in chatroom. * View Model for messages in chatroom.
*/ */
public class PairedMessage { public class PairedMessage {
final Message target; public final Message target;
final Message nextSibling; final Message nextSibling;
public PairedMessage(Message target, Message nextSibling) { public PairedMessage(Message target, Message nextSibling) {
......
package chat.rocket.android.layouthelper.oauth;
import chat.rocket.android.R;
import chat.rocket.android.fragment.oauth.AbstractOAuthFragment;
import chat.rocket.android.fragment.oauth.FacebookOAuthFragment;
import chat.rocket.android.fragment.oauth.GitHubOAuthFragment;
import chat.rocket.android.fragment.oauth.GoogleOAuthFragment;
import chat.rocket.android.fragment.oauth.TwitterOAuthFragment;
import java.util.ArrayList;
public class OAuthProviderInfo {
public String serviceName;
public int buttonId;
public Class<? extends AbstractOAuthFragment> fragmentClass;
public OAuthProviderInfo(String serviceName, int buttonId,
Class<? extends AbstractOAuthFragment> fragmentClass) {
this.serviceName = serviceName;
this.buttonId = buttonId;
this.fragmentClass = fragmentClass;
}
public static ArrayList<OAuthProviderInfo> LIST = new ArrayList<OAuthProviderInfo>() {
{
add(new OAuthProviderInfo(
"twitter", R.id.btn_login_with_twitter, TwitterOAuthFragment.class));
add(new OAuthProviderInfo(
"github", R.id.btn_login_with_github, GitHubOAuthFragment.class));
add(new OAuthProviderInfo(
"google", R.id.btn_login_with_google, GoogleOAuthFragment.class));
add(new OAuthProviderInfo(
"facebook", R.id.btn_login_with_facebook, FacebookOAuthFragment.class));
}
};
}
package chat.rocket.android.model.ddp; package chat.rocket.android.model.ddp;
import io.realm.Realm;
import io.realm.RealmList; import io.realm.RealmList;
import io.realm.RealmObject; import io.realm.RealmObject;
import io.realm.RealmQuery;
import io.realm.annotations.PrimaryKey; import io.realm.annotations.PrimaryKey;
/** /**
...@@ -60,4 +62,8 @@ public class User extends RealmObject { ...@@ -60,4 +62,8 @@ public class User extends RealmObject {
public void setEmails(RealmList<Email> emails) { public void setEmails(RealmList<Email> emails) {
this.emails = emails; this.emails = emails;
} }
public static RealmQuery<User> queryCurrentUser(Realm realm) {
return realm.where(User.class).isNotEmpty("emails");
}
} }
...@@ -12,6 +12,7 @@ import chat.rocket.android.realm_helper.RealmObjectObserver; ...@@ -12,6 +12,7 @@ import chat.rocket.android.realm_helper.RealmObjectObserver;
import chat.rocket.android.service.RocketChatService; import chat.rocket.android.service.RocketChatService;
import io.realm.RealmObject; import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey; import io.realm.annotations.PrimaryKey;
import java.util.HashMap;
import java.util.UUID; import java.util.UUID;
import org.json.JSONObject; import org.json.JSONObject;
import timber.log.Timber; import timber.log.Timber;
...@@ -89,6 +90,8 @@ public class MethodCall extends RealmObject { ...@@ -89,6 +90,8 @@ public class MethodCall extends RealmObject {
} }
} }
private static final HashMap<String, RealmObjectObserver<MethodCall>> refMap = new HashMap<>();
/** /**
* insert a new record to request a method call. * insert a new record to request a method call.
*/ */
...@@ -122,14 +125,17 @@ public class MethodCall extends RealmObject { ...@@ -122,14 +125,17 @@ public class MethodCall extends RealmObject {
task.setResult(resultJson); task.setResult(resultJson);
} }
observer.unsub(); observer.unsub();
refMap.remove(methodCall.getMethodCallId());
remove(realmHelper, methodCall.getMethodCallId()).continueWith(new LogcatIfError()); remove(realmHelper, methodCall.getMethodCallId()).continueWith(new LogcatIfError());
} else if (syncstate == SyncState.FAILED) { } else if (syncstate == SyncState.FAILED) {
task.setError(new Error(methodCall.getResultJson())); task.setError(new Error(methodCall.getResultJson()));
observer.unsub(); observer.unsub();
refMap.remove(methodCall.getMethodCallId());
remove(realmHelper, methodCall.getMethodCallId()).continueWith(new LogcatIfError()); remove(realmHelper, methodCall.getMethodCallId()).continueWith(new LogcatIfError());
} }
}); });
observer.sub(); observer.sub();
refMap.put(newId, observer);
if (context != null) { if (context != null) {
RocketChatService.keepalive(context); RocketChatService.keepalive(context);
......
...@@ -3,7 +3,9 @@ package chat.rocket.android.renderer; ...@@ -3,7 +3,9 @@ package chat.rocket.android.renderer;
import android.content.Context; import android.content.Context;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import chat.rocket.android.R;
import chat.rocket.android.helper.DateTime; import chat.rocket.android.helper.DateTime;
import chat.rocket.android.model.SyncState;
import chat.rocket.android.model.ddp.Message; import chat.rocket.android.model.ddp.Message;
import chat.rocket.android.widget.message.RocketChatMessageLayout; import chat.rocket.android.widget.message.RocketChatMessageLayout;
...@@ -23,7 +25,14 @@ public class MessageRenderer extends AbstractRenderer<Message> { ...@@ -23,7 +25,14 @@ public class MessageRenderer extends AbstractRenderer<Message> {
* show Avatar image. * show Avatar image.
*/ */
public MessageRenderer avatarInto(ImageView imageView, String hostname) { public MessageRenderer avatarInto(ImageView imageView, String hostname) {
switch (object.getSyncstate()) {
case SyncState.FAILED:
imageView.setImageResource(R.drawable.ic_error_outline_black_24dp);
break;
default:
userRenderer.avatarInto(imageView, hostname); userRenderer.avatarInto(imageView, hostname);
break;
}
return this; return this;
} }
...@@ -43,7 +52,15 @@ public class MessageRenderer extends AbstractRenderer<Message> { ...@@ -43,7 +52,15 @@ public class MessageRenderer extends AbstractRenderer<Message> {
return this; return this;
} }
switch (object.getSyncstate()) {
case SyncState.NOT_SYNCED:
case SyncState.SYNCING:
textView.setText(R.string.sending);
break;
default:
textView.setText(DateTime.fromEpocMs(object.getTs(), DateTime.Format.TIME)); textView.setText(DateTime.fromEpocMs(object.getTs(), DateTime.Format.TIME));
break;
}
return this; return this;
} }
......
...@@ -9,11 +9,6 @@ public interface Registerable { ...@@ -9,11 +9,6 @@ public interface Registerable {
*/ */
void register(); void register();
/**
* keepalive.
*/
void keepalive();
/** /**
* unregister. * unregister.
*/ */
......
...@@ -88,7 +88,7 @@ public class RocketChatService extends Service { ...@@ -88,7 +88,7 @@ public class RocketChatService extends Service {
} }
return null; return null;
}).onSuccessTask(task -> { }).onSuccessTask(task -> {
connectionRequiredServerConfigObserver.keepalive(); connectionRequiredServerConfigObserver.sub();
return null; return null;
}); });
return START_STICKY; return START_STICKY;
...@@ -133,6 +133,13 @@ public class RocketChatService extends Service { ...@@ -133,6 +133,13 @@ public class RocketChatService extends Service {
} }
} }
@Override public void onDestroy() {
if (connectionRequiredServerConfigObserver != null) {
connectionRequiredServerConfigObserver.unsub();
}
super.onDestroy();
}
@Nullable @Nullable
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
......
...@@ -13,11 +13,14 @@ import chat.rocket.android.model.ServerConfig; ...@@ -13,11 +13,14 @@ import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.model.internal.Session; import chat.rocket.android.model.internal.Session;
import chat.rocket.android.realm_helper.RealmHelper; import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.realm_helper.RealmStore; import chat.rocket.android.realm_helper.RealmStore;
import chat.rocket.android.service.ddp.ActiveUsersSubscriber; import chat.rocket.android.service.ddp.base.ActiveUsersSubscriber;
import chat.rocket.android.service.ddp.LoginServiceConfigurationSubscriber; import chat.rocket.android.service.ddp.base.LoginServiceConfigurationSubscriber;
import chat.rocket.android.service.ddp.base.UserDataSubscriber;
import chat.rocket.android.service.observer.CurrentUserObserver;
import chat.rocket.android.service.observer.GetUsersOfRoomsProcedureObserver; import chat.rocket.android.service.observer.GetUsersOfRoomsProcedureObserver;
import chat.rocket.android.service.observer.LoadMessageProcedureObserver; import chat.rocket.android.service.observer.LoadMessageProcedureObserver;
import chat.rocket.android.service.observer.MethodCallObserver; import chat.rocket.android.service.observer.MethodCallObserver;
import chat.rocket.android.service.observer.NewMessageObserver;
import chat.rocket.android.service.observer.SessionObserver; import chat.rocket.android.service.observer.SessionObserver;
import chat.rocket.android.service.observer.TokenLoginObserver; import chat.rocket.android.service.observer.TokenLoginObserver;
import chat.rocket.android_ddp.DDPClientCallback; import chat.rocket.android_ddp.DDPClientCallback;
...@@ -35,11 +38,14 @@ public class RocketChatWebSocketThread extends HandlerThread { ...@@ -35,11 +38,14 @@ public class RocketChatWebSocketThread extends HandlerThread {
private static final Class[] REGISTERABLE_CLASSES = { private static final Class[] REGISTERABLE_CLASSES = {
LoginServiceConfigurationSubscriber.class, LoginServiceConfigurationSubscriber.class,
ActiveUsersSubscriber.class, ActiveUsersSubscriber.class,
UserDataSubscriber.class,
TokenLoginObserver.class, TokenLoginObserver.class,
MethodCallObserver.class, MethodCallObserver.class,
SessionObserver.class, SessionObserver.class,
LoadMessageProcedureObserver.class, LoadMessageProcedureObserver.class,
GetUsersOfRoomsProcedureObserver.class GetUsersOfRoomsProcedureObserver.class,
NewMessageObserver.class,
CurrentUserObserver.class
}; };
private final Context appContext; private final Context appContext;
private final String serverConfigId; private final String serverConfigId;
...@@ -132,8 +138,6 @@ public class RocketChatWebSocketThread extends HandlerThread { ...@@ -132,8 +138,6 @@ public class RocketChatWebSocketThread extends HandlerThread {
} }
return null; return null;
}); });
} else {
new Handler(getLooper()).post(this::keepaliveListeners);
} }
} }
...@@ -225,17 +229,6 @@ public class RocketChatWebSocketThread extends HandlerThread { ...@@ -225,17 +229,6 @@ public class RocketChatWebSocketThread extends HandlerThread {
} }
} }
//@DebugLog
private void keepaliveListeners() {
if (!listenersRegistered) {
return;
}
for (Registerable registerable : listeners) {
registerable.keepalive();
}
}
@DebugLog @DebugLog
private void unregisterListeners() { private void unregisterListeners() {
if (!listenersRegistered) { if (!listenersRegistered) {
......
...@@ -2,20 +2,21 @@ package chat.rocket.android.service.ddp; ...@@ -2,20 +2,21 @@ package chat.rocket.android.service.ddp;
import android.content.Context; import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.helper.LogcatIfError; import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.realm_helper.RealmHelper; import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.service.Registerable; import chat.rocket.android.service.Registerable;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android_ddp.DDPSubscription; import chat.rocket.android_ddp.DDPSubscription;
import io.realm.Realm; import io.realm.Realm;
import io.realm.RealmObject; import io.realm.RealmObject;
import java.util.Iterator; import java.util.Iterator;
import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import rx.Subscription; import rx.Subscription;
import timber.log.Timber; import timber.log.Timber;
abstract class AbstractDDPDocEventSubscriber implements Registerable { public abstract class AbstractDDPDocEventSubscriber implements Registerable {
protected final Context context; protected final Context context;
protected final RealmHelper realmHelper; protected final RealmHelper realmHelper;
protected final DDPClientWraper ddpClient; protected final DDPClientWraper ddpClient;
...@@ -31,16 +32,33 @@ abstract class AbstractDDPDocEventSubscriber implements Registerable { ...@@ -31,16 +32,33 @@ abstract class AbstractDDPDocEventSubscriber implements Registerable {
protected abstract String getSubscriptionName(); protected abstract String getSubscriptionName();
protected abstract String getSubscriptionCallbackName(); protected abstract JSONArray getSubscriptionParams() throws JSONException;
protected boolean shouldTruncateTableOnInitialize() {
return false;
}
protected abstract boolean isTarget(String callbackName);
protected abstract Class<? extends RealmObject> getModelClass(); protected abstract Class<? extends RealmObject> getModelClass();
protected JSONObject customizeFieldJson(JSONObject json) { protected JSONObject customizeFieldJson(JSONObject json) throws JSONException {
return json; return json;
} }
@Override public void register() { protected void onRegister() {}
ddpClient.subscribe(getSubscriptionName(), null).onSuccess(task -> {
protected void onUnregister() {}
@Override public final void register() {
JSONArray params = null;
try {
params = getSubscriptionParams();
} catch (JSONException exception) {
// just ignore.
}
ddpClient.subscribe(getSubscriptionName(), params).onSuccess(task -> {
subscriptionId = task.getResult().id; subscriptionId = task.getResult().id;
return null; return null;
}).continueWith(task -> { }).continueWith(task -> {
...@@ -50,20 +68,25 @@ abstract class AbstractDDPDocEventSubscriber implements Registerable { ...@@ -50,20 +68,25 @@ abstract class AbstractDDPDocEventSubscriber implements Registerable {
return null; return null;
}); });
if (shouldTruncateTableOnInitialize()) {
realmHelper.executeTransaction(realm -> { realmHelper.executeTransaction(realm -> {
realm.delete(getModelClass()); realm.delete(getModelClass());
return null; return null;
}).onSuccess(task -> { }).onSuccess(task -> {
registerSubscriptionCallback(); rxSubscription = subscribe();
return null; return null;
}).continueWith(new LogcatIfError()); }).continueWith(new LogcatIfError());
} else {
rxSubscription = subscribe();
}
onRegister();
} }
private void registerSubscriptionCallback() { protected Subscription subscribe() {
rxSubscription = ddpClient.getSubscriptionCallback() return ddpClient.getSubscriptionCallback()
.filter(event -> event instanceof DDPSubscription.DocEvent) .filter(event -> event instanceof DDPSubscription.DocEvent)
.cast(DDPSubscription.DocEvent.class) .cast(DDPSubscription.DocEvent.class)
.filter(event -> getSubscriptionCallbackName().equals(event.collection)) .filter(event -> isTarget(event.collection))
.subscribe(docEvent -> { .subscribe(docEvent -> {
try { try {
if (docEvent instanceof DDPSubscription.Added.Before) { if (docEvent instanceof DDPSubscription.Added.Before) {
...@@ -108,10 +131,12 @@ abstract class AbstractDDPDocEventSubscriber implements Registerable { ...@@ -108,10 +131,12 @@ abstract class AbstractDDPDocEventSubscriber implements Registerable {
throws JSONException { throws JSONException {
//executed in RealmTransaction //executed in RealmTransaction
JSONObject json = new JSONObject().put("_id", docEvent.docID); JSONObject json = new JSONObject().put("_id", docEvent.docID);
if (docEvent.cleared != null) {
for (int i = 0; i < docEvent.cleared.length(); i++) { for (int i = 0; i < docEvent.cleared.length(); i++) {
String fieldToDelete = docEvent.cleared.getString(i); String fieldToDelete = docEvent.cleared.getString(i);
json.put(fieldToDelete, JSONObject.NULL); json.put(fieldToDelete, JSONObject.NULL);
} }
}
mergeJson(json, docEvent.fields); mergeJson(json, docEvent.fields);
realm.createOrUpdateObjectFromJson(getModelClass(), customizeFieldJson(json)); realm.createOrUpdateObjectFromJson(getModelClass(), customizeFieldJson(json));
} }
...@@ -137,11 +162,8 @@ abstract class AbstractDDPDocEventSubscriber implements Registerable { ...@@ -137,11 +162,8 @@ abstract class AbstractDDPDocEventSubscriber implements Registerable {
} }
} }
@Override public void keepalive() { @Override public final void unregister() {
onUnregister();
}
@Override public void unregister() {
if (rxSubscription != null) { if (rxSubscription != null) {
rxSubscription.unsubscribe(); rxSubscription.unsubscribe();
} }
......
package chat.rocket.android.service.ddp.base;
import android.content.Context;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.service.ddp.AbstractDDPDocEventSubscriber;
import org.json.JSONArray;
abstract class AbstractBaseSubscriber extends AbstractDDPDocEventSubscriber {
protected AbstractBaseSubscriber(Context context, RealmHelper realmHelper,
DDPClientWraper ddpClient) {
super(context, realmHelper, ddpClient);
}
@Override protected final JSONArray getSubscriptionParams() {
return null;
}
@Override protected final boolean shouldTruncateTableOnInitialize() {
return true;
}
protected abstract String getSubscriptionCallbackName();
@Override protected final boolean isTarget(String callbackName) {
return getSubscriptionCallbackName().equals(callbackName);
}
}
package chat.rocket.android.service.ddp; package chat.rocket.android.service.ddp.base;
import android.content.Context; import android.content.Context;
import chat.rocket.android.model.ddp.User; import chat.rocket.android.model.ddp.User;
...@@ -9,7 +9,7 @@ import io.realm.RealmObject; ...@@ -9,7 +9,7 @@ import io.realm.RealmObject;
/** /**
* "activeUsers" subscriber. * "activeUsers" subscriber.
*/ */
public class ActiveUsersSubscriber extends AbstractDDPDocEventSubscriber { public class ActiveUsersSubscriber extends AbstractBaseSubscriber {
public ActiveUsersSubscriber(Context context, RealmHelper realmHelper, public ActiveUsersSubscriber(Context context, RealmHelper realmHelper,
DDPClientWraper ddpClient) { DDPClientWraper ddpClient) {
super(context, realmHelper, ddpClient); super(context, realmHelper, ddpClient);
......
package chat.rocket.android.service.ddp; package chat.rocket.android.service.ddp.base;
import android.content.Context; import android.content.Context;
import chat.rocket.android.model.ddp.MeteorLoginServiceConfiguration; import chat.rocket.android.model.ddp.MeteorLoginServiceConfiguration;
...@@ -9,7 +9,7 @@ import io.realm.RealmObject; ...@@ -9,7 +9,7 @@ import io.realm.RealmObject;
/** /**
* meteor.loginServiceConfiguration subscriber * meteor.loginServiceConfiguration subscriber
*/ */
public class LoginServiceConfigurationSubscriber extends AbstractDDPDocEventSubscriber { public class LoginServiceConfigurationSubscriber extends AbstractBaseSubscriber {
public LoginServiceConfigurationSubscriber(Context context, RealmHelper realmHelper, public LoginServiceConfigurationSubscriber(Context context, RealmHelper realmHelper,
DDPClientWraper ddpClient) { DDPClientWraper ddpClient) {
super(context, realmHelper, ddpClient); super(context, realmHelper, ddpClient);
......
package chat.rocket.android.service.ddp.base;
import android.content.Context;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.model.ddp.User;
import chat.rocket.android.realm_helper.RealmHelper;
import io.realm.RealmObject;
/**
* "userData" subscriber.
*/
public class UserDataSubscriber extends AbstractBaseSubscriber {
public UserDataSubscriber(Context context, RealmHelper realmHelper,
DDPClientWraper ddpClient) {
super(context, realmHelper, ddpClient);
}
@Override protected String getSubscriptionName() {
return "userData";
}
@Override protected String getSubscriptionCallbackName() {
return "users";
}
@Override protected Class<? extends RealmObject> getModelClass() {
return User.class;
}
}
package chat.rocket.android.service.ddp.stream;
import android.content.Context;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.service.ddp.AbstractDDPDocEventSubscriber;
import chat.rocket.android_ddp.DDPSubscription;
import org.json.JSONArray;
import org.json.JSONObject;
import timber.log.Timber;
abstract class AbstractStreamNotifyEventSubscriber extends AbstractDDPDocEventSubscriber {
protected AbstractStreamNotifyEventSubscriber(Context context, RealmHelper realmHelper,
DDPClientWraper ddpClient) {
super(context, realmHelper, ddpClient);
}
@Override protected final boolean shouldTruncateTableOnInitialize() {
return false;
}
@Override protected final boolean isTarget(String callbackName) {
return getSubscriptionName().equals(callbackName);
}
protected abstract String getPrimaryKeyForModel();
@Override protected void onDocumentChanged(DDPSubscription.Changed docEvent) {
try {
JSONArray args = docEvent.fields.getJSONArray("args");
String msg = args.length() > 0 ? args.getString(0) : null;
JSONObject target = args.getJSONObject(args.length() - 1);
if ("removed".equals(msg)) {
realmHelper.executeTransaction(realm ->
realm.where(getModelClass())
.equalTo(getPrimaryKeyForModel(), target.getString(getPrimaryKeyForModel()))
.findAll().deleteAllFromRealm()
).continueWith(new LogcatIfError());
} else { //inserted, updated
realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(getModelClass(), customizeFieldJson(target))
).continueWith(new LogcatIfError());
}
} catch (Exception exception) {
Timber.w(exception, "failed to save stream-notify event.");
}
}
}
package chat.rocket.android.service.ddp.stream;
import android.content.Context;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.model.ddp.RoomSubscription;
import chat.rocket.android.realm_helper.RealmHelper;
import io.realm.RealmObject;
import org.json.JSONArray;
import org.json.JSONException;
public class StreamNotifyUserSubscriptionsChanged extends AbstractStreamNotifyEventSubscriber {
private final String userId;
public StreamNotifyUserSubscriptionsChanged(Context context, RealmHelper realmHelper,
DDPClientWraper ddpClient, String userId) {
super(context, realmHelper, ddpClient);
this.userId = userId;
}
@Override protected String getSubscriptionName() {
return "stream-notify-user";
}
@Override protected JSONArray getSubscriptionParams() throws JSONException {
return new JSONArray()
.put(userId + "/subscriptions-changed")
.put(false);
}
@Override protected Class<? extends RealmObject> getModelClass() {
return RoomSubscription.class;
}
@Override protected String getPrimaryKeyForModel() {
return "rid";
}
}
package chat.rocket.android.service.ddp.stream;
import android.content.Context;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.model.ddp.Message;
import chat.rocket.android.realm_helper.RealmHelper;
import io.realm.RealmObject;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* stream-room-message subscriber.
*/
public class StreamRoomMessage extends AbstractStreamNotifyEventSubscriber {
private String roomId;
public StreamRoomMessage(Context context, RealmHelper realmHelper, DDPClientWraper ddpClient,
String roomId) {
super(context, realmHelper, ddpClient);
this.roomId = roomId;
}
@Override protected String getSubscriptionName() {
return "stream-room-messages";
}
@Override protected JSONArray getSubscriptionParams() throws JSONException {
return new JSONArray()
.put(roomId)
.put(false);
}
@Override protected Class<? extends RealmObject> getModelClass() {
return Message.class;
}
@Override protected String getPrimaryKeyForModel() {
return "_id";
}
@Override protected JSONObject customizeFieldJson(JSONObject json) throws JSONException {
return Message.customizeJson(super.customizeFieldJson(json));
}
}
package chat.rocket.android.service.internal;
import android.content.Context;
import android.content.SharedPreferences;
import chat.rocket.android.RocketChatCache;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ddp.RoomSubscription;
import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.service.Registerable;
public abstract class AbstractRocketChatCacheObserver implements Registerable {
private final Context context;
private final RealmHelper realmHelper;
private String roomId;
protected AbstractRocketChatCacheObserver(Context context, RealmHelper realmHelper) {
this.context = context;
this.realmHelper = realmHelper;
}
private void updateRoomIdWith(SharedPreferences prefs) {
String roomId = prefs.getString(RocketChatCache.KEY_SELECTED_ROOM_ID, null);
if (!TextUtils.isEmpty(roomId)) {
RoomSubscription room = realmHelper.executeTransactionForRead(realm ->
realm.where(RoomSubscription.class).equalTo("rid", roomId).findFirst());
if (room != null) {
if (this.roomId == null || !this.roomId.equals(roomId)) {
this.roomId = roomId;
onRoomIdUpdated(roomId);
}
return;
}
}
if (this.roomId != null) {
this.roomId = null;
onRoomIdUpdated(null);
}
}
protected abstract void onRoomIdUpdated(String roomId);
private SharedPreferences.OnSharedPreferenceChangeListener listener =
(prefs, key) -> {
if (RocketChatCache.KEY_SELECTED_ROOM_ID.equals(key)) {
updateRoomIdWith(prefs);
}
};
@Override public final void register() {
SharedPreferences prefs = RocketChatCache.get(context);
prefs.registerOnSharedPreferenceChangeListener(listener);
updateRoomIdWith(prefs);
}
@Override public final void unregister() {
RocketChatCache.get(context).unregisterOnSharedPreferenceChangeListener(listener);
}
}
package chat.rocket.android.service.internal;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.service.Registerable;
import chat.rocket.android.service.ddp.stream.StreamRoomMessage;
/**
* wrapper for managing stream-notify-message depending on RocketChatCache.
*/
public class StreamRoomMessageManager implements Registerable {
private StreamRoomMessage streamRoomMessage;
private final Context context;
private final RealmHelper realmHelper;
private final DDPClientWraper ddpClient;
private final AbstractRocketChatCacheObserver cacheObserver;
private final Handler handler;
public StreamRoomMessageManager(Context context, RealmHelper realmHelper,
DDPClientWraper ddpClient) {
this.context = context;
this.realmHelper = realmHelper;
this.ddpClient = ddpClient;
cacheObserver = new AbstractRocketChatCacheObserver(context, realmHelper) {
@Override protected void onRoomIdUpdated(String roomId) {
unregisterStreamNotifyMessageIfNeeded();
registerStreamNotifyMessage(roomId);
}
};
handler = new Handler(Looper.myLooper());
}
private void registerStreamNotifyMessage(String roomId) {
handler.post(() -> {
streamRoomMessage = new StreamRoomMessage(context, realmHelper, ddpClient, roomId);
streamRoomMessage.register();
});
}
private void unregisterStreamNotifyMessageIfNeeded() {
handler.post(() -> {
if (streamRoomMessage != null) {
streamRoomMessage.unregister();
streamRoomMessage = null;
}
});
}
@Override public void register() {
cacheObserver.register();
}
@Override public void unregister() {
unregisterStreamNotifyMessageIfNeeded();
cacheObserver.unregister();
}
}
...@@ -27,10 +27,6 @@ abstract class AbstractModelObserver<T extends RealmObject> ...@@ -27,10 +27,6 @@ abstract class AbstractModelObserver<T extends RealmObject>
observer.sub(); observer.sub();
} }
@Override public void keepalive() {
observer.keepalive();
}
@Override public void unregister() { @Override public void unregister() {
observer.unsub(); observer.unsub();
} }
......
package chat.rocket.android.service.observer;
import android.content.Context;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.model.ddp.User;
import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.service.Registerable;
import chat.rocket.android.service.ddp.stream.StreamNotifyUserSubscriptionsChanged;
import hugo.weaving.DebugLog;
import io.realm.Realm;
import io.realm.RealmResults;
import java.util.ArrayList;
import java.util.List;
/**
* observe the user with emails.
*/
public class CurrentUserObserver extends AbstractModelObserver<User> {
private boolean currentUserExists;
private final MethodCallHelper methodCall;
private ArrayList<Registerable> listeners;
public CurrentUserObserver(Context context, RealmHelper realmHelper,
DDPClientWraper ddpClient) {
super(context, realmHelper, ddpClient);
methodCall = new MethodCallHelper(realmHelper, ddpClient);
currentUserExists = false;
}
@Override public RealmResults<User> queryItems(Realm realm) {
return User.queryCurrentUser(realm).findAll();
}
@Override public void onUpdateResults(List<User> results) {
boolean exists = !results.isEmpty();
if (currentUserExists != exists) {
if (exists) {
onLogin(results.get(0));
} else {
onLogout();
}
currentUserExists = exists;
}
}
@DebugLog
private void onLogin(User user) {
if (listeners != null) {
onLogout();
}
listeners = new ArrayList<>();
final String userId = user.get_id();
// get and observe Room subscriptions.
methodCall.getRoomSubscriptions().onSuccess(task -> {
Registerable listener = new StreamNotifyUserSubscriptionsChanged(
context, realmHelper, ddpClient, userId);
listener.register();
listeners.add(listener);
return null;
});
}
@DebugLog
private void onLogout() {
if (listeners != null) {
for (Registerable listener : listeners) {
listener.unregister();
}
}
listeners = null;
}
}
package chat.rocket.android.service.observer;
import android.content.Context;
import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.model.SyncState;
import chat.rocket.android.model.ddp.Message;
import chat.rocket.android.realm_helper.RealmHelper;
import io.realm.Realm;
import io.realm.RealmResults;
import java.util.List;
import org.json.JSONObject;
import timber.log.Timber;
/**
* Observe messages for sending.
*/
public class NewMessageObserver extends AbstractModelObserver<Message> {
private final MethodCallHelper methodCall;
public NewMessageObserver(Context context, RealmHelper realmHelper,
DDPClientWraper ddpClient) {
super(context, realmHelper, ddpClient);
methodCall = new MethodCallHelper(realmHelper, ddpClient);
realmHelper.executeTransaction(realm -> {
// resume pending operations.
RealmResults<Message> pendingMethodCalls = realm.where(Message.class)
.equalTo("syncstate", SyncState.SYNCING)
.findAll();
for (Message message : pendingMethodCalls) {
message.setSyncstate(SyncState.NOT_SYNCED);
}
return null;
}).continueWith(new LogcatIfError());
}
@Override public RealmResults<Message> queryItems(Realm realm) {
return realm.where(Message.class)
.equalTo("syncstate", SyncState.NOT_SYNCED)
.isNotNull("rid")
.findAll();
}
@Override public void onUpdateResults(List<Message> results) {
if (results.isEmpty()) {
return;
}
Message message = results.get(0);
final String messageId = message.get_id();
final String roomId = message.getRid();
final String msg = message.getMsg();
realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(Message.class, new JSONObject()
.put("_id", messageId)
.put("syncstate", SyncState.SYNCING)
)
).onSuccessTask(task ->
methodCall.sendMessage(messageId, roomId, msg).onSuccessTask(_task -> {
JSONObject messageJson = _task.getResult();
messageJson.put("syncstate", SyncState.SYNCED);
return realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(Message.class, messageJson));
})
).continueWith(task -> {
if (task.isFaulted()) {
Timber.w(task.getError());
realmHelper.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(Message.class, new JSONObject()
.put("_id", messageId)
.put("syncstate", SyncState.FAILED)));
}
return null;
});
}
}
...@@ -2,12 +2,13 @@ package chat.rocket.android.service.observer; ...@@ -2,12 +2,13 @@ package chat.rocket.android.service.observer;
import android.content.Context; import android.content.Context;
import chat.rocket.android.api.DDPClientWraper; import chat.rocket.android.api.DDPClientWraper;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.helper.LogcatIfError; import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.model.internal.GetUsersOfRoomsProcedure;
import chat.rocket.android.model.internal.LoadMessageProcedure; import chat.rocket.android.model.internal.LoadMessageProcedure;
import chat.rocket.android.model.internal.MethodCall; import chat.rocket.android.model.internal.MethodCall;
import chat.rocket.android.model.internal.Session; import chat.rocket.android.model.internal.Session;
import chat.rocket.android.realm_helper.RealmHelper; import chat.rocket.android.realm_helper.RealmHelper;
import chat.rocket.android.service.internal.StreamRoomMessageManager;
import hugo.weaving.DebugLog; import hugo.weaving.DebugLog;
import io.realm.Realm; import io.realm.Realm;
import io.realm.RealmResults; import io.realm.RealmResults;
...@@ -17,16 +18,18 @@ import java.util.List; ...@@ -17,16 +18,18 @@ import java.util.List;
* Observes user is logged into server. * Observes user is logged into server.
*/ */
public class SessionObserver extends AbstractModelObserver<Session> { public class SessionObserver extends AbstractModelObserver<Session> {
private final MethodCallHelper methodCall;
private int count; private int count;
private final StreamRoomMessageManager streamNotifyMessage;
/** /**
* constructor. * constructor.
*/ */
public SessionObserver(Context context, RealmHelper realmHelper, DDPClientWraper ddpClient) { public SessionObserver(Context context, RealmHelper realmHelper, DDPClientWraper ddpClient) {
super(context, realmHelper, ddpClient); super(context, realmHelper, ddpClient);
methodCall = new MethodCallHelper(realmHelper, ddpClient);
count = 0; count = 0;
streamNotifyMessage = new StreamRoomMessageManager(context, realmHelper, ddpClient);
} }
@Override public RealmResults<Session> queryItems(Realm realm) { @Override public RealmResults<Session> queryItems(Realm realm) {
...@@ -57,15 +60,17 @@ public class SessionObserver extends AbstractModelObserver<Session> { ...@@ -57,15 +60,17 @@ public class SessionObserver extends AbstractModelObserver<Session> {
} }
@DebugLog private void onLogin() { @DebugLog private void onLogin() {
methodCall.getRooms().continueWith(new LogcatIfError()); streamNotifyMessage.register();
} }
@DebugLog private void onLogout() { @DebugLog private void onLogout() {
streamNotifyMessage.unregister();
realmHelper.executeTransaction(realm -> { realmHelper.executeTransaction(realm -> {
// remove all tables. ONLY INTERNAL TABLES!. // remove all tables. ONLY INTERNAL TABLES!.
realm.delete(MethodCall.class); realm.delete(MethodCall.class);
realm.delete(LoadMessageProcedure.class); realm.delete(LoadMessageProcedure.class);
realm.delete(GetUsersOfRoomsProcedure.class);
return null; return null;
}).continueWith(new LogcatIfError()); }).continueWith(new LogcatIfError());
} }
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>
<vector android:alpha="0.78" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>
...@@ -2,28 +2,24 @@ ...@@ -2,28 +2,24 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
>
<include layout="@layout/sidebar"/> <include layout="@layout/sidebar" />
<android.support.design.widget.CoordinatorLayout <android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
>
<android.support.design.widget.AppBarLayout <android.support.design.widget.AppBarLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
>
<android.support.v7.widget.Toolbar <android.support.v7.widget.Toolbar
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="@string/app_name" app:title="@string/app_name" />
/>
</android.support.design.widget.AppBarLayout> </android.support.design.widget.AppBarLayout>
<FrameLayout <FrameLayout
...@@ -31,8 +27,7 @@ ...@@ -31,8 +27,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/white" android:background="@color/white"
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior" />
></FrameLayout>
</android.support.design.widget.CoordinatorLayout> </android.support.design.widget.CoordinatorLayout>
</LinearLayout> </LinearLayout>
\ No newline at end of file
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal"> android:orientation="horizontal">
<include layout="@layout/fragment_room_main"/> <include layout="@layout/fragment_room_main" />
<include layout="@layout/room_side_menu"/> <include layout="@layout/room_side_menu" />
</LinearLayout> </LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.SlidingPaneLayout <android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sliding_pane"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/sliding_pane"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
>
<include layout="@layout/sidebar"/> <include layout="@layout/sidebar" />
<android.support.design.widget.CoordinatorLayout <android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
>
<android.support.design.widget.AppBarLayout <android.support.design.widget.AppBarLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
>
<android.support.v7.widget.Toolbar <android.support.v7.widget.Toolbar
android:id="@+id/activity_main_toolbar" android:id="@+id/activity_main_toolbar"
...@@ -25,8 +21,7 @@ ...@@ -25,8 +21,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="@string/app_name" app:title="@string/app_name" />
/>
</android.support.design.widget.AppBarLayout> </android.support.design.widget.AppBarLayout>
<FrameLayout <FrameLayout
...@@ -35,8 +30,7 @@ ...@@ -35,8 +30,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/white" android:background="@color/white"
android:clickable="true" android:clickable="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior" />
></FrameLayout>
</android.support.design.widget.CoordinatorLayout> </android.support.design.widget.CoordinatorLayout>
</android.support.v4.widget.SlidingPaneLayout> </android.support.v4.widget.SlidingPaneLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:minWidth="288dp" android:minWidth="288dp"
android:padding="@dimen/margin_24" android:orientation="vertical"
> android:padding="@dimen/margin_24">
<android.support.design.widget.TextInputLayout <android.support.design.widget.TextInputLayout
android:id="@+id/text_input_email" android:id="@+id/text_input_email"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
>
<android.support.design.widget.TextInputEditText <android.support.design.widget.TextInputEditText
android:id="@+id/editor_email" android:id="@+id/editor_email"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="email address" android:hint="@string/dialog_user_registration_email"
android:imeOptions="actionNext" android:imeOptions="actionNext"
android:inputType="textWebEmailAddress" android:inputType="textWebEmailAddress"
android:singleLine="true" android:singleLine="true" />
/>
</android.support.design.widget.TextInputLayout> </android.support.design.widget.TextInputLayout>
<Space <Space
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/margin_8" android:layout_height="@dimen/margin_8" />
/>
<android.support.design.widget.TextInputLayout <android.support.design.widget.TextInputLayout
android:id="@+id/text_input_username" android:id="@+id/text_input_username"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
>
<android.support.design.widget.TextInputEditText <android.support.design.widget.TextInputEditText
android:id="@+id/editor_username" android:id="@+id/editor_username"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="username" android:hint="@string/dialog_user_registration_username"
android:imeOptions="actionNext" android:imeOptions="actionNext"
android:inputType="textWebEmailAddress" android:inputType="textWebEmailAddress"
android:singleLine="true" android:singleLine="true" />
/>
</android.support.design.widget.TextInputLayout> </android.support.design.widget.TextInputLayout>
<Space <Space
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/margin_8" android:layout_height="@dimen/margin_8" />
/>
<android.support.design.widget.TextInputLayout <android.support.design.widget.TextInputLayout
android:id="@+id/text_input_passwd" android:id="@+id/text_input_passwd"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:passwordToggleEnabled="true" app:passwordToggleEnabled="true">
>
<android.support.design.widget.TextInputEditText <android.support.design.widget.TextInputEditText
android:id="@+id/editor_passwd" android:id="@+id/editor_passwd"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="password" android:hint="@string/dialog_user_registration_password"
android:imeOptions="actionNext" android:imeOptions="actionNext"
android:inputType="textWebPassword" android:inputType="textWebPassword"
android:singleLine="true" android:singleLine="true" />
/>
</android.support.design.widget.TextInputLayout> </android.support.design.widget.TextInputLayout>
<Space <Space
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/margin_16" android:layout_height="@dimen/margin_16" />
/>
<android.support.design.widget.FloatingActionButton <android.support.design.widget.FloatingActionButton
android:id="@+id/btn_register_user" android:id="@+id/btn_register_user"
...@@ -82,16 +72,14 @@ ...@@ -82,16 +72,14 @@
android:layout_gravity="end|bottom" android:layout_gravity="end|bottom"
app:elevation="2dp" app:elevation="2dp"
app:fabSize="mini" app:fabSize="mini"
app:srcCompat="@drawable/ic_arrow_forward_white_24dp" app:srcCompat="@drawable/ic_arrow_forward_white_24dp" />
/>
<chat.rocket.android.widget.WaitingView <chat.rocket.android.widget.WaitingView
android:id="@+id/waiting" android:id="@+id/waiting"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:dotCount="5"
app:dotSize="12dp"
android:layout_gravity="center" android:layout_gravity="center"
/> app:dotCount="5"
app:dotSize="12dp" />
</LinearLayout> </LinearLayout>
\ No newline at end of file
...@@ -6,34 +6,35 @@ ...@@ -6,34 +6,35 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical">
>
<FrameLayout <FrameLayout
android:id="@+id/room_user_titlebar" android:id="@+id/room_user_titlebar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:paddingStart="@dimen/margin_16"
android:paddingRight="@dimen/margin_16" android:paddingRight="@dimen/margin_16"
> android:paddingStart="@dimen/margin_16">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:text="@string/users_of_room_title" android:text="@string/users_of_room_title"
android:textAppearance="@style/TextAppearance.AppCompat.Title" android:textAppearance="@style/TextAppearance.AppCompat.Title" />
android:layout_gravity="start|center_vertical"/>
<TextView <TextView
android:id="@+id/room_user_count" android:id="@+id/room_user_count"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:layout_gravity="end|center_vertical"
android:layout_gravity="end|center_vertical"/> android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</FrameLayout> </FrameLayout>
<android.support.v7.widget.RecyclerView <android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview" android:id="@+id/recyclerview"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"
android:orientation="vertical" />
</LinearLayout> </LinearLayout>
...@@ -41,7 +42,6 @@ ...@@ -41,7 +42,6 @@
android:id="@+id/waiting" android:id="@+id/waiting"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center" />
/>
</FrameLayout> </FrameLayout>
\ No newline at end of file
...@@ -2,26 +2,26 @@ ...@@ -2,26 +2,26 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"
android:minWidth="288dp"
android:padding="@dimen/margin_24"
android:gravity="center" android:gravity="center"
> android:minWidth="288dp"
android:orientation="vertical"
android:padding="@dimen/margin_24">
<ImageView <ImageView
android:layout_width="80dp" android:layout_width="80dp"
android:layout_height="80dp" android:layout_height="80dp"
android:layout_marginBottom="@dimen/margin_24" android:layout_marginBottom="@dimen/margin_24"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@mipmap/ic_launcher"/> android:src="@mipmap/ic_launcher" />
<chat.rocket.android.widget.WaitingView <chat.rocket.android.widget.WaitingView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
/>
<TextView <TextView
android:id="@+id/txt_caption" android:id="@+id/txt_caption"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" android:layout_marginTop="@dimen/margin_16"
android:layout_marginTop="@dimen/margin_16"/> android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout> </LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:theme="@style/Theme.AppCompat.Light" android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_16" android:padding="@dimen/margin_16"
android:gravity="center"> android:theme="@style/Theme.AppCompat.Light">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Welcome to Rocket.Chat.Android\nSelect a channel from the drawer." android:layout_marginBottom="@dimen/margin_16"
android:textSize="14sp"
android:gravity="center" android:gravity="center"
android:layout_marginBottom="@dimen/margin_16"/> android:text="@string/fragment_home_welcome_message"
android:textSize="14sp" />
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/> android:src="@mipmap/ic_launcher" />
</LinearLayout> </LinearLayout>
\ No newline at end of file
...@@ -3,8 +3,7 @@ ...@@ -3,8 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorPrimaryDark" android:background="?attr/colorPrimaryDark">
>
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
...@@ -13,38 +12,33 @@ ...@@ -13,38 +12,33 @@
android:background="@color/white" android:background="@color/white"
android:minWidth="288dp" android:minWidth="288dp"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/margin_24" android:padding="@dimen/margin_24">
>
<LinearLayout <LinearLayout
android:layout_width="0px" android:layout_width="0px"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:orientation="vertical" android:orientation="vertical">
>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="hostname" android:text="@string/fragment_input_hostname_hostname"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
/>
<EditText <EditText
android:id="@+id/editor_hostname" android:id="@+id/editor_hostname"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="demo.rocket.chat" android:hint="@string/fragment_input_hostname_server_hint"
android:imeOptions="actionGo" android:imeOptions="actionGo"
android:inputType="textWebEditText" android:inputType="textWebEditText"
android:singleLine="true" android:singleLine="true" />
/>
</LinearLayout> </LinearLayout>
<Space <Space
android:layout_width="@dimen/margin_8" android:layout_width="@dimen/margin_8"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
/>
<android.support.design.widget.FloatingActionButton <android.support.design.widget.FloatingActionButton
android:id="@+id/btn_connect" android:id="@+id/btn_connect"
...@@ -53,7 +47,6 @@ ...@@ -53,7 +47,6 @@
android:layout_gravity="end|bottom" android:layout_gravity="end|bottom"
app:elevation="2dp" app:elevation="2dp"
app:fabSize="mini" app:fabSize="mini"
app:srcCompat="@drawable/ic_arrow_forward_white_24dp" app:srcCompat="@drawable/ic_arrow_forward_white_24dp" />
/>
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
\ No newline at end of file
...@@ -3,8 +3,7 @@ ...@@ -3,8 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorPrimaryDark" android:background="?attr/colorPrimaryDark">
>
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
...@@ -13,94 +12,100 @@ ...@@ -13,94 +12,100 @@
android:background="@color/white" android:background="@color/white"
android:minWidth="288dp" android:minWidth="288dp"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/margin_24" android:padding="@dimen/margin_24">
>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal">
>
<chat.rocket.android.widget.FontAwesomeButton <chat.rocket.android.widget.FontAwesomeButton
android:id="@+id/btn_login_with_twitter" android:id="@+id/btn_login_with_twitter"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginEnd="@dimen/margin_8"
android:text="@string/fa_twitter" android:text="@string/fa_twitter"
android:textSize="16dp" android:textSize="16dp" />
<chat.rocket.android.widget.FontAwesomeButton
android:id="@+id/btn_login_with_facebook"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="@dimen/margin_8" android:layout_marginEnd="@dimen/margin_8"
android:enabled="false" android:text="@string/fa_facebook_official"
/> android:textSize="16dp" />
<chat.rocket.android.widget.FontAwesomeButton <chat.rocket.android.widget.FontAwesomeButton
android:id="@+id/btn_login_with_github" android:id="@+id/btn_login_with_github"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginEnd="@dimen/margin_8"
android:text="@string/fa_github" android:text="@string/fa_github"
android:textSize="16dp" android:textSize="16dp" />
<chat.rocket.android.widget.FontAwesomeButton
android:id="@+id/btn_login_with_google"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="@dimen/margin_8" android:layout_marginEnd="@dimen/margin_8"
/> android:text="@string/fa_google"
android:textSize="16dp" />
</LinearLayout> </LinearLayout>
<android.support.design.widget.TextInputLayout <android.support.design.widget.TextInputLayout
android:id="@+id/text_input_username" android:id="@+id/text_input_username"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
>
<android.support.design.widget.TextInputEditText <android.support.design.widget.TextInputEditText
android:id="@+id/editor_username" android:id="@+id/editor_username"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="username or email" android:hint="@string/fragment_login_username_or_email"
android:imeOptions="actionNext" android:imeOptions="actionNext"
android:inputType="textWebEmailAddress" android:inputType="textWebEmailAddress"
android:singleLine="true" android:singleLine="true" />
/>
</android.support.design.widget.TextInputLayout> </android.support.design.widget.TextInputLayout>
<Space <Space
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/margin_8" android:layout_height="@dimen/margin_8" />
/>
<android.support.design.widget.TextInputLayout <android.support.design.widget.TextInputLayout
android:id="@+id/text_input_passwd" android:id="@+id/text_input_passwd"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:passwordToggleEnabled="true" app:passwordToggleEnabled="true">
>
<android.support.design.widget.TextInputEditText <android.support.design.widget.TextInputEditText
android:id="@+id/editor_passwd" android:id="@+id/editor_passwd"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="password" android:hint="@string/fragment_login_password"
android:imeOptions="actionNext" android:imeOptions="actionNext"
android:inputType="textWebPassword" android:inputType="textWebPassword"
android:singleLine="true" android:singleLine="true" />
/>
</android.support.design.widget.TextInputLayout> </android.support.design.widget.TextInputLayout>
<Space <Space
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/margin_16" android:layout_height="@dimen/margin_16" />
/>
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal">
>
<android.support.design.widget.FloatingActionButton <android.support.design.widget.FloatingActionButton
android:id="@+id/btn_user_registration" android:id="@+id/btn_user_registration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start|bottom" android:layout_gravity="start|bottom"
app:backgroundTint="@color/white"
app:elevation="2dp" app:elevation="2dp"
app:fabSize="mini" app:fabSize="mini"
app:backgroundTint="@color/white" app:srcCompat="@drawable/ic_user_registration_blue_24dp" />
app:srcCompat="@drawable/ic_user_registration_blue_24dp"
/>
<android.support.design.widget.FloatingActionButton <android.support.design.widget.FloatingActionButton
android:id="@+id/btn_login_with_email" android:id="@+id/btn_login_with_email"
...@@ -109,8 +114,7 @@ ...@@ -109,8 +114,7 @@
android:layout_gravity="end|bottom" android:layout_gravity="end|bottom"
app:elevation="2dp" app:elevation="2dp"
app:fabSize="normal" app:fabSize="normal"
app:srcCompat="@drawable/ic_arrow_forward_white_24dp" app:srcCompat="@drawable/ic_arrow_forward_white_24dp" />
/>
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
\ No newline at end of file
...@@ -3,8 +3,7 @@ ...@@ -3,8 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorPrimaryDark" android:background="?attr/colorPrimaryDark">
>
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
...@@ -13,32 +12,29 @@ ...@@ -13,32 +12,29 @@
android:background="@color/white" android:background="@color/white"
android:minWidth="288dp" android:minWidth="288dp"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/margin_24" android:padding="@dimen/margin_24">
>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Title" android:text="@string/fragment_retry_login_error_title"
android:text="Oops..." android:textAppearance="@style/TextAppearance.AppCompat.Title" />
/>
<TextView <TextView
android:id="@+id/txt_error_description" android:id="@+id/txt_error_description"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
android:layout_marginTop="@dimen/margin_8"
android:layout_marginBottom="@dimen/margin_8" android:layout_marginBottom="@dimen/margin_8"
/> android:layout_marginTop="@dimen/margin_8"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical">
>
<android.support.design.widget.FloatingActionButton <android.support.design.widget.FloatingActionButton
android:id="@+id/btn_retry_login" android:id="@+id/btn_retry_login"
android:layout_width="wrap_content" android:layout_width="wrap_content"
...@@ -46,15 +42,13 @@ ...@@ -46,15 +42,13 @@
android:layout_margin="@dimen/margin_8" android:layout_margin="@dimen/margin_8"
app:elevation="2dp" app:elevation="2dp"
app:fabSize="normal" app:fabSize="normal"
app:srcCompat="@drawable/ic_arrow_forward_white_24dp" app:srcCompat="@drawable/ic_arrow_forward_white_24dp" />
/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:text="@string/fragment_retry_login_retry_title"
android:text="RETRY" android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
/>
</LinearLayout> </LinearLayout>
...@@ -62,11 +56,10 @@ ...@@ -62,11 +56,10 @@
android:id="@+id/waiting" android:id="@+id/waiting"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="@dimen/margin_16" android:layout_marginTop="@dimen/margin_16"
app:dotCount="5" app:dotCount="5"
app:dotSize="12dp" app:dotSize="12dp" />
android:layout_gravity="center"
/>
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout" android:id="@+id/drawer_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
>
<include layout="@layout/fragment_room_main"/> <include layout="@layout/fragment_room_main" />
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="end" android:layout_gravity="end"
android:clickable="true" android:clickable="true"
android:theme="@style/AppTheme.Dark" android:theme="@style/AppTheme.Dark">
>
<include layout="@layout/room_side_menu"/> <include layout="@layout/room_side_menu" />
</FrameLayout> </FrameLayout>
</android.support.v4.widget.DrawerLayout> </android.support.v4.widget.DrawerLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
> >
...@@ -11,11 +12,20 @@ ...@@ -11,11 +12,20 @@
android:layout_height="match_parent" android:layout_height="match_parent"
/> />
<chat.rocket.android.widget.message.MessageComposer
android:id="@+id/message_composer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.AppCompat.Light"
android:layout_gravity="bottom"
android:background="@android:color/white"/>
<android.support.design.widget.FloatingActionButton <android.support.design.widget.FloatingActionButton
android:id="@+id/fab_compose" android:id="@+id/fab_compose"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end|bottom" android:layout_gravity="end|bottom"
android:layout_margin="@dimen/margin_16" android:layout_margin="@dimen/margin_16"
app:srcCompat="@drawable/ic_compose_white_24dp"
/> />
</FrameLayout> </FrameLayout>
\ No newline at end of file
...@@ -3,26 +3,25 @@ ...@@ -3,26 +3,25 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorPrimaryDark" android:background="?attr/colorPrimaryDark"
android:theme="@style/AppTheme.Dark" android:theme="@style/AppTheme.Dark">
>
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center" android:gravity="center"
> android:orientation="vertical">
<chat.rocket.android.widget.WaitingView <chat.rocket.android.widget.WaitingView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
/>
<TextView <TextView
android:id="@+id/txt_caption" android:id="@+id/txt_caption"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" android:layout_marginTop="@dimen/margin_32"
android:layout_marginTop="@dimen/margin_32"/> android:textAppearance="@style/TextAppearance.AppCompat.Caption" />
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/AppTheme" android:orientation="vertical"
> android:theme="@style/AppTheme">
<include layout="@layout/list_item_message_newday"/> <include layout="@layout/list_item_message_newday" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal">
>
<ImageView <ImageView
android:id="@+id/user_avatar" android:id="@+id/user_avatar"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:layout_margin="8dp" android:layout_margin="8dp"
tools:src="@drawable/ic_default_avatar" tools:src="@drawable/ic_default_avatar" />
/>
<LinearLayout <LinearLayout
android:layout_width="0px" android:layout_width="0px"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_weight="1" android:layout_weight="1"
android:orientation="vertical" android:orientation="vertical">
android:layout_marginEnd="8dp">
<LinearLayout <LinearLayout
android:id="@+id/user_and_timestamp_container" android:id="@+id/user_and_timestamp_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
android:id="@+id/username" android:id="@+id/username"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textStyle="bold" android:textStyle="bold"
tools:text="John Doe"/> tools:text="John Doe" />
<Space <Space
android:layout_width="@dimen/margin_8" android:layout_width="@dimen/margin_8"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
/>
<TextView <TextView
android:id="@+id/timestamp" android:id="@+id/timestamp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:enabled="false" android:enabled="false"
tools:text="12:34"/> tools:text="12:34" />
</LinearLayout> </LinearLayout>
<chat.rocket.android.widget.message.RocketChatMessageLayout <chat.rocket.android.widget.message.RocketChatMessageLayout
android:id="@+id/message_body" android:id="@+id/message_body"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Space <Space xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/space" android:id="@+id/space"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="88dp" android:layout_height="88dp" />
/> \ No newline at end of file
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/AppTheme" android:orientation="vertical"
> android:theme="@style/AppTheme">
<chat.rocket.android.widget.WaitingView <chat.rocket.android.widget.WaitingView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_margin="@dimen/margin_8" android:layout_margin="@dimen/margin_8" />
/>
</FrameLayout> </FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/newday_container" android:id="@+id/newday_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:layout_margin="16dp" android:layout_margin="16dp"
> android:gravity="center_vertical"
android:orientation="horizontal">
<View <View
android:layout_width="0px" android:layout_width="0px"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@color/newday_color"/> android:background="@color/newday_color" />
<TextView <TextView
android:id="@+id/newday_text" android:id="@+id/newday_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:textColor="@color/newday_color" android:textColor="@color/newday_color"
android:textSize="8sp" android:textSize="8sp"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginLeft="16dp" tools:text="2016/01/23" />
android:layout_marginRight="16dp"
tools:text="2016/01/23"/>
<View <View
android:layout_width="0px" android:layout_width="0px"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@color/newday_color"/> android:background="@color/newday_color" />
</LinearLayout> </LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/AppTheme" android:orientation="vertical"
> android:theme="@style/AppTheme">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/margin_16" android:layout_margin="@dimen/margin_16"
android:text="@string/start_of_conversation" android:text="@string/start_of_conversation"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
android:layout_gravity="center"
/>
</FrameLayout> </FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall" android:layout_height="?attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"> android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView <ImageView
android:id="@+id/room_user_status" android:id="@+id/room_user_status"
android:layout_width="8dp" android:layout_width="8dp"
android:layout_height="8dp" android:layout_height="8dp"
android:layout_margin="@dimen/margin_8"/> android:layout_margin="@dimen/margin_8" />
<ImageView <ImageView
android:id="@+id/room_user_avatar" android:id="@+id/room_user_avatar"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/room_side_menu" android:id="@+id/room_side_menu"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"
android:layout_gravity="end" android:layout_gravity="end"
> android:orientation="vertical">
<chat.rocket.android.widget.FontAwesomeButton <chat.rocket.android.widget.FontAwesomeButton
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:textSize="24dp"
android:text="@string/fa_search"
android:enabled="false" android:enabled="false"
/> android:text="@string/fa_search"
android:textSize="24dp" />
<chat.rocket.android.widget.FontAwesomeButton <chat.rocket.android.widget.FontAwesomeButton
android:id="@+id/btn_users" android:id="@+id/btn_users"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:textSize="24dp"
android:text="@string/fa_users" android:text="@string/fa_users"
/> android:textSize="24dp" />
<chat.rocket.android.widget.FontAwesomeButton <chat.rocket.android.widget.FontAwesomeButton
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:textSize="24dp"
android:text="@string/fa_at"
android:enabled="false" android:enabled="false"
/> android:text="@string/fa_at"
android:textSize="24dp" />
</LinearLayout> </LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.SlidingPaneLayout <android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sub_sliding_pane" android:id="@+id/sub_sliding_pane"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_gravity="start"
android:layout_width="280dp" android:layout_width="280dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:theme="@style/AppTheme.Dark" android:layout_gravity="start"
> android:theme="@style/AppTheme.Dark">
<android.support.v4.widget.NestedScrollView <android.support.v4.widget.NestedScrollView
android:layout_width="96dp" android:layout_width="96dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"
android:background="?attr/colorPrimaryDark" android:background="?attr/colorPrimaryDark">
>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical">
>
<ImageButton <ImageButton
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="80dp" android:layout_width="80dp"
android:layout_height="80dp" android:layout_height="80dp"
android:layout_margin="@dimen/margin_8" android:layout_margin="@dimen/margin_8"
android:src="@mipmap/ic_launcher" android:src="@mipmap/ic_launcher" />
style="@style/Base.Widget.AppCompat.Button.Borderless"
/>
<chat.rocket.android.widget.FontAwesomeButton <chat.rocket.android.widget.FontAwesomeButton
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="80dp" android:layout_width="80dp"
android:layout_height="80dp" android:layout_height="80dp"
android:layout_margin="@dimen/margin_8" android:layout_margin="@dimen/margin_8"
android:text="@string/fa_plus" android:text="@string/fa_plus" />
style="@style/Base.Widget.AppCompat.Button.Borderless"
/>
</LinearLayout> </LinearLayout>
</android.support.v4.widget.NestedScrollView> </android.support.v4.widget.NestedScrollView>
<FrameLayout <FrameLayout
android:id="@+id/sidebar_fragment_container" android:id="@+id/sidebar_fragment_container"
android:layout_width="280dp" android:layout_width="280dp"
android:layout_height="match_parent" android:layout_height="match_parent" />
/>
</android.support.v4.widget.SlidingPaneLayout> </android.support.v4.widget.SlidingPaneLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<WebView <WebView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/webview" android:id="@+id/webview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent" />
/> \ No newline at end of file
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="instabug_api_key">e111b2f9ac16d4eed4a3568cf6270835</string> <string name="instabug_api_key">ac4314823dbb87263c76b22db0135727</string>
</resources> </resources>
\ No newline at end of file
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
<string name="fa_chevron_down" translatable="false">&#xf078;</string> <string name="fa_chevron_down" translatable="false">&#xf078;</string>
<string name="fa_twitter" translatable="false">&#xf099;</string> <string name="fa_twitter" translatable="false">&#xf099;</string>
<string name="fa_github" translatable="false">&#xf09b;</string> <string name="fa_github" translatable="false">&#xf09b;</string>
<string name="fa_google" translatable="false">&#xf1a0;</string>
<string name="fa_facebook_official" translatable="false">&#xf230;</string>
<string name="fa_plus" translatable="false">&#xf067;</string> <string name="fa_plus" translatable="false">&#xf067;</string>
<string name="fa_sign_out" translatable="false">&#xf08b;</string> <string name="fa_sign_out" translatable="false">&#xf08b;</string>
......
...@@ -11,4 +11,21 @@ ...@@ -11,4 +11,21 @@
<string name="start_of_conversation">Start of conversation</string> <string name="start_of_conversation">Start of conversation</string>
<string name="users_of_room_title">Members List</string> <string name="users_of_room_title">Members List</string>
<string name="fmt_room_user_count">Total: %,d users</string> <string name="fmt_room_user_count">Total: %,d users</string>
<string name="sending">Sending…</string>
<string name="resend">Resend</string>
<string name="discard">Discard</string>
<string name="dialog_user_registration_email">Email</string>
<string name="dialog_user_registration_username">Username</string>
<string name="dialog_user_registration_password">Password</string>
<string name="fragment_home_welcome_message">Welcome to Rocket.Chat.Android\nSelect a channel from the drawer.</string>
<string name="fragment_input_hostname_hostname">Hostname</string>
<string name="fragment_input_hostname_server_hint">demo.rocket.chat</string>
<string name="fragment_login_username_or_email">Username or email</string>
<string name="fragment_login_password">Password</string>
<string name="fragment_retry_login_retry_title">RETRY</string>
<string name="fragment_retry_login_error_title">Oops…</string>
<string name="add_server_activity_waiting_server">Connecting to server…</string>
<string name="server_config_activity_authenticating">Authenticating…</string>
<string name="home_fragment_title">Rocket.Chat - Home</string>
</resources> </resources>
ext { ext {
androidPlugin = 'com.android.tools.build:gradle:2.2.3' androidPlugin = 'com.android.tools.build:gradle:2.2.3'
realmPlugin = 'io.realm:realm-gradle-plugin:2.2.1' realmPlugin = 'io.realm:realm-gradle-plugin:2.2.1'
retroLambdaPlugin = 'me.tatarka:gradle-retrolambda:3.3.1'
retroLambdaPatch = 'me.tatarka.retrolambda.projectlombok:lombok.ast:0.2.3.a2'
compileSdkVersion = 25 compileSdkVersion = 25
buildToolsVersion = '25.0.1' buildToolsVersion = '25.0.1'
minSdkVersion = 21 //for accelerating multi-dex build. OVERRIDEN BY Circle CI to 17 minSdkVersion = 21 //for accelerating multi-dex build. OVERRIDEN BY Circle CI to 17
......
...@@ -4,7 +4,6 @@ import io.realm.Realm; ...@@ -4,7 +4,6 @@ import io.realm.Realm;
import io.realm.RealmChangeListener; import io.realm.RealmChangeListener;
import io.realm.RealmObject; import io.realm.RealmObject;
import io.realm.RealmResults; import io.realm.RealmResults;
import timber.log.Timber;
abstract class AbstractRealmResultsObserver<T extends RealmObject> { abstract class AbstractRealmResultsObserver<T extends RealmObject> {
protected Realm realm; protected Realm realm;
...@@ -31,26 +30,16 @@ abstract class AbstractRealmResultsObserver<T extends RealmObject> { ...@@ -31,26 +30,16 @@ abstract class AbstractRealmResultsObserver<T extends RealmObject> {
results.addChangeListener(listener); results.addChangeListener(listener);
} }
public void keepalive() {
if (realm == null || realm.isClosed() || !results.isValid()) {
unsub();
sub();
}
}
public void unsub() { public void unsub() {
try { if (realm != null) {
if (results != null) { if (results != null) {
if (results.isValid()) { if (results.isValid()) {
results.removeChangeListener(listener); results.removeChangeListener(listener);
} }
results = null; results = null;
} }
if (realm != null && !realm.isClosed()) {
realm.close(); realm.close();
} realm = null;
} catch (IllegalStateException exception) {
Timber.w(exception);
} }
} }
......
...@@ -11,6 +11,7 @@ import io.realm.RealmObject; ...@@ -11,6 +11,7 @@ import io.realm.RealmObject;
import io.realm.RealmResults; import io.realm.RealmResults;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.json.JSONException;
import timber.log.Timber; import timber.log.Timber;
public class RealmHelper { public class RealmHelper {
...@@ -34,12 +35,9 @@ public class RealmHelper { ...@@ -34,12 +35,9 @@ public class RealmHelper {
return Collections.emptyList(); return Collections.emptyList();
} }
Realm realm = instance(); try (Realm realm = instance()) {
List<E> list = realm.copyFromRealm(objects); return realm.copyFromRealm(objects);
if (!realm.isClosed()) {
realm.close();
} }
return list;
} }
public <E extends RealmObject> E copyFromRealm(E object) { public <E extends RealmObject> E copyFromRealm(E object) {
...@@ -47,56 +45,33 @@ public class RealmHelper { ...@@ -47,56 +45,33 @@ public class RealmHelper {
return null; return null;
} }
Realm realm = instance(); try (Realm realm = instance()) {
E element = realm.copyFromRealm(object); return realm.copyFromRealm(object);
if (!realm.isClosed()) {
realm.close();
} }
return element;
} }
public interface Transaction<T> { public interface Transaction<T> {
T execute(Realm realm) throws Exception; T execute(Realm realm) throws JSONException;
} }
public <T extends RealmObject> T executeTransactionForRead(Transaction<T> transaction) { public <T extends RealmObject> T executeTransactionForRead(Transaction<T> transaction) {
Realm realm = instance(); try (Realm realm = instance()) {
T object;
try {
T source = transaction.execute(realm); T source = transaction.execute(realm);
object = source != null ? realm.copyFromRealm(source) : null; return source != null ? realm.copyFromRealm(source) : null;
} catch (Exception exception) { } catch (Exception exception) {
Timber.w(exception); Timber.w(exception, "failed to execute copyFromRealm");
object = null; return null;
} finally {
if (!realm.isClosed()) {
realm.close();
}
} }
return object;
} }
public <T extends RealmObject> List<T> executeTransactionForReadResults( public <T extends RealmObject> List<T> executeTransactionForReadResults(
Transaction<RealmResults<T>> transaction) { Transaction<RealmResults<T>> transaction) {
Realm realm = instance(); try (Realm realm = instance()) {
return realm.copyFromRealm(transaction.execute(realm));
List<T> object;
try {
object = realm.copyFromRealm(transaction.execute(realm));
} catch (Exception exception) { } catch (Exception exception) {
Timber.w(exception); Timber.w(exception, "failed to execute copyFromRealm");
object = null; return Collections.emptyList();
} finally {
if (!realm.isClosed()) {
realm.close();
}
} }
return object;
} }
public Task<Void> executeTransaction(final RealmHelper.Transaction transaction) { public Task<Void> executeTransaction(final RealmHelper.Transaction transaction) {
...@@ -107,19 +82,19 @@ public class RealmHelper { ...@@ -107,19 +82,19 @@ public class RealmHelper {
private Task<Void> executeTransactionSync(final RealmHelper.Transaction transaction) { private Task<Void> executeTransactionSync(final RealmHelper.Transaction transaction) {
final TaskCompletionSource<Void> task = new TaskCompletionSource<>(); final TaskCompletionSource<Void> task = new TaskCompletionSource<>();
final Realm realm = instance(); try (Realm realm = instance()) {
realm.executeTransaction(new Realm.Transaction() { realm.executeTransaction(new Realm.Transaction() {
@Override public void execute(Realm realm) { @Override public void execute(Realm realm) {
try { try {
transaction.execute(realm); transaction.execute(realm);
task.setResult(null); } catch (JSONException exception) {
} catch (Exception exception) { throw new RuntimeException(exception);
task.setError(exception);
} }
} }
}); });
if (!realm.isClosed()) { task.setResult(null);
realm.close(); } catch (Exception exception) {
task.setError(exception);
} }
return task.getTask(); return task.getTask();
...@@ -133,27 +108,22 @@ public class RealmHelper { ...@@ -133,27 +108,22 @@ public class RealmHelper {
@Override public void execute(Realm realm) { @Override public void execute(Realm realm) {
try { try {
transaction.execute(realm); transaction.execute(realm);
} catch (Exception exception) { } catch (JSONException exception) {
task.setError(exception); throw new RuntimeException(exception);
if (!realm.isClosed()) {
realm.close();
}
} }
} }
}, new Realm.Transaction.OnSuccess() { }, new Realm.Transaction.OnSuccess() {
@Override public void onSuccess() { @Override public void onSuccess() {
if (task.trySetResult(null)) {
if (realm != null && !realm.isClosed()) {
realm.close(); realm.close();
} task.setResult(null);
}
} }
}, new Realm.Transaction.OnError() { }, new Realm.Transaction.OnError() {
@Override public void onError(Throwable error) { @Override public void onError(Throwable error) {
if (task.trySetError(new Exception(error))) {
if (!realm.isClosed()) {
realm.close(); realm.close();
} if (error instanceof Exception) {
task.setError((Exception) error);
} else {
task.setError(new Exception(error));
} }
} }
}); });
......
...@@ -16,9 +16,14 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM, ...@@ -16,9 +16,14 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM,
RealmModelListAdapter<T, VM, VH> getNewInstance(Context context); RealmModelListAdapter<T, VM, VH> getNewInstance(Context context);
} }
public interface OnItemClickListener<VM> {
void onItemClick(VM model);
}
protected final LayoutInflater inflater; protected final LayoutInflater inflater;
private RealmListObserver<T> realmListObserver; private RealmListObserver<T> realmListObserver;
private List<VM> adapterData; private List<VM> adapterData;
private OnItemClickListener<VM> onItemClickListener;
protected RealmModelListAdapter(Context context) { protected RealmModelListAdapter(Context context) {
this.inflater = LayoutInflater.from(context); this.inflater = LayoutInflater.from(context);
...@@ -64,7 +69,17 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM, ...@@ -64,7 +69,17 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM,
} }
@Override public void onBindViewHolder(VH holder, int position) { @Override public void onBindViewHolder(VH holder, int position) {
holder.bind(getItem(position)); VM model = getItem(position);
holder.itemView.setTag(model);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
VM model2 = (VM) (view.getTag());
if (model2 != null && onItemClickListener != null) {
onItemClickListener.onItemClick(model2);
}
}
});
holder.bind(model);
} }
@Override public int getItemCount() { @Override public int getItemCount() {
...@@ -85,4 +100,8 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM, ...@@ -85,4 +100,8 @@ public abstract class RealmModelListAdapter<T extends RealmObject, VM,
notifyDataSetChanged(); notifyDataSetChanged();
} }
} }
public void setOnItemClickListener(OnItemClickListener<VM> onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
} }
...@@ -18,6 +18,8 @@ android { ...@@ -18,6 +18,8 @@ android {
targetSdkVersion rootProject.ext.compileSdkVersion targetSdkVersion rootProject.ext.compileSdkVersion
versionCode 1 versionCode 1
versionName "1" versionName "1"
vectorDrawables.useSupportLibrary = true
} }
buildTypes { buildTypes {
release { release {
......
package chat.rocket.android.widget.message;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.LinearLayout;
import android.widget.TextView;
import chat.rocket.android.widget.R;
public class MessageComposer extends LinearLayout {
protected ActionListener actionListener;
protected ViewGroup composer;
public MessageComposer(Context context) {
super(context);
init();
}
public MessageComposer(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MessageComposer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public MessageComposer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public final void setOnActionListener(@Nullable ActionListener listener) {
actionListener = listener;
}
private void init() {
composer = (ViewGroup) LayoutInflater.from(getContext())
.inflate(R.layout.message_composer, this, false);
composer.findViewById(R.id.btn_submit).setOnClickListener(new OnClickListener() {
@Override public void onClick(View view) {
String messageText = getText();
if (messageText.length() > 0) {
if (actionListener != null) {
actionListener.onSubmit(messageText);
}
}
}
});
addView(composer);
}
private TextView getEditor() {
return (TextView) composer.findViewById(R.id.editor);
}
public final String getText() {
return getEditor().getText().toString().trim();
}
public final void setText(CharSequence text) {
getEditor().setText(text);
}
public void setEnabled(boolean enabled) {
getEditor().setEnabled(enabled);
composer.findViewById(R.id.btn_submit).setEnabled(enabled);
}
protected final void focusToEditor() {
final TextView editor = getEditor();
editor.requestFocus();
InputMethodManager imm =
(InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(editor, InputMethodManager.SHOW_IMPLICIT);
}
protected final void unFocusEditor() {
InputMethodManager imm =
(InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
public void show(@Nullable Runnable callback) {
focusToEditor();
setVisibility(View.VISIBLE);
if (callback != null) {
callback.run();
}
}
public void hide(@Nullable Runnable callback) {
unFocusEditor();
setVisibility(View.GONE);
if (callback != null) {
callback.run();
}
}
public boolean isShown() {
return getVisibility() == View.VISIBLE;
}
public interface ActionListener {
void onSubmit(String message);
void onCancel();
}
}
...@@ -9,6 +9,7 @@ import android.widget.LinearLayout; ...@@ -9,6 +9,7 @@ import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import chat.rocket.android.widget.R; import chat.rocket.android.widget.R;
import chat.rocket.android.widget.helper.Linkify; import chat.rocket.android.widget.helper.Linkify;
import com.emojione.Emojione;
/** /**
*/ */
...@@ -49,7 +50,7 @@ public class RocketChatMessageLayout extends LinearLayout { ...@@ -49,7 +50,7 @@ public class RocketChatMessageLayout extends LinearLayout {
private void appendTextView(String text) { private void appendTextView(String text) {
TextView textView = (TextView) inflater.inflate(R.layout.message_body, this, false); TextView textView = (TextView) inflater.inflate(R.layout.message_body, this, false);
textView.setText(text); textView.setText(Emojione.shortnameToUnicode(text, false));
Linkify.markup(textView); Linkify.markup(textView);
InlineHightlighter.highlight(textView); InlineHightlighter.highlight(textView);
......
This source diff could not be displayed because it is too large. You can view the blob instead.
<vector android:alpha="0.87" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<chat.rocket.android.widget.DividerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<EditText
android:id="@+id/editor"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxLines="3"
/>
<ImageButton
android:id="@+id/btn_submit"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
app:srcCompat="@drawable/ic_send_black_24dp"
/>
</LinearLayout>
</LinearLayout>
include ':app', ':rocket-chat-android-widgets', ':realm-helpers' include ':app', ':android-ddp', ':rocket-chat-android-widgets', ':realm-helpers'
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