Commit f6831454 authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Merge branch 'ux' of github.com:filipedelimabrito/Rocket.Chat.Android into ux

parents fa07e7ff 81cdf381
# Java language rules
### Don't ignore exceptions
It can be tempting to write code that completely ignores an exception, such as:
```java
void setServerPort(String value) {
try {
serverPort = Integer.parseInt(value);
} catch (NumberFormatException e) { }
}
```
Do not do this. While you may think your code will never encounter this error condition or that it is not important to handle it, ignoring exceptions as above creates mines in your code for someone else to trigger some day. You must handle every Exception in your code in a principled way; the specific handling varies depending on the case.
_Anytime somebody has an empty catch clause they should have a creepy feeling. There are definitely times when it is actually the correct thing to do, but at least you have to think about it. In Java you can't escape the creepy feeling._ [-James Gosling](http://www.artima.com/intv/solid4.html)
Acceptable alternatives (in order of preference) are:
* Throw the exception up to the caller of your method.
```java
void setServerPort(String value) throws NumberFormatException {
serverPort = Integer.parseInt(value);
}
```
* Throw a new exception that's appropriate to your level of abstraction.
```java
void setServerPort(String value) throws ConfigurationException {
try {
serverPort = Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new ConfigurationException("Port " + value + " is not valid.");
}
}
```
* Handle the error gracefully and substitute an appropriate value in the catch {} block.
```java
/** Set port. If value is not a valid number, 80 is substituted. */
void setServerPort(String value) {
try {
serverPort = Integer.parseInt(value);
} catch (NumberFormatException e) {
serverPort = 80; // default port for server
}
}
```
* Catch the Exception and throw a new RuntimeException. This is dangerous, so do it only if you are positive that if this error occurs the appropriate thing to do is crash.
```java
/** Set port. If value is not a valid number, die. */
void setServerPort(String value) {
try {
serverPort = Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new RuntimeException("port " + value " is invalid, ", e);
}
}
```
* As a last resort, if you are confident that ignoring the exception is appropriate then you may ignore it, but you must also comment why with a good reason:
```java
/** If value is not a valid number, original port number is used. */
void setServerPort(String value) {
try {
serverPort = Integer.parseInt(value);
} catch (NumberFormatException e) {
// Method is documented to just ignore invalid user input.
// serverPort will just be unchanged.
}
}
```
### Don't catch generic exception
Try to avoid doing this:
```java
try {
someComplicatedIOFunction(); // may throw IOException
someComplicatedParsingFunction(); // may throw ParsingException
someComplicatedSecurityFunction(); // may throw SecurityException
// phew, made it all the way
} catch (Exception e) { // I'll just catch all exceptions
handleError(); // with one generic handler!
}
```
Multiple Exceptions can mean multiple errors, it can be a network error, a parsing, etc, so ideally we should act accordingly.
See more reasons why and some alternatives [here](https://source.android.com/source/code-style.html#dont-catch-generic-exception)
### Fully qualify imports
This is bad: `import foo.*;`
This is good: `import foo.Bar;`
See more info [here](https://source.android.com/source/code-style.html#fully-qualify-imports)
# Java style rules
## Rules common to all identifiers
Identifiers use only ASCII letters and digits, and, in a small number of cases noted below, underscores. Thus each valid identifier name is matched by the regular expression `\w+` .
Special prefixes or suffixes, like those seen in the examples `name_`, `mName`, `s_name` and `kName`, are **not** used.
## Rules by identifier
### Package names
Package names are all lowercase, with consecutive words simply concatenated together (no underscores). For example, `com.example.deepspace`, **not** `com.example.deepSpace` or `com.example.deep_space`.
### Class names
Class names are written in UpperCamelCase.
Class names are typically nouns or noun phrases. For example, `Character` or `ImmutableList`. Interface names may also be nouns or noun phrases (for example, `List`), but may sometimes be adjectives or adjective phrases instead (for example, `Readable`).
There are no specific rules or even well-established conventions for naming annotation types.
Test classes are named starting with the name of the class they are testing, and ending with `Test`. For example, `HashTest` or `HashIntegrationTest`.
### Method names
Method names are written in lowerCamelCase.
Method names are typically verbs or verb phrases. For example, `sendMessage` or `stop`.
Underscores may appear in JUnit test method names to separate logical components of the name. One typical pattern is `test<MethodUnderTest>_<state>`, for example `testPop_emptyStack`. There is no One Correct Way to name test methods.
### Constant names
Constant names use CONSTANT_CASE: all uppercase letters, with words separated by underscores. But what is a constant, exactly?
Constants are static final fields whose contents are deeply immutable and whose methods have no detectable side effects. This includes primitives, Strings, immutable types, and immutable collections of immutable types. If any of the instance's observable state can change, it is not a constant. Merely intending to never mutate the object is not enough. Examples:
```java
// Constants
static final int NUMBER = 5;
static final ImmutableList<String> NAMES = ImmutableList.of("Ed", "Ann");
static final ImmutableMap<String, Integer> AGES = ImmutableMap.of("Ed", 35, "Ann", 32);
static final Joiner COMMA_JOINER = Joiner.on(','); // because Joiner is immutable
static final SomeMutableType[] EMPTY_ARRAY = {};
enum SomeEnum { ENUM_CONSTANT }
// Not constants
static String nonFinal = "non-final";
final String nonStatic = "non-static";
static final Set<String> mutableCollection = new HashSet<String>();
static final ImmutableSet<SomeMutableType> mutableElements = ImmutableSet.of(mutable);
static final ImmutableMap<String, SomeMutableType> mutableValues =
ImmutableMap.of("Ed", mutableInstance, "Ann", mutableInstance2);
static final Logger logger = Logger.getLogger(MyClass.getName());
static final String[] nonEmptyArray = {"these", "can", "change"};
```
These names are typically nouns or noun phrases.
### Non-constant field names
Non-constant field names (static or otherwise) are written in lowerCamelCase.
These names are typically nouns or noun phrases. For example, `computedValues` or `index`.
The greater the scope, more indicative must be the variable name. For example a variable with a loop scope could be called just `i`, while on function scope it could be `index`, and a class member variable would be `currentIndex`.
### Parameter names
Parameter names are written in lowerCamelCase.
One-character parameter names in public methods should be avoided.
### Local variable names
Local variable names are written in lowerCamelCase.
Even when final and immutable, local variables are not considered to be constants, and should not be styled as constants.
### Type variable names
Each type variable is named in one of two styles:
A single capital letter, optionally followed by a single numeral (such as `E`, `T`, `X`, `T2`)
A name in the form used for classes (see Section 5.2.2, Class names), followed by the capital letter T (examples: `RequestT`, `FooBarT`).
## Limit variable scope
_The scope of local variables should be kept to a minimum (Effective Java Item 29). By doing so, you increase the readability and maintainability of your code and reduce the likelihood of error. Each variable should be declared in the innermost block that encloses all uses of the variable._
_Local variables should be declared at the point they are first used. Nearly every local variable declaration should contain an initializer. If you don't yet have enough information to initialize a variable sensibly, you should postpone the declaration until you do._ - ([Android code style guidelines](https://source.android.com/source/code-style.html#limit-variable-scope))
## Order import statements
If you are using an IDE such as Android Studio, you don't have to worry about this because your IDE is already obeying these rules. If not, have a look below.
The ordering of import statements is:
1. Android imports
2. Imports from third parties (com, junit, net, org)
3. java and javax
4. Same project imports
To exactly match the IDE settings, the imports should be:
* Alphabetically ordered within each grouping, with capital letters before lower case letters (e.g. Z before a).
* There should be a blank line between each major grouping (android, com, junit, net, org, java, javax).
More info [here](https://source.android.com/source/code-style.html#limit-variable-scope)
## Class member ordering
There is no single correct solution for this but using a __logical__ and __consistent__ order will improve code learnability and readability. It is recommendable to use the following order:
1. Constants
2. Fields
3. Constructors
4. Override methods and callbacks (public or private)
5. Public methods
6. Private methods
7. Inner classes or interfaces
Example:
```java
public class MainActivity extends Activity {
private String title;
private TextView textViewTitle;
public void setTitle(String title) {
this.title = title;
}
@Override
public void onCreate() {
...
}
private void setUpView() {
...
}
static class AnInnerClass {
}
}
```
If your class is extending an __Android component__ such as an Activity or a Fragment, it is a good practice to order the override methods so that they __match the component's lifecycle__. For example, if you have an Activity that implements `onCreate()`, `onDestroy()`, `onPause()` and `onResume()`, then the correct order is:
```java
public class MainActivity extends Activity {
//Order matches Activity lifecycle
@Override
public void onCreate() {}
@Override
public void onResume() {}
@Override
public void onPause() {}
@Override
public void onDestroy() {}
}
```
## Parameter ordering in methods
When programming for Android, it is quite common to define methods that take a `Context`. If you are writing a method like this, then the __Context__ must be the __first__ parameter.
The opposite case are __callback__ interfaces that should always be the __last__ parameter.
Examples:
```java
// Context always goes first
public User loadUser(Context context, int userId);
// Callbacks always go last
public void loadUserAsync(Context context, int userId, UserCallback callback);
```
## String constants, naming, and values
Many elements of the Android SDK such as `SharedPreferences`, `Bundle`, or `Intent` use a key-value pair approach so it's very likely that even for a small app you end up having to write a lot of String constants.
When using one of these components, you __must__ define the keys as a `static final` fields and they should be prefixed as indicated below.
| Element | Field Name Prefix |
| ----------------- | ----------------- |
| SharedPreferences | `PREF_` |
| Bundle | `BUNDLE_` |
| Fragment Arguments | `ARGUMENT_` |
| Intent Extra | `EXTRA_` |
| Intent Action | `ACTION_` |
Note that the arguments of a Fragment - `Fragment.getArguments()` - are also a Bundle. However, because this is a quite common use of Bundles, we define a different prefix for them.
Example:
```java
// Note the value of the field is the same as the name to avoid duplication issues
static final String PREF_EMAIL = "PREF_EMAIL";
static final String BUNDLE_AGE = "BUNDLE_AGE";
static final String ARGUMENT_USER_ID = "ARGUMENT_USER_ID";
// Intent-related items use full package name as value
static final String EXTRA_SURNAME = "com.myapp.extras.EXTRA_SURNAME";
static final String ACTION_OPEN_USER = "com.myapp.action.ACTION_OPEN_USER";
```
# Camel case: defined
Sometimes there is more than one reasonable way to convert an English phrase into camel case, such as when acronyms or unusual constructs like "IPv6" or "iOS" are present. To improve predictability, Conding Style specifies the following (nearly) deterministic scheme.
Beginning with the prose form of the name:
1. Convert the phrase to plain ASCII and remove any apostrophes. For example, "Müller's algorithm" might become "Muellers algorithm".
2. Divide this result into words, splitting on spaces and any remaining punctuation (typically hyphens).
Recommended: if any word already has a conventional camel-case appearance in common usage, split this into its constituent parts (e.g., "AdWords" becomes "ad words"). Note that a word such as "iOS" is not really in camel case per se; it defies any convention, so this recommendation does not apply.
3. Now lowercase everything (including acronyms), then uppercase only the first character of:
... each word, to yield upper camel case, or
... each word except the first, to yield lower camel case
4. Finally, join all the words into a single identifier.
Note that the casing of the original words is almost entirely disregarded. Examples:
| Prose form | Correct | Incorrect |
| ----------------------- | ----------------- | ----------------- |
| "XML HTTP request" | `XmlHttpRequest` | `XMLHTTPRequest` |
| "new customer ID" | `newCustomerId` | `newCustomerID` |
| "inner stopwatch" | `innerStopwatch` | `innerStopWatch` |
| "supports IPv6 on iOS?" | `supportsIpv6OnIos` | `supportsIPv6OnIOS` |
| "YouTube importer" | `YouTubeImporter` `YoutubeImporter`* |
*Acceptable, but not recommended.
Note: Some words are ambiguously hyphenated in the English language: for example "nonempty" and "non-empty" are both correct, so the method names checkNonempty and checkNonEmpty are likewise both correct.
# Indentation
### Use spaces for indentation
Use __4 space__ indents for blocks:
```java
if (x == 1) {
x++;
}
```
Use __8 space__ indents for line wraps:
```java
Instrument i =
someLongExpression(that, wouldNotFit, on, one, line);
```
### Use standard brace style
Braces go on the same line as the code before them.
```java
class MyClass {
int func() {
if (something) {
// ...
} else if (somethingElse) {
// ...
} else {
// ...
}
}
}
```
Braces around the statements are **required** unless the condition and the body fit on one line.
If the condition and the body fit on one line and that line is shorter than the max line length, then braces are not required, e.g.
```java
if (condition) body();
```
This is __bad__:
```java
if (condition)
body(); // bad!
```
### Line length limit
Code lines should not exceed __100 characters__. If the line is longer than this limit there are usually two options to reduce its length:
* Extract a local variable or method (preferable).
* Apply line-wrapping to divide a single line into multiple ones.
There are two __exceptions__ where it is possible to have lines longer than 100:
* Lines that are not possible to split, e.g. long URLs in comments.
* `package` and `import` statements.
#### Line-wrapping strategies
There isn't an exact formula that explains how to line-wrap and quite often different solutions are valid. However there are a few rules that can be applied to common cases.
__Break at operators__
When the line is broken at an operator, the break comes __before__ the operator. For example:
```java
int longName = anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne
+ theFinalOne;
```
__Assignment Operator Exception__
An exception to the `break at operators` rule is the assignment operator `=`, where the line break should happen __after__ the operator.
```java
int longName =
anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne + theFinalOne;
```
__Method chain case__
When multiple methods are chained in the same line - for example when using Builders - every call to a method should go in its own line, breaking the line before the `.`
```java
Picasso.with(context).load("http://ribot.co.uk/images/sexyjoe.jpg").into(imageView);
```
```java
Picasso.with(context)
.load("http://ribot.co.uk/images/sexyjoe.jpg")
.into(imageView);
```
__Long parameters case__
When a method has many parameters or its parameters are very long, we should break the line after every comma `,`
```java
loadPicture(context, "http://ribot.co.uk/images/sexyjoe.jpg", mImageViewProfilePicture, clickListener, "Title of the picture");
```
```java
loadPicture(context,
"http://ribot.co.uk/images/sexyjoe.jpg",
mImageViewProfilePicture,
clickListener,
"Title of the picture");
```
### RxJava chains styling
Rx chains of operators require line-wrapping. Every operator must go in a new line and the line should be broken before the `.`
```java
public Observable<Location> syncLocations() {
return databaseHelper.getAllLocations()
.concatMap(new Func1<Location, Observable<? extends Location>>() {
@Override
public Observable<? extends Location> call(Location location) {
return mRetrofitService.getLocation(location.id);
}
})
.retry(new Func2<Integer, Throwable, Boolean>() {
@Override
public Boolean call(Integer numRetries, Throwable throwable) {
return throwable instanceof RetrofitError;
}
});
}
```
# Annotations
### Annotations practices
According to the [Android code style guide](https://source.android.com/source/code-style), the standard practices for some of the predefined annotations in Java are:
* `@Override`: The @Override annotation __must be used__ whenever a method overrides the declaration or implementation from a super-class. For example, if you use the @inheritdocs Javadoc tag, and derive from a class (not an interface), you must also annotate that the method @Overrides the parent class's method.
* `@SuppressWarnings`: The @SuppressWarnings annotation should only be used under circumstances where it is impossible to eliminate a warning. If a warning passes this "impossible to eliminate" test, the @SuppressWarnings annotation must be used, so as to ensure that all warnings reflect actual problems in the code.
More information about annotation guidelines can be found [here](http://source.android.com/source/code-style.html#use-standard-java-annotations).
### Annotations style
__Classes, Methods and Constructors__
When annotations are applied to a class, method, or constructor, they are listed after the documentation block and should appear as __one annotation per line__ .
```java
/* This is the documentation block about the class */
@AnnotationA
@AnnotationB
public class MyAnnotatedClass { }
```
__Fields__
Annotations applying to fields should be listed __on one line__, unless the line reaches the maximum line length, and the field declaration on the line bellow.
```java
@Nullable @Mock
DataManager dataManager;
```
#XML style rules
### Use self closing tags
When an XML element doesn't have any contents, you __must__ use self closing tags.
This is good:
```xml
<TextView
android:id="@+id/text_view_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
```
This is __bad__ :
```xml
<!-- Don\'t do this! -->
<TextView
android:id="@+id/text_view_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</TextView>
```
### Resources naming
Resource IDs and names are written in __lowercase_underscore__.
#### ID naming
IDs should be prefixed with the name of the element in lowercase underscore. For example:
| Element | Prefix |
| ----------------- | ----------------- |
| `TextView` | `text_` |
| `ImageView` | `image_` |
| `Button` | `button_` |
| `Menu` | `menu_` |
Image view example:
```xml
<ImageView
android:id="@+id/image_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
```
Menu example:
```xml
<menu>
<item
android:id="@+id/menu_done"
android:title="Done" />
</menu>
```
#### Strings
String names start with a prefix that identifies the section they belong to. For example `registration_email_hint` or `registration_name_hint`. If a string __doesn't belong__ to any section, then you should follow the rules below:
| Prefix | Description |
| ----------------- | --------------------------------------|
| `error_` | An error message |
| `msg_` | A regular information message |
| `title_` | A title, i.e. a dialog title |
| `action_` | An action such as "Save" or "Create" |
#### Styles and Themes
Unless the rest of resources, style names are written in __UpperCamelCase__.
### Attributes ordering
As a general rule you should try to group similar attributes together. A good way of ordering the most common attributes is:
1. View Id
2. Style
3. Layout width and layout height
4. Other layout attributes, sorted alphabetically
5. Remaining attributes, sorted alphabetically
# Kotlin
## Naming Style
If in doubt, default to the Java Coding Conventions such as:
- use of camelCase for names (and avoid underscore in names)
- types start with upper case
- methods and properties start with lower case
- use 4 space indentation
- public functions should have documentation such that it appears in Kotlin Doc
## Colon
There is a space before colon where colon separates type and supertype and there's no space where colon separates instance and type:
```kotlin
interface Foo<out T : Any> : Bar {
fun foo(a: Int): T
}
```
## Lambdas
In lambda expressions, spaces should be used around the curly braces, as well as around the arrow which separates the parameters from the body. Whenever possible, a lambda should be passed outside of parentheses.
```kotlin
list.filter { it > 10 }.map { element -> element * 2 }
```
In lambdas which are short and not nested, it's recommended to use the it convention instead of declaring the parameter explicitly. In nested lambdas with parameters, parameters should be always declared explicitly.
## Class header formatting
Classes with a few arguments can be written in a single line:
```kotlin
class Person(id: Int, name: String)
```
Classes with longer headers should be formatted so that each primary constructor argument is in a separate line with indentation. Also, the closing parenthesis should be on a new line. If we use inheritance, then the superclass constructor call or list of implemented interfaces should be located on the same line as the parenthesis:
```kotlin
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name) {
// ...
}
```
For multiple interfaces, the superclass constructor call should be located first and then each interface should be located in a different line:
```kotlin
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name),
KotlinMaker {
// ...
}
```
Constructor parameters can use either the regular indent or the continuation indent (double the regular indent).
## Unit
If a function returns Unit, the return type should be omitted:
```kotlin
fun foo() { // ": Unit" is omitted here
}
```
## Functions vs Properties
In some cases functions with no arguments might be interchangeable with read-only properties. Although the semantics are similar, there are some stylistic conventions on when to prefer one to another.
Prefer a property over a function when the underlying algorithm:
- does not throw
- has a O(1) complexity
- is cheap to calculate (or caсhed on the first run)
- returns the same result over invocations
# Tests style rules
## Unit tests
Test classes should match the name of the class the tests are targeting, followed by `Test`. For example, if we create a test class that contains tests for the `DatabaseHelper`, we should name it `DatabaseHelperTest`.
Test methods are annotated with `@Test` and should generally start with the name of the method that is being tested, followed by a precondition and/or expected behaviour.
* Template: `@Test void methodNamePreconditionExpectedBehaviour()`
* Example: `@Test void signInWithEmptyEmailFails()`
Precondition and/or expected behaviour may not always be required if the test is clear enough without them.
Sometimes a class may contain a large amount of methods, that at the same time require several tests for each method. In this case, it's recommendable to split up the test class into multiple ones. For example, if the `DataManager` contains a lot of methods we may want to divide it into `DataManagerSignInTest`, `DataManagerLoadUsersTest`, etc. Generally you will be able to see what tests belong together because they have common [test fixtures](https://en.wikipedia.org/wiki/Test_fixture).
### 1.4.2 Espresso tests
Every Espresso test class usually targets an Activity, therefore the name should match the name of the targeted Activity followed by `Test`, e.g. `SignInActivityTest`
When using the Espresso API it is a common practice to place chained methods in new lines.
```java
onView(withId(R.id.view))
.perform(scrollTo())
.check(matches(isDisplayed()))
```
# License
```
Copyright 2017 Rocket Chat
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
\ No newline at end of file
......@@ -21,3 +21,7 @@ echo "sdk.dir="$ANDROID_HOME > local.properties
## Bug report & Feature request
Please report via [GitHub issue](https://github.com/RocketChat/Rocket.Chat.Android/issues) :)
## Coding Style
Please follow our [coding style](https://github.com/RocketChat/Rocket.Chat.Android/blob/develop/CODING_STYLE.md) when contributing.
......@@ -3,26 +3,29 @@ package chat.rocket.android_ddp;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import io.reactivex.Flowable;
import io.reactivex.disposables.CompositeDisposable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import bolts.Task;
import bolts.TaskCompletionSource;
import chat.rocket.android.log.RCLog;
import chat.rocket.android_ddp.rx.RxWebSocket;
import chat.rocket.android_ddp.rx.RxWebSocketCallback;
import io.reactivex.Flowable;
import io.reactivex.disposables.CompositeDisposable;
import okhttp3.OkHttpClient;
public class DDPClientImpl {
private final DDPClient client;
private final RxWebSocket websocket;
private RxWebSocket websocket;
private Flowable<RxWebSocketCallback.Base> flowable;
private CompositeDisposable subscriptions;
private CompositeDisposable disposables;
private String currentSession;
public DDPClientImpl(DDPClient self, OkHttpClient client) {
websocket = new RxWebSocket(client);
......@@ -51,26 +54,24 @@ public class DDPClientImpl {
public void connect(final TaskCompletionSource<DDPClientCallback.Connect> task, final String url,
String session) {
try {
flowable = websocket.connect(url).autoConnect();
CompositeDisposable subscriptions = new CompositeDisposable();
flowable = websocket.connect(url).autoConnect(2);
CompositeDisposable disposables = new CompositeDisposable();
subscriptions.add(
flowable.filter(callback -> callback instanceof RxWebSocketCallback.Open)
disposables.add(
flowable.retry().filter(callback -> callback instanceof RxWebSocketCallback.Open)
.subscribe(
callback -> {
callback ->
sendMessage("connect",
json -> (TextUtils.isEmpty(session) ? json : json.put("session", session))
json -> (TextUtils.isEmpty(session) ? json : json.put("session", DDPClientImpl.this.currentSession))
.put(
"version", "pre2")
.put("support", new JSONArray().put("pre2").put("pre1")),
task);
},
err -> {
}
task),
RCLog::e
)
);
subscriptions.add(
disposables.add(
flowable.filter(
callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
......@@ -79,24 +80,25 @@ public class DDPClientImpl {
.subscribe(response -> {
String msg = extractMsg(response);
if ("connected".equals(msg) && !response.isNull("session")) {
currentSession = response.optString("session");
task.trySetResult(
new DDPClientCallback.Connect(client, response.optString("session")));
subscriptions.dispose();
disposables.clear();
} else if ("error".equals(msg) && "Already connected".equals(
response.optString("reason"))) {
task.trySetResult(new DDPClientCallback.Connect(client, null));
subscriptions.dispose();
disposables.clear();
} else if ("failed".equals(msg)) {
task.trySetError(
new DDPClientCallback.Connect.Failed(client, response.optString("version")));
subscriptions.dispose();
disposables.clear();
}
},
err -> task.trySetError(new DDPClientCallback.Connect.Timeout(client))
)
);
addErrorCallback(subscriptions, task);
addErrorCallback(disposables, task);
subscribeBaseListeners();
} catch (Exception e) {
......@@ -112,34 +114,35 @@ public class DDPClientImpl {
sendMessage("ping", json -> json.put("id", id));
if (requested) {
CompositeDisposable subscriptions = new CompositeDisposable();
CompositeDisposable disposables = new CompositeDisposable();
subscriptions.add(
disposables.add(
flowable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.timeout(8, TimeUnit.SECONDS)
.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.dispose();
disposables.clear();
} else {
String _id = response.optString("id");
if (id.equals(_id)) {
task.setResult(new DDPClientCallback.Ping(client, id));
subscriptions.dispose();
disposables.clear();
}
}
disposables.clear();
}
},
err -> task.setError(new DDPClientCallback.Ping.Timeout(client))
)
);
addErrorCallback(subscriptions, task);
addErrorCallback(disposables, task);
} else {
task.trySetError(new DDPClientCallback.Closed(client));
}
......@@ -151,9 +154,9 @@ public class DDPClientImpl {
sendMessage("sub", json -> json.put("id", id).put("name", name).put("params", params));
if (requested) {
CompositeDisposable subscriptions = new CompositeDisposable();
CompositeDisposable disposables = new CompositeDisposable();
subscriptions.add(
disposables.add(
flowable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
.map(DDPClientImpl::toJson)
......@@ -166,7 +169,7 @@ public class DDPClientImpl {
String _id = ids.optString(i);
if (id.equals(_id)) {
task.setResult(new DDPSubscription.Ready(client, id));
subscriptions.dispose();
disposables.clear();
break;
}
}
......@@ -176,16 +179,15 @@ public class DDPClientImpl {
if (id.equals(_id)) {
task.setError(new DDPSubscription.NoSub.Error(client, id,
response.optJSONObject("error")));
subscriptions.dispose();
disposables.clear();
}
}
},
err -> {
}
RCLog::e
)
);
addErrorCallback(subscriptions, task);
addErrorCallback(disposables, task);
} else {
task.trySetError(new DDPClientCallback.Closed(client));
}
......@@ -197,9 +199,9 @@ public class DDPClientImpl {
final boolean requested = sendMessage("unsub", json -> json.put("id", id));
if (requested) {
CompositeDisposable subscriptions = new CompositeDisposable();
CompositeDisposable disposables = new CompositeDisposable();
subscriptions.add(
disposables.add(
flowable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
.map(DDPClientImpl::toJson)
......@@ -210,7 +212,7 @@ public class DDPClientImpl {
String _id = response.optString("id");
if (id.equals(_id)) {
task.setResult(new DDPSubscription.NoSub(client, id));
subscriptions.dispose();
disposables.clear();
}
}
},
......@@ -219,7 +221,7 @@ public class DDPClientImpl {
)
);
addErrorCallback(subscriptions, task);
addErrorCallback(disposables, task);
} else {
task.trySetError(new DDPClientCallback.Closed(client));
}
......@@ -232,9 +234,9 @@ public class DDPClientImpl {
json -> json.put("method", method).put("params", params).put("id", id));
if (requested) {
CompositeDisposable subscriptions = new CompositeDisposable();
CompositeDisposable disposables = new CompositeDisposable();
subscriptions.add(
disposables.add(
flowable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
.map(DDPClientImpl::toJson)
......@@ -252,7 +254,7 @@ public class DDPClientImpl {
String result = response.optString("result");
task.setResult(new DDPClientCallback.RPC(client, id, result));
}
subscriptions.dispose();
disposables.clear();
}
}
},
......@@ -264,20 +266,20 @@ public class DDPClientImpl {
)
);
addErrorCallback(subscriptions, task);
addErrorCallback(disposables, task);
} else {
task.trySetError(new DDPClientCallback.Closed(client));
}
}
private void subscribeBaseListeners() {
if (subscriptions != null &&
subscriptions.size() > 0 && !subscriptions.isDisposed()) {
if (disposables != null &&
disposables.size() > 0 && !disposables.isDisposed()) {
return;
}
subscriptions = new CompositeDisposable();
subscriptions.add(
disposables = new CompositeDisposable();
disposables.add(
flowable.filter(callback -> callback instanceof RxWebSocketCallback.Message)
.map(callback -> ((RxWebSocketCallback.Message) callback).responseBodyString)
.map(DDPClientImpl::toJson)
......@@ -292,8 +294,7 @@ public class DDPClientImpl {
}
}
},
err -> {
}
RCLog::e
)
);
}
......@@ -342,8 +343,8 @@ public class DDPClientImpl {
}
public void unsubscribeBaseListeners() {
if (subscriptions.size() > 0 || !subscriptions.isDisposed()) {
subscriptions.dispose();
if (disposables.size() > 0 || !disposables.isDisposed()) {
disposables.clear();
}
}
......@@ -354,13 +355,7 @@ public class DDPClientImpl {
.cast(RxWebSocketCallback.Close.class)
.subscribe(
task::setResult,
err -> {
if (err instanceof Exception) {
task.setError((Exception) err);
} else {
task.setError(new Exception(err));
}
}
err -> setTaskError(task, err)
);
return task.getTask().onSuccessTask(_task -> {
......@@ -373,28 +368,29 @@ public class DDPClientImpl {
try {
JSONObject origJson = new JSONObject().put("msg", msg);
String msg2 = (json == null ? origJson : json.create(origJson)).toString();
return websocket.sendText(msg2);
websocket.sendText(msg2);
} catch (Exception e) {
RCLog.e(e);
return false;
}
return true; // ignore exception here.
}
private void sendMessage(String msg, @Nullable JSONBuilder json,
TaskCompletionSource<?> taskForSetError) {
if (!sendMessage(msg, json)) {
if (!sendMessage(msg, json) && taskForSetError != null) {
taskForSetError.trySetError(new DDPClientCallback.Closed(client));
}
}
private void addErrorCallback(CompositeDisposable subscriptions, TaskCompletionSource<?> task) {
subscriptions.add(
private void addErrorCallback(CompositeDisposable disposables, TaskCompletionSource<?> task) {
disposables.add(
flowable.subscribe(
base -> {
},
err -> {
task.trySetError(new Exception(err));
subscriptions.dispose();
disposables.clear();
}
)
);
......@@ -408,6 +404,14 @@ public class DDPClientImpl {
}
}
private void setTaskError(TaskCompletionSource<? extends RxWebSocketCallback.Base> task, Throwable throwable) {
if (throwable instanceof Exception) {
task.setError((Exception) throwable);
} else {
task.setError(new Exception(throwable));
}
}
private interface JSONBuilder {
@NonNull
JSONObject create(JSONObject root) throws JSONException;
......
package chat.rocket.android_ddp.rx;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import chat.rocket.android.log.RCLog;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.FlowableOnSubscribe;
import io.reactivex.exceptions.OnErrorNotImplementedException;
import io.reactivex.flowables.ConnectableFlowable;
import java.io.IOException;
import chat.rocket.android.log.RCLog;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
......@@ -15,8 +16,10 @@ import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
public class RxWebSocket {
public static final int REASON_NETWORK_ERROR = 100;
private OkHttpClient httpClient;
private WebSocket webSocket;
private boolean hadErrorsBefore;
public RxWebSocket(OkHttpClient client) {
httpClient = client;
......@@ -30,6 +33,7 @@ public class RxWebSocket {
.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
hadErrorsBefore = false;
RxWebSocket.this.webSocket = webSocket;
emitter.onNext(new RxWebSocketCallback.Open(RxWebSocket.this.webSocket, response));
}
......@@ -37,7 +41,11 @@ public class RxWebSocket {
@Override
public void onFailure(WebSocket webSocket, Throwable err, Response response) {
try {
emitter.onError(new RxWebSocketCallback.Failure(webSocket, err, response));
if (!hadErrorsBefore) {
hadErrorsBefore = true;
emitter.onNext(new RxWebSocketCallback.Close(webSocket, REASON_NETWORK_ERROR, err.getMessage()));
emitter.onComplete();
}
} catch (OnErrorNotImplementedException ex) {
RCLog.w(ex, "OnErrorNotImplementedException ignored");
}
......@@ -51,11 +59,10 @@ public class RxWebSocket {
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
emitter.onNext(new RxWebSocketCallback.Close(webSocket, code, reason));
emitter.onComplete();
}
}),
BackpressureStrategy.BUFFER
).publish();
).delay(4, TimeUnit.SECONDS).publish();
}
public boolean sendText(String message) throws IOException {
......
package chat.rocket.android_ddp.rx;
import static android.R.attr.type;
import chat.rocket.android.log.RCLog;
import okhttp3.Response;
import okhttp3.WebSocket;
......@@ -28,25 +26,8 @@ public class RxWebSocketCallback {
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, Throwable err, Response response) {
super(err);
this.ws = websocket;
this.response = response;
}
@Override
public String toString() {
if (response != null) {
return "[" + type + "] " + response.message();
} else {
return super.toString();
if (response != null && response.body() != null) {
this.response.body().close();
}
}
}
......
......@@ -35,7 +35,7 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 16
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 26
versionCode 27
versionName "1.0.16"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
......
......@@ -16,11 +16,11 @@ import chat.rocket.android.fragment.chatroom.HomeFragment;
import chat.rocket.android.fragment.chatroom.RoomFragment;
import chat.rocket.android.fragment.sidebar.SidebarMainFragment;
import chat.rocket.android.helper.KeyboardHelper;
import chat.rocket.android.service.ConnectivityManager;
import chat.rocket.android.widget.RoomToolbar;
import chat.rocket.core.interactors.CanCreateRoomInteractor;
import chat.rocket.core.interactors.RoomInteractor;
import chat.rocket.core.interactors.SessionInteractor;
import chat.rocket.android.service.ConnectivityManager;
import chat.rocket.android.widget.RoomToolbar;
import chat.rocket.persistence.realm.repositories.RealmRoomRepository;
import chat.rocket.persistence.realm.repositories.RealmSessionRepository;
import chat.rocket.persistence.realm.repositories.RealmUserRepository;
......@@ -34,6 +34,7 @@ public class MainActivity extends AbstractAuthedActivity implements MainContract
private StatusTicker statusTicker;
private MainContract.Presenter presenter;
private RoomFragment roomFragment;
@Override
protected int getLayoutContainerForFragment() {
......@@ -179,7 +180,8 @@ public class MainActivity extends AbstractAuthedActivity implements MainContract
@Override
public void showRoom(String hostname, String roomId) {
showFragment(RoomFragment.create(hostname, roomId));
roomFragment = RoomFragment.create(hostname, roomId);
showFragment(roomFragment);
closeSidebarIfNeeded();
KeyboardHelper.hideSoftKeyboard(this);
}
......@@ -222,6 +224,9 @@ public class MainActivity extends AbstractAuthedActivity implements MainContract
@Override
public void showConnectionOk() {
statusTicker.updateStatus(StatusTicker.STATUS_DISMISS, null);
if (roomFragment != null) {
roomFragment.refreshRoom();
}
}
//TODO: consider this class to define in layouthelper for more complicated operation.
......
......@@ -3,22 +3,23 @@ package chat.rocket.android.activity;
import android.support.annotation.NonNull;
import android.support.v4.util.Pair;
import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import chat.rocket.android.BackgroundLooper;
import chat.rocket.android.RocketChatCache;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.helper.LogIfError;
import chat.rocket.android.helper.Logger;
import chat.rocket.android.service.ConnectivityManagerApi;
import chat.rocket.android.service.ServerConnectivity;
import chat.rocket.android.shared.BasePresenter;
import chat.rocket.core.interactors.CanCreateRoomInteractor;
import chat.rocket.core.interactors.RoomInteractor;
import chat.rocket.core.interactors.SessionInteractor;
import chat.rocket.core.models.Session;
import chat.rocket.core.models.User;
import hu.akarnokd.rxjava.interop.RxJavaInterop;
import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
public class MainPresenter extends BasePresenter<MainContract.View>
implements MainContract.Presenter {
......@@ -63,6 +64,7 @@ public class MainPresenter extends BasePresenter<MainContract.View>
openRoom();
subscribeToNetworkChanges();
subscribeToUnreadCount();
subscribeToSession();
setUserOnline();
......@@ -96,10 +98,8 @@ public class MainPresenter extends BasePresenter<MainContract.View>
@Override
public void onRetryLogin() {
final Disposable subscription = sessionInteractor.retryLogin()
.subscribe();
addSubscription(subscription);
view.showConnecting();
connectivityManagerApi.keepAliveServer();
}
private void openRoom() {
......@@ -161,6 +161,23 @@ public class MainPresenter extends BasePresenter<MainContract.View>
addSubscription(subscription);
}
private void subscribeToNetworkChanges() {
Disposable disposable = RxJavaInterop.toV2Flowable(connectivityManagerApi.getServerConnectivityAsObservable())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
connectivity -> {
if (connectivity.state == ServerConnectivity.STATE_CONNECTED) {
view.showConnectionOk();
return;
}
view.showConnecting();
},
Logger::report
);
addSubscription(disposable);
}
private void setUserOnline() {
methodCallHelper.setUserPresence(User.STATUS_ONLINE)
.continueWith(new LogIfError());
......
package chat.rocket.android.api;
import android.support.annotation.Nullable;
import io.reactivex.Flowable;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.UUID;
import bolts.Task;
import chat.rocket.android.helper.OkHttpHelper;
import chat.rocket.android.helper.TextUtils;
......@@ -13,6 +14,7 @@ import chat.rocket.android.log.RCLog;
import chat.rocket.android_ddp.DDPClient;
import chat.rocket.android_ddp.DDPClientCallback;
import chat.rocket.android_ddp.DDPSubscription;
import io.reactivex.Flowable;
/**
* DDP client wrapper.
......
......@@ -49,5 +49,7 @@ public interface RoomContract {
void onUnreadCount();
void onMarkAsRead();
void refreshRoom();
}
}
......@@ -21,6 +21,11 @@ import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import chat.rocket.android.BackgroundLooper;
import chat.rocket.android.R;
import chat.rocket.android.api.MethodCallHelper;
......@@ -70,15 +75,13 @@ import chat.rocket.persistence.realm.repositories.RealmSessionRepository;
import chat.rocket.persistence.realm.repositories.RealmSpotlightRoomRepository;
import chat.rocket.persistence.realm.repositories.RealmSpotlightUserRepository;
import chat.rocket.persistence.realm.repositories.RealmUserRepository;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import com.jakewharton.rxbinding2.support.v4.widget.RxDrawerLayout;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import permissions.dispatcher.NeedsPermission;
import permissions.dispatcher.RuntimePermissions;
......@@ -188,7 +191,7 @@ public class RoomFragment extends AbstractChatRoomFragment implements
protected void onSetupView() {
messageRecyclerView = rootView.findViewById(R.id.messageRecyclerView);
messageListAdapter = new MessageListAdapter(getContext());
messageListAdapter = new MessageListAdapter(getContext(), hostname);
messageRecyclerView.setAdapter(messageListAdapter);
messageListAdapter.setOnItemClickListener(this);
messageListAdapter.setOnItemLongClickListener(this);
......@@ -251,8 +254,8 @@ public class RoomFragment extends AbstractChatRoomFragment implements
protected Snackbar getUnreadCountIndicatorView(int count) {
// TODO: replace with another custom View widget, not to hide message composer.
final String caption = getResources().getString(
R.string.fmt_dialog_view_latest_message_title, count);
final String caption = getResources().getQuantityString(
R.plurals.fmt_dialog_view_latest_message_title, count, count);
return Snackbar.make(rootView, caption, Snackbar.LENGTH_LONG)
.setAction(R.string.dialog_view_latest_message_action, view -> scrollToLatestMessage());
......@@ -369,7 +372,7 @@ public class RoomFragment extends AbstractChatRoomFragment implements
new DefaultTempSpotlightUserCaller(methodCallHelper)
),
pair.first.get(),
RocketChatUserStatusProvider.getInstance(),
RocketChatUserStatusProvider.INSTANCE,
AndroidSchedulers.from(BackgroundLooper.get()),
AndroidSchedulers.mainThread()
)
......@@ -599,4 +602,8 @@ public class RoomFragment extends AbstractChatRoomFragment implements
edittingMessage = message;
messageFormManager.setEditMessage(message.getMessage());
}
public void refreshRoom() {
presenter.loadMessages();
}
}
\ No newline at end of file
......@@ -4,7 +4,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.Pair;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
......@@ -55,7 +55,11 @@ public class RoomPresenter extends BasePresenter<RoomContract.View>
@Override
public void bindView(@NonNull RoomContract.View view) {
super.bindView(view);
refreshRoom();
}
@Override
public void refreshRoom() {
getRoomRoles();
getRoomInfo();
getRoomHistoryStateInfo();
......
......@@ -7,7 +7,7 @@ import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.TextView;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import org.json.JSONArray;
......@@ -117,7 +117,7 @@ public class UsersOfRoomDialogFragment extends AbstractChatRoomDialogFragment {
RecyclerView recyclerView = (RecyclerView) getDialog().findViewById(R.id.recyclerview);
recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
recyclerView.setAdapter(
new RoomUserAdapter(getContext(), realmHelper, rocketChatAbsoluteUrlOptional.get()));
new RoomUserAdapter(getContext(), realmHelper, rocketChatAbsoluteUrlOptional.get(), hostname));
}
private void requestGetUsersOfRoom() {
......@@ -192,7 +192,7 @@ public class UsersOfRoomDialogFragment extends AbstractChatRoomDialogFragment {
*/
private void onRenderTotalCount(long total) {
TextView userCount = (TextView) getDialog().findViewById(R.id.room_user_count);
userCount.setText(getString(R.string.fmt_room_user_count, total));
userCount.setText(getResources().getQuantityString(R.plurals.fmt_room_user_count, (int) total, total));
}
/**
......
......@@ -2,7 +2,7 @@ package chat.rocket.android.fragment.server_config;
import android.support.annotation.NonNull;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.android.schedulers.AndroidSchedulers;
import bolts.Task;
......
......@@ -2,7 +2,7 @@ package chat.rocket.android.fragment.server_config;
import android.support.annotation.NonNull;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.android.schedulers.AndroidSchedulers;
import chat.rocket.android.BackgroundLooper;
......
......@@ -224,10 +224,10 @@ public class SidebarMainFragment extends AbstractFragment implements SidebarMain
private void onRenderCurrentUser(User user, RocketChatAbsoluteUrl absoluteUrl) {
if (user != null && absoluteUrl != null) {
new UserRenderer(getContext(), user)
.avatarInto(rootView.findViewById(R.id.current_user_avatar), absoluteUrl)
.usernameInto(rootView.findViewById(R.id.current_user_name))
.statusColorInto(rootView.findViewById(R.id.current_user_status));
UserRenderer userRenderer = new UserRenderer(user);
userRenderer.showAvatar(rootView.findViewById(R.id.current_user_avatar), hostname);
userRenderer.showUsername(rootView.findViewById(R.id.current_user_name));
userRenderer.showStatusColor(rootView.findViewById(R.id.current_user_status));
}
}
......
......@@ -5,7 +5,7 @@ import android.os.Bundle;
import android.view.View;
import android.widget.AutoCompleteTextView;
import android.widget.TextView;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import com.jakewharton.rxbinding2.widget.RxTextView;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
......@@ -97,7 +97,7 @@ public class AddDirectMessageDialogFragment extends AbstractAddRoomDialogFragmen
(realm, text) -> realm.where(RealmUser.class)
.contains(RealmUser.USERNAME, text, Case.INSENSITIVE)
.findAllSorted(RealmUser.USERNAME),
context -> new SuggestUserAdapter(context, rocketChatAbsoluteUrlOptional.get()));
context -> new SuggestUserAdapter(context, rocketChatAbsoluteUrlOptional.get(), hostname));
autoCompleteTextView.setAdapter(adapter);
}
......
package chat.rocket.android.helper;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
......
package chat.rocket.android.helper
import chat.rocket.android.widget.AbsoluteUrl
import chat.rocket.android.widget.RocketChatAvatar
import java.net.URLEncoder
class Avatar(val absoluteUrl: AbsoluteUrl?, val username: String) {
val imageUrl: String
/** REMARK
* This is often a SVG image (see Rocket.Chat:server/startup/avatar.js)
*/
get() {
val avatarUrl = "/avatar/" + URLEncoder.encode(username, "UTF-8")
if (absoluteUrl == null) {
return avatarUrl
}
return absoluteUrl.from("/avatar/" + URLEncoder.encode(username, "UTF-8"))
}
/**
* render avatar into RocketChatAvatar.
*/
fun into(rocketChatAvatar: RocketChatAvatar) {
rocketChatAvatar.loadImage(imageUrl)
}
}
\ No newline at end of file
package chat.rocket.android.helper
import java.net.URLEncoder
class RocketChatUserAvatar(val hostname: String, val username: String) {
val imageUri: String
/** REMARK
* This is often a SVG image (see Rocket.Chat:server/startup/avatar.js).
*/
get() {
return "https://" +
hostname.replace("http://", "").replace("https://", "") +
"/avatar/" +
URLEncoder.encode(username, "UTF-8")
}
}
\ No newline at end of file
......@@ -13,21 +13,24 @@ import chat.rocket.core.SyncState;
public abstract class AbstractMessageViewHolder extends ModelViewHolder<PairedMessage> {
protected final RocketChatAvatar avatar;
protected final ImageView userNotFoundAvatarImageView;
protected final ImageView errorImageView;
protected final TextView username;
protected final TextView subUsername;
protected final TextView timestamp;
protected final View userAndTimeContainer;
protected final AbsoluteUrl absoluteUrl;
protected final String hostname;
protected final View newDayContainer;
protected final TextView newDayText;
/**
* constructor WITH hostname.
*/
public AbstractMessageViewHolder(View itemView, AbsoluteUrl absoluteUrl) {
public AbstractMessageViewHolder(View itemView, AbsoluteUrl absoluteUrl, String hostname) {
super(itemView);
avatar = itemView.findViewById(R.id.user_avatar);
userNotFoundAvatarImageView = itemView.findViewById(R.id.userNotFoundAvatarImageView);
errorImageView = itemView.findViewById(R.id.errorImageView);
username = itemView.findViewById(R.id.username);
subUsername = itemView.findViewById(R.id.sub_username);
......@@ -36,6 +39,7 @@ public abstract class AbstractMessageViewHolder extends ModelViewHolder<PairedMe
newDayContainer = itemView.findViewById(R.id.newday_container);
newDayText = itemView.findViewById(R.id.newday_text);
this.absoluteUrl = absoluteUrl;
this.hostname = hostname;
}
/**
......
......@@ -22,14 +22,16 @@ public class MessageListAdapter extends ExtModelListAdapter<Message, PairedMessa
private static final int VIEW_TYPE_NORMAL_MESSAGE = 1;
private static final int VIEW_TYPE_SYSTEM_MESSAGE = 2;
private String hostname;
private AbsoluteUrl absoluteUrl;
private boolean autoloadImages = false;
private boolean hasNext;
private boolean isLoaded;
public MessageListAdapter(Context context) {
public MessageListAdapter(Context context, String hostname) {
super(context);
this.hostname = hostname;
}
public void setAbsoluteUrl(AbsoluteUrl absoluteUrl) {
......@@ -92,11 +94,11 @@ public class MessageListAdapter extends ExtModelListAdapter<Message, PairedMessa
protected AbstractMessageViewHolder onCreateRealmModelViewHolder(int viewType, View itemView) {
switch (viewType) {
case VIEW_TYPE_NORMAL_MESSAGE:
return new MessageNormalViewHolder(itemView, absoluteUrl);
return new MessageNormalViewHolder(itemView, absoluteUrl, hostname);
case VIEW_TYPE_SYSTEM_MESSAGE:
return new MessageSystemViewHolder(itemView, absoluteUrl);
return new MessageSystemViewHolder(itemView, absoluteUrl, hostname);
default:
return new AbstractMessageViewHolder(itemView, absoluteUrl) {
return new AbstractMessageViewHolder(itemView, absoluteUrl, hostname) {
@Override
protected void bindMessage(PairedMessage pairedMessage, boolean autoloadImages) {}
};
......
......@@ -19,8 +19,8 @@ public class MessageNormalViewHolder extends AbstractMessageViewHolder {
/**
* constructor WITH hostname.
*/
public MessageNormalViewHolder(View itemView, AbsoluteUrl absoluteUrl) {
super(itemView, absoluteUrl);
public MessageNormalViewHolder(View itemView, AbsoluteUrl absoluteUrl, String hostname) {
super(itemView, absoluteUrl, hostname);
body = itemView.findViewById(R.id.message_body);
urls = itemView.findViewById(R.id.message_urls);
attachments = itemView.findViewById(R.id.message_attachments);
......@@ -28,12 +28,12 @@ public class MessageNormalViewHolder extends AbstractMessageViewHolder {
@Override
protected void bindMessage(PairedMessage pairedMessage, boolean autoloadImages) {
new MessageRenderer(itemView.getContext(), pairedMessage.target, autoloadImages)
.avatarInto(avatar, absoluteUrl)
.usernameInto(username, subUsername)
.timestampInto(timestamp)
.bodyInto(body)
.urlsInto(urls)
.attachmentsInto(attachments, absoluteUrl);
MessageRenderer messageRenderer = new MessageRenderer(pairedMessage.target, autoloadImages);
messageRenderer.showAvatar(avatar, hostname, userNotFoundAvatarImageView);
messageRenderer.showUsername(username, subUsername);
messageRenderer.showTimestampOrMessageState(timestamp);
messageRenderer.showBody(body);
messageRenderer.showUrl(urls);
messageRenderer.showAttachment(attachments, absoluteUrl);
}
}
......@@ -16,18 +16,17 @@ public class MessageSystemViewHolder extends AbstractMessageViewHolder {
/**
* constructor WITH hostname.
*/
public MessageSystemViewHolder(View itemView, AbsoluteUrl absoluteUrl) {
super(itemView, absoluteUrl);
public MessageSystemViewHolder(View itemView, AbsoluteUrl absoluteUrl, String hostname) {
super(itemView, absoluteUrl, hostname);
body = itemView.findViewById(R.id.message_body);
}
@Override
protected void bindMessage(PairedMessage pairedMessage, boolean autoloadImages) {
new MessageRenderer(itemView.getContext(), pairedMessage.target, autoloadImages)
.avatarInto(avatar, absoluteUrl)
.usernameInto(username, subUsername)
.timestampInto(timestamp);
MessageRenderer messageRenderer = new MessageRenderer(pairedMessage.target, autoloadImages);
messageRenderer.showAvatar(avatar, hostname, userNotFoundAvatarImageView);
messageRenderer.showUsername(username, subUsername);
messageRenderer.showTimestampOrMessageState(timestamp);
if (pairedMessage.target != null) {
body.setText(MessageType.parse(pairedMessage.target.getType())
.getString(body.getContext(), pairedMessage.target));
......
......@@ -24,16 +24,18 @@ public class RoomUserAdapter extends RecyclerView.Adapter<RoomUserViewHolder> {
private final LayoutInflater inflater;
private final RealmHelper realmHelper;
private final AbsoluteUrl absoluteUrl;
private final String hostname;
private List<String> usernames;
/**
* Constructor with required parameters.
*/
public RoomUserAdapter(Context context, RealmHelper realmHelper, AbsoluteUrl absoluteUrl) {
public RoomUserAdapter(Context context, RealmHelper realmHelper, AbsoluteUrl absoluteUrl, String hostname) {
this.context = context;
this.inflater = LayoutInflater.from(context);
this.realmHelper = realmHelper;
this.absoluteUrl = absoluteUrl;
this.hostname = hostname;
}
@Override
......@@ -57,14 +59,15 @@ public class RoomUserAdapter extends RecyclerView.Adapter<RoomUserViewHolder> {
.setUsername(username)
.setUtcOffset(0)
.build();
new UserRenderer(context, user)
.avatarInto(holder.avatar, absoluteUrl)
.usernameInto(holder.username);
UserRenderer userRenderer = new UserRenderer(user);
userRenderer.showAvatar(holder.avatar, hostname);
userRenderer.showUsername(holder.username);
} else {
new UserRenderer(context, realmUser.asUser())
.statusColorInto(holder.status)
.avatarInto(holder.avatar, absoluteUrl)
.usernameInto(holder.username);
UserRenderer userRenderer = new UserRenderer(realmUser.asUser());
userRenderer.showAvatar(holder.avatar, hostname);
userRenderer.showUsername(holder.username);
userRenderer.showStatusColor(holder.status);
}
}
......
......@@ -16,17 +16,19 @@ import chat.rocket.android.renderer.UserRenderer;
*/
public class SuggestUserAdapter extends RealmAutoCompleteAdapter<RealmUser> {
private final AbsoluteUrl absoluteUrl;
private final String hostname;
public SuggestUserAdapter(Context context, AbsoluteUrl absoluteUrl) {
public SuggestUserAdapter(Context context, AbsoluteUrl absoluteUrl, String hostname) {
super(context, R.layout.listitem_room_user, R.id.room_user_name);
this.absoluteUrl = absoluteUrl;
this.hostname = hostname;
}
@Override
protected void onBindItemView(View itemView, RealmUser user) {
new UserRenderer(itemView.getContext(), user.asUser())
.statusColorInto(itemView.findViewById(R.id.room_user_status))
.avatarInto(itemView.findViewById(R.id.room_user_avatar), absoluteUrl);
UserRenderer userRenderer = new UserRenderer(user.asUser());
userRenderer.showStatusColor(itemView.findViewById(R.id.room_user_status));
userRenderer.showAvatar(itemView.findViewById(R.id.room_user_avatar), hostname);
}
@Override
......
package chat.rocket.android.push;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
......@@ -13,13 +14,16 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.RequiresApi;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.RemoteInput;
import android.support.v4.util.SparseArrayCompat;
import android.text.Html;
import android.text.Spanned;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
......@@ -31,6 +35,7 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import chat.rocket.android.activity.MainActivity;
import chat.rocket.android.helper.ServerPolicyHelper;
import chat.rocket.android.service.ConnectivityManager;
......@@ -93,7 +98,7 @@ public class PushNotificationHandler implements PushConstants {
}
public void createNotification(Context context, Bundle extras) {
NotificationManager mNotificationManager =
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
String appName = getAppName(context);
String packageName = context.getPackageName();
......@@ -117,13 +122,6 @@ public class PushNotificationHandler implements PushConstants {
PendingIntent contentIntent = PendingIntent
.getActivity(context, requestCode, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context)
.setWhen(System.currentTimeMillis())
.setContentTitle(fromHtml(extras.getString(TITLE)))
.setTicker(fromHtml(extras.getString(TITLE)))
.setContentIntent(contentIntent)
.setAutoCancel(true);
SharedPreferences prefs = context
.getSharedPreferences(PushConstants.COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE);
String localIcon = prefs.getString(ICON, null);
......@@ -135,91 +133,62 @@ public class PushNotificationHandler implements PushConstants {
Log.d(LOG_TAG, "stored sound=" + soundOption);
Log.d(LOG_TAG, "stored vibrate=" + vibrateOption);
/*
* Notification Vibration
*/
setNotificationVibration(extras, vibrateOption, notificationBuilder);
if (Build.VERSION.SDK_INT >= 26) {
String channelId = "rocket-chat-channel";
CharSequence name = "RocketChatMessage";
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel channel = new NotificationChannel(channelId, name, importance);
channel.enableLights(true);
notificationManager.createNotificationChannel(channel);
/*
* Notification Icon Color
*
* Sets the small-icon background color of the notification.
* To use, add the `iconColor` key to plugin android options
*
*/
setNotificationIconColor(extras.getString("color"), notificationBuilder, localIconColor);
Notification.Builder notificationBuilder = new Notification.Builder(context, channelId)
.setWhen(System.currentTimeMillis())
.setContentTitle(fromHtml(extras.getString(TITLE)))
.setTicker(fromHtml(extras.getString(TITLE)))
.setContentIntent(contentIntent)
.setChannelId(channelId)
.setAutoCancel(true);
/*
* Notification Icon
*
* Sets the small-icon of the notification.
*
* - checks the plugin options for `icon` key
* - if none, uses the application icon
*
* The icon value must be a string that maps to a drawable resource.
* If no resource is found, falls
*
*/
setNotificationSmallIcon(context, extras, packageName, resources, notificationBuilder,
setNotificationImportance(extras, channel);
setNotificationVibration(extras, vibrateOption, channel);
setNotificationMessage(notId, extras, notificationBuilder);
setNotificationCount(context, extras, notificationBuilder);
setNotificationSmallIcon(extras, packageName, resources, notificationBuilder,
localIcon);
/*
* Notification Large-Icon
*
* Sets the large-icon of the notification
*
* - checks the gcm data for the `image` key
* - checks to see if remote image, loads it.
* - checks to see if assets image, Loads It.
* - checks to see if resource image, LOADS IT!
* - if none, we don't set the large icon
*
*/
setNotificationLargeIcon(context, extras, packageName, resources, notificationBuilder);
setNotificationLedColor(extras, channel);
if (soundOption) {
setNotificationSound(context, extras, channel);
}
createActions(context, extras, notificationBuilder, resources, packageName, notId);
notificationManager.notify(notId, notificationBuilder.build());
} else {
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context)
.setWhen(System.currentTimeMillis())
.setContentTitle(fromHtml(extras.getString(TITLE)))
.setTicker(fromHtml(extras.getString(TITLE)))
.setContentIntent(contentIntent)
.setAutoCancel(true);
/*
* Notification Sound
*/
setNotificationCount(context, extras, notificationBuilder);
setNotificationVibration(extras, vibrateOption, notificationBuilder);
setNotificationIconColor(extras.getString("color"), notificationBuilder, localIconColor);
setNotificationSmallIcon(extras, packageName, resources, notificationBuilder,
localIcon);
setNotificationLargeIcon(context, extras, packageName, resources, notificationBuilder);
if (soundOption) {
setNotificationSound(context, extras, notificationBuilder);
}
/*
* LED Notification
*/
setNotificationLedColor(extras, notificationBuilder);
/*
* Priority Notification
*/
setNotificationPriority(extras, notificationBuilder);
/*
* Notification message
*/
setNotificationMessage(notId, extras, notificationBuilder);
/*
* Notification count
*/
setNotificationCount(context, extras, notificationBuilder);
/*
* Notification count
*/
setVisibility(context, extras, notificationBuilder);
/*
* Notification add actions
*/
createActions(context, extras, notificationBuilder, resources, packageName, notId);
mNotificationManager.notify(appName, notId, notificationBuilder.build());
notificationManager.notify(appName, notId, notificationBuilder.build());
}
}
private void createActions(Context context, Bundle extras, NotificationCompat.Builder mBuilder,
private void createActions(Context context, Bundle extras, NotificationCompat.Builder builder,
Resources resources, String packageName, int notId) {
Log.d(LOG_TAG, "create actions: with in-line");
String actions = extras.getString(ACTIONS);
......@@ -298,16 +267,112 @@ public class PushNotificationHandler implements PushConstants {
wActions.add(actionBuilder.build());
if (inline) {
mBuilder.addAction(wAction);
builder.addAction(wAction);
} else {
builder.addAction(
resources.getIdentifier(action.optString(ICON, ""), DRAWABLE, packageName),
action.getString(TITLE), pIntent);
}
wAction = null;
pIntent = null;
}
builder.extend(new NotificationCompat.WearableExtender().addActions(wActions));
wActions.clear();
} catch (JSONException e) {
// nope
}
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH)
private void createActions(Context context, Bundle extras, Notification.Builder builder,
Resources resources, String packageName, int notId) {
Log.d(LOG_TAG, "create actions: with in-line");
String actions = extras.getString(ACTIONS);
if (actions == null) {
return;
}
try {
JSONArray actionsArray = new JSONArray(actions);
ArrayList<Notification.Action> wActions = new ArrayList<>();
for (int i = 0; i < actionsArray.length(); i++) {
int min = 1;
int max = 2000000000;
Random random = new Random();
int uniquePendingIntentRequestCode = random.nextInt((max - min) + 1) + min;
Log.d(LOG_TAG, "adding action");
JSONObject action = actionsArray.getJSONObject(i);
Log.d(LOG_TAG, "adding callback = " + action.getString(CALLBACK));
boolean foreground = action.optBoolean(FOREGROUND, true);
boolean inline = action.optBoolean("inline", false);
Intent intent;
PendingIntent pIntent;
if (inline) {
Log.d(LOG_TAG, "Version: " + android.os.Build.VERSION.SDK_INT + " = "
+ android.os.Build.VERSION_CODES.M);
if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.M) {
Log.d(LOG_TAG, "push activity");
intent = new Intent(context, MainActivity.class);
} else {
Log.d(LOG_TAG, "push receiver");
intent = new Intent(context, BackgroundActionButtonHandler.class);
}
updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId);
if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.M) {
Log.d(LOG_TAG, "push activity for notId " + notId);
pIntent =
PendingIntent.getActivity(context, uniquePendingIntentRequestCode, intent,
PendingIntent.FLAG_ONE_SHOT);
} else {
Log.d(LOG_TAG, "push receiver for notId " + notId);
pIntent = PendingIntent
.getBroadcast(context, uniquePendingIntentRequestCode, intent,
PendingIntent.FLAG_ONE_SHOT);
}
} else if (foreground) {
intent = new Intent(context, MainActivity.class);
updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId);
pIntent = PendingIntent
.getActivity(context, uniquePendingIntentRequestCode, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
} else {
intent = new Intent(context, BackgroundActionButtonHandler.class);
updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId);
pIntent = PendingIntent
.getBroadcast(context, uniquePendingIntentRequestCode, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
Notification.Action.Builder actionBuilder = new Notification.Action.Builder(
resources.getIdentifier(action.optString(ICON, ""), DRAWABLE, packageName),
action.getString(TITLE), pIntent);
android.app.RemoteInput remoteInput;
if (inline) {
Log.d(LOG_TAG, "create remote input");
String replyLabel = "Enter your reply here";
remoteInput = new android.app.RemoteInput.Builder(INLINE_REPLY)
.setLabel(replyLabel)
.build();
actionBuilder.addRemoteInput(remoteInput);
}
Notification.Action wAction = actionBuilder.build();
wActions.add(actionBuilder.build());
if (inline) {
builder.addAction(wAction);
} else {
mBuilder.addAction(
builder.addAction(
resources.getIdentifier(action.optString(ICON, ""), DRAWABLE, packageName),
action.getString(TITLE), pIntent);
}
wAction = null;
pIntent = null;
}
mBuilder.extend(new NotificationCompat.WearableExtender().addActions(wActions));
builder.extend(new Notification.WearableExtender().addActions(wActions));
wActions.clear();
} catch (JSONException e) {
// nope
......@@ -315,15 +380,24 @@ public class PushNotificationHandler implements PushConstants {
}
private void setNotificationCount(Context context, Bundle extras,
NotificationCompat.Builder mBuilder) {
NotificationCompat.Builder builder) {
int count = extractBadgeCount(extras);
if (count >= 0) {
Log.d(LOG_TAG, "count =[" + count + "]");
builder.setNumber(count);
}
}
private void setNotificationCount(Context context, Bundle extras,
Notification.Builder builder) {
int count = extractBadgeCount(extras);
if (count >= 0) {
Log.d(LOG_TAG, "count =[" + count + "]");
mBuilder.setNumber(count);
builder.setNumber(count);
}
}
private void setVisibility(Context context, Bundle extras, NotificationCompat.Builder mBuilder) {
private void setVisibility(Context context, Bundle extras, NotificationCompat.Builder builder) {
String visibilityStr = extras.getString(VISIBILITY);
if (visibilityStr == null) {
return;
......@@ -333,7 +407,7 @@ public class PushNotificationHandler implements PushConstants {
Integer visibility = Integer.parseInt(visibilityStr);
if (visibility >= NotificationCompat.VISIBILITY_SECRET
&& visibility <= NotificationCompat.VISIBILITY_PUBLIC) {
mBuilder.setVisibility(visibility);
builder.setVisibility(visibility);
} else {
Log.e(LOG_TAG, "Visibility parameter must be between -1 and 1");
}
......@@ -343,7 +417,28 @@ public class PushNotificationHandler implements PushConstants {
}
private void setNotificationVibration(Bundle extras, Boolean vibrateOption,
NotificationCompat.Builder mBuilder) {
NotificationCompat.Builder builder) {
String vibrationPattern = extras.getString(VIBRATION_PATTERN);
if (vibrationPattern != null) {
String[] items = vibrationPattern.replaceAll("\\[", "").replaceAll("\\]", "").split(",");
long[] results = new long[items.length];
for (int i = 0; i < items.length; i++) {
try {
results[i] = Long.parseLong(items[i].trim());
} catch (NumberFormatException nfe) {
}
}
builder.setVibrate(results);
} else {
if (vibrateOption) {
builder.setDefaults(Notification.DEFAULT_VIBRATE);
}
}
}
@RequiresApi(api = 26)
private void setNotificationVibration(Bundle extras, Boolean vibrateOption,
NotificationChannel channel) {
String vibrationPattern = extras.getString(VIBRATION_PATTERN);
if (vibrationPattern != null) {
String[] items = vibrationPattern.replaceAll("\\[", "").replaceAll("\\]", "").split(",");
......@@ -354,23 +449,23 @@ public class PushNotificationHandler implements PushConstants {
} catch (NumberFormatException nfe) {
}
}
mBuilder.setVibrate(results);
channel.setVibrationPattern(results);
} else {
if (vibrateOption) {
mBuilder.setDefaults(Notification.DEFAULT_VIBRATE);
channel.enableVibration(true);
}
}
}
private void setNotificationMessage(int notId, Bundle extras,
NotificationCompat.Builder mBuilder) {
NotificationCompat.Builder builder) {
String message = extras.getString(MESSAGE);
String style = extras.getString(STYLE, STYLE_TEXT);
if (STYLE_INBOX.equals(style)) {
setNotification(notId, message);
mBuilder.setContentText(fromHtml(message));
builder.setContentText(fromHtml(message));
ArrayList<String> messageList = getMessageList(notId);
Integer sizeList = messageList.size();
......@@ -389,13 +484,13 @@ public class PushNotificationHandler implements PushConstants {
notificationInbox.addLine(fromHtml(messageList.get(i)));
}
mBuilder.setStyle(notificationInbox);
builder.setStyle(notificationInbox);
} else {
NotificationCompat.BigTextStyle bigText = new NotificationCompat.BigTextStyle();
if (message != null) {
bigText.bigText(fromHtml(message));
bigText.setBigContentTitle(fromHtml(extras.getString(TITLE)));
mBuilder.setStyle(bigText);
builder.setStyle(bigText);
}
}
} else if (STYLE_PICTURE.equals(style)) {
......@@ -406,17 +501,17 @@ public class PushNotificationHandler implements PushConstants {
bigPicture.setBigContentTitle(fromHtml(extras.getString(TITLE)));
bigPicture.setSummaryText(fromHtml(extras.getString(SUMMARY_TEXT)));
mBuilder.setContentTitle(fromHtml(extras.getString(TITLE)));
mBuilder.setContentText(fromHtml(message));
builder.setContentTitle(fromHtml(extras.getString(TITLE)));
builder.setContentText(fromHtml(message));
mBuilder.setStyle(bigPicture);
builder.setStyle(bigPicture);
} else {
setNotification(notId, "");
NotificationCompat.BigTextStyle bigText = new NotificationCompat.BigTextStyle();
if (message != null) {
mBuilder.setContentText(fromHtml(message));
builder.setContentText(fromHtml(message));
bigText.bigText(fromHtml(message));
bigText.setBigContentTitle(fromHtml(extras.getString(TITLE)));
......@@ -426,30 +521,145 @@ public class PushNotificationHandler implements PushConstants {
bigText.setSummaryText(fromHtml(summaryText));
}
mBuilder.setStyle(bigText);
builder.setStyle(bigText);
}
}
}
private void setNotificationMessage(int notId, Bundle extras,
Notification.Builder builder) {
String message = extras.getString(MESSAGE);
String style = extras.getString(STYLE, STYLE_TEXT);
if (STYLE_INBOX.equals(style)) {
setNotification(notId, message);
builder.setContentText(fromHtml(message));
ArrayList<String> messageList = getMessageList(notId);
Integer sizeList = messageList.size();
if (sizeList > 1) {
String sizeListMessage = sizeList.toString();
String stacking = sizeList + " more";
if (extras.getString(SUMMARY_TEXT) != null) {
stacking = extras.getString(SUMMARY_TEXT);
stacking = stacking.replace("%n%", sizeListMessage);
}
Notification.InboxStyle notificationInbox = new Notification.InboxStyle()
.setBigContentTitle(fromHtml(extras.getString(TITLE)))
.setSummaryText(fromHtml(stacking));
for (int i = messageList.size() - 1; i >= 0; i--) {
notificationInbox.addLine(fromHtml(messageList.get(i)));
}
builder.setStyle(notificationInbox);
} else {
Notification.BigTextStyle bigText = new Notification.BigTextStyle();
if (message != null) {
bigText.bigText(fromHtml(message));
bigText.setBigContentTitle(fromHtml(extras.getString(TITLE)));
builder.setStyle(bigText);
}
}
} else if (STYLE_PICTURE.equals(style)) {
setNotification(notId, "");
Notification.BigPictureStyle bigPicture = new Notification.BigPictureStyle();
bigPicture.bigPicture(getBitmapFromURL(extras.getString(PICTURE)));
bigPicture.setBigContentTitle(fromHtml(extras.getString(TITLE)));
bigPicture.setSummaryText(fromHtml(extras.getString(SUMMARY_TEXT)));
builder.setContentTitle(fromHtml(extras.getString(TITLE)));
builder.setContentText(fromHtml(message));
builder.setStyle(bigPicture);
} else {
setNotification(notId, "");
Notification.BigTextStyle bigText = new Notification.BigTextStyle();
if (message != null) {
builder.setContentText(fromHtml(message));
bigText.bigText(fromHtml(message));
bigText.setBigContentTitle(fromHtml(extras.getString(TITLE)));
String summaryText = extras.getString(SUMMARY_TEXT);
if (summaryText != null) {
bigText.setSummaryText(fromHtml(summaryText));
}
builder.setStyle(bigText);
}
}
}
private void setNotificationSound(Context context, Bundle extras,
NotificationCompat.Builder builder) {
String soundname = extras.getString(SOUNDNAME);
if (soundname == null) {
soundname = extras.getString(SOUND);
}
if (SOUND_RINGTONE.equals(soundname)) {
builder.setSound(android.provider.Settings.System.DEFAULT_RINGTONE_URI);
} else if (soundname != null && !soundname.contentEquals(SOUND_DEFAULT)) {
Uri sound = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.getPackageName() + "/raw/" + soundname);
Log.d(LOG_TAG, sound.toString());
builder.setSound(sound);
} else {
builder.setSound(android.provider.Settings.System.DEFAULT_NOTIFICATION_URI);
}
}
@RequiresApi(api = 26)
private void setNotificationSound(Context context, Bundle extras,
NotificationCompat.Builder mBuilder) {
NotificationChannel channel) {
String soundname = extras.getString(SOUNDNAME);
if (soundname == null) {
soundname = extras.getString(SOUND);
}
if (SOUND_RINGTONE.equals(soundname)) {
mBuilder.setSound(android.provider.Settings.System.DEFAULT_RINGTONE_URI);
channel.setSound(android.provider.Settings.System.DEFAULT_RINGTONE_URI,
Notification.AUDIO_ATTRIBUTES_DEFAULT);
} else if (soundname != null && !soundname.contentEquals(SOUND_DEFAULT)) {
Uri sound = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.getPackageName() + "/raw/" + soundname);
Log.d(LOG_TAG, sound.toString());
mBuilder.setSound(sound);
channel.setSound(sound, Notification.AUDIO_ATTRIBUTES_DEFAULT);
} else {
channel.setSound(android.provider.Settings.System.DEFAULT_NOTIFICATION_URI,
Notification.AUDIO_ATTRIBUTES_DEFAULT);
}
}
private void setNotificationLedColor(Bundle extras, NotificationCompat.Builder builder) {
String ledColor = extras.getString(LED_COLOR);
if (ledColor == null) {
return;
}
// Converts parse Int Array from ledColor
String[] items = ledColor.replaceAll("\\[", "").replaceAll("\\]", "").split(",");
int[] results = new int[items.length];
for (int i = 0; i < items.length; i++) {
try {
results[i] = Integer.parseInt(items[i].trim());
} catch (NumberFormatException nfe) {
}
}
if (results.length == 4) {
builder.setLights(Color.argb(results[0], results[1], results[2], results[3]), 500, 500);
} else {
mBuilder.setSound(android.provider.Settings.System.DEFAULT_NOTIFICATION_URI);
Log.e(LOG_TAG, "ledColor parameter must be an array of length == 4 (ARGB)");
}
}
private void setNotificationLedColor(Bundle extras, NotificationCompat.Builder mBuilder) {
@RequiresApi(api = 26)
private void setNotificationLedColor(Bundle extras, NotificationChannel channel) {
String ledColor = extras.getString(LED_COLOR);
if (ledColor == null) {
return;
......@@ -466,14 +676,14 @@ public class PushNotificationHandler implements PushConstants {
}
if (results.length == 4) {
mBuilder.setLights(Color.argb(results[0], results[1], results[2], results[3]), 500, 500);
channel.setLightColor(Color.argb(results[0], results[1], results[2], results[3]));
} else {
Log.e(LOG_TAG, "ledColor parameter must be an array of length == 4 (ARGB)");
}
}
private void setNotificationPriority(Bundle extras, NotificationCompat.Builder mBuilder) {
private void setNotificationPriority(Bundle extras, NotificationCompat.Builder builder) {
String priorityStr = extras.getString(PRIORITY);
if (priorityStr == null) {
return;
......@@ -483,7 +693,28 @@ public class PushNotificationHandler implements PushConstants {
Integer priority = Integer.parseInt(priorityStr);
if (priority >= NotificationCompat.PRIORITY_MIN
&& priority <= NotificationCompat.PRIORITY_MAX) {
mBuilder.setPriority(priority);
builder.setPriority(priority);
} else {
Log.e(LOG_TAG, "Priority parameter must be between -2 and 2");
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
@RequiresApi(api = 26)
private void setNotificationImportance(Bundle extras, NotificationChannel channel) {
String priorityStr = extras.getString(PRIORITY);
if (priorityStr == null) {
return;
}
try {
Integer priority = Integer.parseInt(priorityStr);
if (priority >= NotificationCompat.PRIORITY_MIN
&& priority <= NotificationCompat.PRIORITY_MAX) {
channel.setImportance(priority);
} else {
Log.e(LOG_TAG, "Priority parameter must be between -2 and 2");
}
......@@ -494,14 +725,14 @@ public class PushNotificationHandler implements PushConstants {
}
private void setNotificationLargeIcon(Context context, Bundle extras, String packageName,
Resources resources, NotificationCompat.Builder mBuilder) {
Resources resources, NotificationCompat.Builder builder) {
String gcmLargeIcon = extras.getString(IMAGE); // from gcm
if (gcmLargeIcon == null || "".equals(gcmLargeIcon)) {
return;
}
if (gcmLargeIcon.startsWith("http://") || gcmLargeIcon.startsWith("https://")) {
mBuilder.setLargeIcon(getBitmapFromURL(gcmLargeIcon));
builder.setLargeIcon(getBitmapFromURL(gcmLargeIcon));
Log.d(LOG_TAG, "using remote large-icon from gcm");
} else {
AssetManager assetManager = context.getAssets();
......@@ -509,13 +740,13 @@ public class PushNotificationHandler implements PushConstants {
try {
istr = assetManager.open(gcmLargeIcon);
Bitmap bitmap = BitmapFactory.decodeStream(istr);
mBuilder.setLargeIcon(bitmap);
builder.setLargeIcon(bitmap);
Log.d(LOG_TAG, "using assets large-icon from gcm");
} catch (IOException e) {
int largeIconId = resources.getIdentifier(gcmLargeIcon, DRAWABLE, packageName);
if (largeIconId != 0) {
Bitmap largeIconBitmap = BitmapFactory.decodeResource(resources, largeIconId);
mBuilder.setLargeIcon(largeIconBitmap);
builder.setLargeIcon(largeIconBitmap);
Log.d(LOG_TAG, "using resources large-icon from gcm");
} else {
Log.d(LOG_TAG, "Not setting large icon");
......@@ -524,8 +755,59 @@ public class PushNotificationHandler implements PushConstants {
}
}
private void setNotificationSmallIcon(Context context, Bundle extras, String packageName,
Resources resources, NotificationCompat.Builder mBuilder,
private void setNotificationLargeIcon(Context context, Bundle extras, String packageName,
Resources resources, Notification.Builder builder) {
String gcmLargeIcon = extras.getString(IMAGE); // from gcm
if (gcmLargeIcon == null || "".equals(gcmLargeIcon)) {
return;
}
if (gcmLargeIcon.startsWith("http://") || gcmLargeIcon.startsWith("https://")) {
builder.setLargeIcon(getBitmapFromURL(gcmLargeIcon));
Log.d(LOG_TAG, "using remote large-icon from gcm");
} else {
AssetManager assetManager = context.getAssets();
InputStream istr;
try {
istr = assetManager.open(gcmLargeIcon);
Bitmap bitmap = BitmapFactory.decodeStream(istr);
builder.setLargeIcon(bitmap);
Log.d(LOG_TAG, "using assets large-icon from gcm");
} catch (IOException e) {
int largeIconId = resources.getIdentifier(gcmLargeIcon, DRAWABLE, packageName);
if (largeIconId != 0) {
Bitmap largeIconBitmap = BitmapFactory.decodeResource(resources, largeIconId);
builder.setLargeIcon(largeIconBitmap);
Log.d(LOG_TAG, "using resources large-icon from gcm");
} else {
Log.d(LOG_TAG, "Not setting large icon");
}
}
}
}
private void setNotificationSmallIcon(Bundle extras, String packageName,
Resources resources, NotificationCompat.Builder builder,
String localIcon) {
int iconId = 0;
String icon = extras.getString(ICON);
if (icon != null && !"".equals(icon)) {
iconId = resources.getIdentifier(icon, DRAWABLE, packageName);
Log.d(LOG_TAG, "using icon from plugin options");
} else if (localIcon != null && !"".equals(localIcon)) {
iconId = resources.getIdentifier(localIcon, DRAWABLE, packageName);
Log.d(LOG_TAG, "using icon from plugin options");
}
if (iconId == 0) {
Log.d(LOG_TAG, "no icon resource found - using default icon");
iconId = resources.getIdentifier("rocket_chat_notification", DRAWABLE, packageName);
}
builder.setSmallIcon(iconId);
}
private void setNotificationSmallIcon(Bundle extras, String packageName,
Resources resources, Notification.Builder builder,
String localIcon) {
int iconId = 0;
String icon = extras.getString(ICON);
......@@ -540,10 +822,10 @@ public class PushNotificationHandler implements PushConstants {
Log.d(LOG_TAG, "no icon resource found - using default icon");
iconId = resources.getIdentifier("rocket_chat_notification", DRAWABLE, packageName);
}
mBuilder.setSmallIcon(iconId);
builder.setSmallIcon(iconId);
}
private void setNotificationIconColor(String color, NotificationCompat.Builder mBuilder,
private void setNotificationIconColor(String color, NotificationCompat.Builder builder,
String localIconColor) {
int iconColor = 0;
if (color != null && !"".equals(color)) {
......@@ -560,7 +842,7 @@ public class PushNotificationHandler implements PushConstants {
}
}
if (iconColor != 0) {
mBuilder.setColor(iconColor);
builder.setColor(iconColor);
}
}
......
package chat.rocket.android.renderer;
import android.content.Context;
import android.view.View;
import android.widget.TextView;
import chat.rocket.android.R;
import chat.rocket.android.helper.DateTime;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.widget.AbsoluteUrl;
import chat.rocket.android.widget.RocketChatAvatar;
import chat.rocket.android.widget.message.RocketChatMessageAttachmentsLayout;
import chat.rocket.android.widget.message.RocketChatMessageLayout;
import chat.rocket.android.widget.message.RocketChatMessageUrlsLayout;
import chat.rocket.core.SyncState;
import chat.rocket.core.models.Attachment;
import chat.rocket.core.models.Message;
import chat.rocket.core.models.WebContent;
import java.util.List;
/**
* Renderer for RealmMessage model.
*/
public class MessageRenderer extends AbstractRenderer<Message> {
private final UserRenderer userRenderer;
private final boolean autoloadImages;
public MessageRenderer(Context context, Message message, boolean autoloadImages) {
super(context, message);
userRenderer = new UserRenderer(context, message.getUser());
this.autoloadImages = autoloadImages;
}
/**
* show Avatar image.
*/
public MessageRenderer avatarInto(RocketChatAvatar rocketChatAvatar, AbsoluteUrl absoluteUrl) {
if (!shouldHandle(rocketChatAvatar)) {
return this;
}
if (TextUtils.isEmpty(object.getAvatar())) {
userRenderer.avatarInto(rocketChatAvatar, absoluteUrl);
// Avatar from oauth providers
} else {
rocketChatAvatar.loadImage(object.getAvatar());
}
return this;
}
/**
* show Username in textView.
*/
public MessageRenderer usernameInto(TextView usernameTextView, TextView subUsernameTextView) {
if (TextUtils.isEmpty(object.getAlias())) {
userRenderer.usernameInto(usernameTextView);
if (subUsernameTextView != null) {
subUsernameTextView.setVisibility(View.GONE);
}
} else {
aliasAndUsernameInto(usernameTextView, subUsernameTextView);
}
return this;
}
/**
* show timestamp in textView.
*/
public MessageRenderer timestampInto(TextView textView) {
if (!shouldHandle(textView)) {
return this;
}
switch (object.getSyncState()) {
case SyncState.SYNCING:
textView.setText(R.string.sending);
break;
case SyncState.NOT_SYNCED:
textView.setText(R.string.not_synced);
break;
case SyncState.FAILED:
textView.setText(R.string.failed_to_sync);
break;
default:
textView.setText(DateTime.fromEpocMs(object.getTimestamp(), DateTime.Format.TIME));
break;
}
return this;
}
/**
* show body in RocketChatMessageLayout.
*/
public MessageRenderer bodyInto(RocketChatMessageLayout rocketChatMessageLayout) {
if (!shouldHandle(rocketChatMessageLayout)) {
return this;
}
rocketChatMessageLayout.setText(object.getMessage());
return this;
}
/**
* show urls in RocketChatMessageUrlsLayout.
*/
public MessageRenderer urlsInto(RocketChatMessageUrlsLayout urlsLayout) {
if (!shouldHandle(urlsLayout)) {
return this;
}
List<WebContent> webContents = object.getWebContents();
if (webContents == null || webContents.size() == 0) {
urlsLayout.setVisibility(View.GONE);
} else {
urlsLayout.setVisibility(View.VISIBLE);
urlsLayout.setUrls(webContents, autoloadImages);
}
return this;
}
/**
* show urls in RocketChatMessageUrlsLayout.
*/
public MessageRenderer attachmentsInto(RocketChatMessageAttachmentsLayout attachmentsLayout, AbsoluteUrl absoluteUrl) {
if (!shouldHandle(attachmentsLayout)) {
return this;
}
List<Attachment> attachments = object.getAttachments();
if (attachments == null || attachments.size() == 0) {
attachmentsLayout.setVisibility(View.GONE);
} else {
attachmentsLayout.setVisibility(View.VISIBLE);
attachmentsLayout.setAbsoluteUrl(absoluteUrl);
attachmentsLayout.setAttachments(attachments, autoloadImages);
}
return this;
}
private void aliasAndUsernameInto(TextView aliasTextView, TextView usernameTextView) {
if (shouldHandle(aliasTextView)) {
aliasTextView.setText(object.getAlias());
}
if (shouldHandle(usernameTextView)) {
if (object.getUser() != null) {
usernameTextView.setText("@" + object.getUser().getUsername());
usernameTextView.setVisibility(View.VISIBLE);
} else {
usernameTextView.setVisibility(View.GONE);
}
}
}
}
\ No newline at end of file
package chat.rocket.android.renderer
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.helper.DateTime
import chat.rocket.android.helper.RocketChatUserAvatar
import chat.rocket.android.widget.AbsoluteUrl
import chat.rocket.android.widget.RocketChatAvatar
import chat.rocket.android.widget.message.RocketChatMessageAttachmentsLayout
import chat.rocket.android.widget.message.RocketChatMessageLayout
import chat.rocket.android.widget.message.RocketChatMessageUrlsLayout
import chat.rocket.core.SyncState
import chat.rocket.core.models.Message
class MessageRenderer(val message: Message, val autoLoadImage: Boolean) {
/**
* Show user's avatar image in RocketChatAvatar widget.
*/
fun showAvatar(rocketChatAvatarWidget: RocketChatAvatar, hostname: String, userNotFoundAvatarImageView: ImageView) {
if (message.avatar != null) {
// Load user's avatar image from Oauth provider URI.
rocketChatAvatarWidget.loadImage(message.avatar)
} else {
val username: String? = message.user?.username
if (username != null) {
// Load user's avatar image from Rocket.Chat URI.
rocketChatAvatarWidget.loadImage(RocketChatUserAvatar(hostname, username).imageUri)
userNotFoundAvatarImageView.visibility = View.GONE
rocketChatAvatarWidget.visibility = View.VISIBLE
} else {
rocketChatAvatarWidget.visibility = View.GONE
userNotFoundAvatarImageView.visibility = View.VISIBLE
}
}
}
/**
* Show username in textView.
*/
fun showUsername(usernameTextView: TextView, subUsernameTextView: TextView?) {
if (message.alias == null) {
usernameTextView.text = message.user?.username ?: usernameTextView.context.getText(R.string.user_not_found)
} else {
usernameTextView.text = message.alias
val username: String? = message.user?.username
if (username != null && subUsernameTextView != null) {
subUsernameTextView.text = subUsernameTextView.context.getString(R.string.sub_username, username)
subUsernameTextView.visibility = View.VISIBLE
}
}
}
/**
* Show timestamp or message state in textView.
*/
fun showTimestampOrMessageState(textView: TextView) {
when (message.syncState) {
SyncState.SYNCING -> textView.text = textView.context.getText(R.string.sending)
SyncState.NOT_SYNCED -> textView.text = textView.context.getText(R.string.not_synced)
SyncState.FAILED -> textView.text = textView.context.getText(R.string.failed_to_sync)
else -> textView.text = DateTime.fromEpocMs(message.timestamp, DateTime.Format.TIME)
}
}
/**
* Show body in RocketChatMessageLayout widget.
*/
fun showBody(rocketChatMessageLayout: RocketChatMessageLayout) {
rocketChatMessageLayout.setText(message.message)
}
/**
* Show urls in RocketChatMessageUrlsLayout widget.
*/
fun showUrl(rocketChatMessageUrlsLayout: RocketChatMessageUrlsLayout) {
val webContents = message.webContents
if (webContents == null || webContents.isEmpty()) {
rocketChatMessageUrlsLayout.visibility = View.GONE
} else {
rocketChatMessageUrlsLayout.setUrls(webContents, autoLoadImage)
rocketChatMessageUrlsLayout.visibility = View.VISIBLE
}
}
/**
* show attachments in RocketChatMessageAttachmentsLayout widget.
*/
fun showAttachment(rocketChatMessageAttachmentsLayout: RocketChatMessageAttachmentsLayout, absoluteUrl: AbsoluteUrl?) {
val attachments = message.attachments
if (attachments == null || attachments.isEmpty()) {
rocketChatMessageAttachmentsLayout.visibility = View.GONE
} else {
rocketChatMessageAttachmentsLayout.setAbsoluteUrl(absoluteUrl)
rocketChatMessageAttachmentsLayout.setAttachments(attachments, autoLoadImage)
rocketChatMessageAttachmentsLayout.visibility = View.VISIBLE
}
}
}
\ No newline at end of file
package chat.rocket.android.renderer;
import android.support.annotation.DrawableRes;
import chat.rocket.android.R;
import chat.rocket.android.widget.helper.UserStatusProvider;
import chat.rocket.core.models.User;
public class RocketChatUserStatusProvider implements UserStatusProvider {
private static RocketChatUserStatusProvider instance;
private RocketChatUserStatusProvider() {
}
public static RocketChatUserStatusProvider getInstance() {
if (instance == null) {
instance = new RocketChatUserStatusProvider();
}
return instance;
}
@Override
@DrawableRes
public int getStatusResId(String status) {
if (User.STATUS_ONLINE.equals(status)) {
return R.drawable.userstatus_online;
} else if (User.STATUS_AWAY.equals(status)) {
return R.drawable.userstatus_away;
} else if (User.STATUS_BUSY.equals(status)) {
return R.drawable.userstatus_busy;
} else if (User.STATUS_OFFLINE.equals(status)) {
return R.drawable.userstatus_offline;
}
// unknown status is rendered as "offline" status.
return R.drawable.userstatus_offline;
}
}
package chat.rocket.android.renderer
import chat.rocket.android.R
import chat.rocket.android.widget.helper.UserStatusProvider
import chat.rocket.core.models.User
object RocketChatUserStatusProvider: UserStatusProvider {
override fun getStatusResId(status: String?): Int {
var userStatusDrawableId = R.drawable.userstatus_offline
when (status) {
User.STATUS_ONLINE -> {
userStatusDrawableId = R.drawable.userstatus_online
}
User.STATUS_AWAY -> {
userStatusDrawableId = R.drawable.userstatus_away
}
User.STATUS_BUSY -> {
userStatusDrawableId = R.drawable.userstatus_busy
}
}
return userStatusDrawableId
}
}
\ No newline at end of file
package chat.rocket.android.renderer;
import android.content.Context;
import android.widget.ImageView;
import android.widget.TextView;
import chat.rocket.android.helper.Avatar;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.widget.AbsoluteUrl;
import chat.rocket.android.widget.RocketChatAvatar;
import chat.rocket.core.models.User;
/**
* Renderer for RealmUser model.
*/
public class UserRenderer extends AbstractRenderer<User> {
public UserRenderer(Context context, User user) {
super(context, user);
}
/**
* show Avatar image
*/
public UserRenderer avatarInto(RocketChatAvatar rocketChatAvatar, AbsoluteUrl absoluteUrl) {
if (!shouldHandle(rocketChatAvatar)) {
return this;
}
if (!TextUtils.isEmpty(object.getUsername())) {
new Avatar(absoluteUrl, object.getUsername())
.into(rocketChatAvatar);
}
return this;
}
/**
* show Username in textView
*/
public UserRenderer usernameInto(TextView textView) {
if (!shouldHandle(textView)) {
return this;
}
textView.setText(object.getUsername());
return this;
}
/**
* show user's status color into imageView.
*/
public UserRenderer statusColorInto(ImageView imageView) {
if (!shouldHandle(imageView)) {
return this;
}
String status = object.getStatus();
imageView.setImageResource(RocketChatUserStatusProvider.getInstance().getStatusResId(status));
return this;
}
}
package chat.rocket.android.renderer
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.helper.RocketChatUserAvatar
import chat.rocket.android.widget.RocketChatAvatar
import chat.rocket.core.models.User
class UserRenderer(val user: User) {
/**
* Show user's avatar image in RocketChatAvatar widget.
*/
fun showAvatar(rocketChatAvatarWidget: RocketChatAvatar, hostname: String) {
val username: String? = user.username
if (username != null) {
rocketChatAvatarWidget.loadImage(RocketChatUserAvatar(hostname, username).imageUri)
} else {
rocketChatAvatarWidget.visibility = View.GONE
}
}
/**
* Show username in textView.
*/
fun showUsername(textView: TextView) {
textView.text = user.username ?: textView.context.getText(R.string.user_not_found)
}
/**
* Show user's status color in imageView.
*/
fun showStatusColor(imageView: ImageView) {
val userStatus: String? = user.status
if (userStatus != null) {
imageView.setImageResource(RocketChatUserStatusProvider.getStatusResId(userStatus))
} else {
imageView.visibility = View.GONE
}
}
}
\ No newline at end of file
......@@ -23,4 +23,6 @@ import chat.rocket.core.models.ServerInfo;
void notifyConnectionEstablished(String hostname, String session);
void notifyConnectionLost(String hostname, int reason);
void notifyConnecting(String hostname);
}
......@@ -133,6 +133,14 @@ import rx.subjects.PublishSubject;
new ServerConnectivity(hostname, ServerConnectivity.STATE_DISCONNECTED));
}
@DebugLog
@Override
public void notifyConnecting(String hostname) {
serverConnectivityList.put(hostname, ServerConnectivity.STATE_CONNECTING);
connectivitySubject.onNext(
new ServerConnectivity(hostname, ServerConnectivity.STATE_CONNECTING));
}
@Override
public Observable<ServerConnectivity> getServerConnectivityAsObservable() {
return Observable.concat(Observable.from(getCurrentConnectivityList()), connectivitySubject);
......
......@@ -9,6 +9,7 @@ import android.os.IBinder;
import android.support.annotation.Nullable;
import java.util.HashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import chat.rocket.android.activity.MainActivity;
......@@ -24,6 +25,7 @@ public class RocketChatService extends Service implements ConnectivityServiceInt
private ConnectivityManagerInternal connectivityManager;
private HashMap<String, RocketChatWebSocketThread> webSocketThreads;
private Semaphore webSocketThreadLock = new Semaphore(1);
public class LocalBinder extends Binder {
ConnectivityServiceInterface getServiceInterface() {
......@@ -49,6 +51,7 @@ public class RocketChatService extends Service implements ConnectivityServiceInt
context.unbindService(serviceConnection);
}
@DebugLog
@Override
public void onCreate() {
super.onCreate();
......@@ -57,6 +60,7 @@ public class RocketChatService extends Service implements ConnectivityServiceInt
webSocketThreads = new HashMap<>();
}
@DebugLog
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
connectivityManager.ensureConnections();
......@@ -106,12 +110,17 @@ public class RocketChatService extends Service implements ConnectivityServiceInt
@DebugLog
private Single<RocketChatWebSocketThread> getOrCreateWebSocketThread(String hostname) {
return Single.defer(() -> {
webSocketThreadLock.acquire();
if (webSocketThreads.containsKey(hostname)) {
RocketChatWebSocketThread thread = webSocketThreads.get(hostname);
webSocketThreadLock.release();
return Single.just(thread);
}
return RocketChatWebSocketThread.getStarted(getApplicationContext(), hostname)
.doOnSuccess(thread -> webSocketThreads.put(hostname, thread));
.doOnSuccess(thread -> {
webSocketThreads.put(hostname, thread);
webSocketThreadLock.release();
});
});
}
......
......@@ -3,24 +3,26 @@ package chat.rocket.android.service;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import org.json.JSONObject;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import bolts.Task;
import chat.rocket.android.RocketChatCache;
import chat.rocket.android.api.DDPClientWrapper;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.helper.LogIfError;
import chat.rocket.android.helper.RxHelper;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.log.RCLog;
import chat.rocket.core.models.ServerInfo;
import chat.rocket.persistence.realm.models.internal.RealmSession;
import chat.rocket.persistence.realm.RealmHelper;
import chat.rocket.persistence.realm.RealmStore;
import chat.rocket.android.service.ddp.base.ActiveUsersSubscriber;
import chat.rocket.android.service.ddp.base.LoginServiceConfigurationSubscriber;
import chat.rocket.android.service.ddp.base.UserDataSubscriber;
import chat.rocket.android.service.ddp.stream.StreamRoomMessage;
import chat.rocket.android.service.observer.CurrentUserObserver;
import chat.rocket.android.service.observer.FileUploadingToUrlObserver;
import chat.rocket.android.service.observer.FileUploadingWithUfsObserver;
......@@ -32,8 +34,13 @@ import chat.rocket.android.service.observer.NewMessageObserver;
import chat.rocket.android.service.observer.PushSettingsObserver;
import chat.rocket.android.service.observer.SessionObserver;
import chat.rocket.android.service.observer.TokenLoginObserver;
import chat.rocket.core.models.ServerInfo;
import chat.rocket.persistence.realm.RealmHelper;
import chat.rocket.persistence.realm.RealmStore;
import chat.rocket.persistence.realm.models.internal.RealmSession;
import hugo.weaving.DebugLog;
import rx.Single;
import rx.subscriptions.CompositeSubscription;
/**
* Thread for handling WebSocket connection.
......@@ -62,6 +69,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
private final ArrayList<Registrable> listeners = new ArrayList<>();
private DDPClientWrapper ddpClient;
private boolean listenersRegistered;
private RocketChatCache rocketChatCache;
private final DDPClientRef ddpClientRef = new DDPClientRef() {
@Override
public DDPClientWrapper get() {
......@@ -69,7 +77,6 @@ public class RocketChatWebSocketThread extends HandlerThread {
}
};
private static class KeepAliveTimer {
private long lastTime;
private final long thresholdMs;
......@@ -96,6 +103,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
this.hostname = hostname;
this.realmHelper = RealmStore.getOrCreate(hostname);
this.connectivityManager = ConnectivityManager.getInstanceForInternal(appContext);
this.rocketChatCache = new RocketChatCache(appContext);
}
/**
......@@ -147,7 +155,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
return Single.fromEmitter(emitter -> {
new Handler(getLooper()).post(() -> {
RCLog.d("thread %s: terminated()", Thread.currentThread().getId());
unregisterListeners();
unregisterListenersAndClose();
connectivityManager.notifyConnectionLost(hostname,
ConnectivityManagerInternal.REASON_CLOSED_BY_USER);
RocketChatWebSocketThread.super.quit();
......@@ -196,9 +204,9 @@ public class RocketChatWebSocketThread extends HandlerThread {
public void run() {
ddpClient.ping().continueWith(task -> {
if (task.isFaulted()) {
RCLog.e(task.getError());
Exception error = task.getError();
RCLog.e(error);
emitter.onSuccess(false);
ddpClient.close();
} else {
keepAliveTimer.update();
emitter.onSuccess(true);
......@@ -232,9 +240,26 @@ public class RocketChatWebSocketThread extends HandlerThread {
// handling WebSocket#onClose() callback.
task.getResult().client.getOnCloseCallback().onSuccess(_task -> {
if (listenersRegistered) {
terminate();
}
ddpClient.close();
forceInvalidateTokens();
connectivityManager.notifyConnecting(hostname);
// Needed to use subscriptions because of legacy code.
// TODO: Should update to RxJava 2
final CompositeSubscription subscriptions = new CompositeSubscription();
subscriptions.add(
connect().retryWhen(RxHelper.exponentialBackoff(3, 500, TimeUnit.MILLISECONDS))
.subscribe(
connected -> {
if (!connected) {
connectivityManager.notifyConnectionLost(
hostname, ConnectivityManagerInternal.REASON_NETWORK_ERROR
);
}
subscriptions.clear();
},
err -> logErrorAndUnsubscribe(subscriptions, err)
)
);
return null;
});
......@@ -265,6 +290,11 @@ public class RocketChatWebSocketThread extends HandlerThread {
}));
}
private void logErrorAndUnsubscribe(CompositeSubscription subscriptions, Throwable err) {
RCLog.e(err);
subscriptions.clear();
}
@DebugLog
private Single<Boolean> connect() {
return connectDDPClient()
......@@ -284,7 +314,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
return new MethodCallHelper(realmHelper, ddpClientRef).getPermissions();
}
//@DebugLog
@DebugLog
private void registerListeners() {
if (!Thread.currentThread().getName().equals("RC_thread_" + hostname)) {
// execute in Looper.
......@@ -293,7 +323,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
}
if (listenersRegistered) {
return;
unregisterListeners();
}
listenersRegistered = true;
......@@ -308,12 +338,30 @@ public class RocketChatWebSocketThread extends HandlerThread {
registrable.register();
listeners.add(registrable);
}
// Register for room stream messages
String roomId = rocketChatCache.getSelectedRoomId();
if (roomId != null && !roomId.isEmpty()) {
StreamRoomMessage streamRoomMessage = new StreamRoomMessage(
appContext, hostname, realmHelper, ddpClientRef, roomId
);
streamRoomMessage.register();
listeners.add(streamRoomMessage);
}
} catch (Exception exception) {
RCLog.w(exception, "Failed to register listeners!!");
}
}
}
@DebugLog
private void unregisterListenersAndClose() {
unregisterListeners();
if (ddpClient != null) {
ddpClient.close();
ddpClient = null;
}
}
@DebugLog
private void unregisterListeners() {
Iterator<Registrable> iterator = listeners.iterator();
......@@ -323,9 +371,5 @@ public class RocketChatWebSocketThread extends HandlerThread {
iterator.remove();
}
listenersRegistered = false;
if (ddpClient != null) {
ddpClient.close();
ddpClient = null;
}
}
}
......@@ -6,7 +6,7 @@ package chat.rocket.android.service;
public class ServerConnectivity {
public static final int STATE_CONNECTED = 1;
public static final int STATE_DISCONNECTED = 2;
/*package*/ static final int STATE_CONNECTING = 3;
public static final int STATE_CONNECTING = 3;
/*package*/ static final int STATE_DISCONNECTING = 4;
public final String hostname;
......
......@@ -185,7 +185,7 @@ public abstract class AbstractDDPDocEventSubscriber implements Registrable {
if (rxSubscription != null) {
rxSubscription.dispose();
}
if (!TextUtils.isEmpty(subscriptionId)) {
if (!TextUtils.isEmpty(subscriptionId) && ddpClientRef.get() != null) {
ddpClientRef.get().unsubscribe(subscriptionId).continueWith(new LogIfError());
}
}
......
......@@ -2,6 +2,7 @@ package chat.rocket.android.service.internal;
import android.content.Context;
import chat.rocket.android.log.RCLog;
import io.reactivex.disposables.CompositeDisposable;
import chat.rocket.android.RocketChatCache;
......@@ -47,7 +48,7 @@ public abstract class AbstractRocketChatCacheObserver implements Registrable {
compositeDisposable.add(
new RocketChatCache(context)
.getSelectedRoomIdPublisher()
.subscribe(this::updateRoomIdWith)
.subscribe(this::updateRoomIdWith, RCLog::e)
);
}
......
package chat.rocket.android.service.observer;
import android.content.Context;
import io.realm.Realm;
import io.realm.RealmResults;
import org.json.JSONObject;
import java.util.List;
import chat.rocket.android.helper.CheckSum;
import chat.rocket.android.helper.LogIfError;
import chat.rocket.android_ddp.rx.RxWebSocketCallback;
import chat.rocket.core.SyncState;
import chat.rocket.persistence.realm.models.internal.MethodCall;
import chat.rocket.persistence.realm.RealmHelper;
import chat.rocket.android.service.DDPClientRef;
import chat.rocket.android_ddp.DDPClientCallback;
import chat.rocket.core.SyncState;
import chat.rocket.persistence.realm.RealmHelper;
import chat.rocket.persistence.realm.models.internal.MethodCall;
import io.realm.Realm;
import io.realm.RealmResults;
/**
* Observing MethodCall record, executing RPC if needed.
......@@ -117,9 +118,6 @@ public class MethodCallObserver extends AbstractModelObserver<MethodCall> {
} else if (exception instanceof DDPClientCallback.RPC.Timeout) {
// temp "fix"- we need to rewrite the connection layer a bit
errMessage = "{\"message\": \"Connection Timeout\"}";
} else if (exception instanceof RxWebSocketCallback.Failure) {
// temp "fix"- we need to rewrite the connection layer a bit
errMessage = "{\"message\": \"Connection Failure\"}";
} else {
errMessage = exception.getMessage();
}
......
<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:pathData="M-0,0L24,0L24,24L-0,24L-0,0ZM11.963,17.661L12.036,17.661L17.162,17.661C17.162,13.975 13.979,13.915 13.178,12.982L13.082,12.474C14.095,11.946 14.807,10.699 14.807,9.245C14.807,7.313 13.55,5.746 12,5.746C10.45,5.746 9.193,7.313 9.193,9.245C9.193,10.712 9.917,11.966 10.944,12.486L10.86,12.933C10.129,13.913 6.837,13.912 6.837,17.661L11.963,17.661Z"
android:fillColor="#FF000000"/>
</vector>
\ No newline at end of file
......@@ -20,6 +20,14 @@
android:layout_height="32dp"
android:layout_margin="8dp" />
<ImageView
android:id="@+id/userNotFoundAvatarImageView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="8dp"
app:srcCompat="@drawable/ic_user_not_found_avatar_black_24dp"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
......@@ -51,7 +59,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.RocketChat.Message.SubUsername"
tools:text="\@John Doe" />
tools:text="\@John Doe"
android:visibility="gone" />
<Space
android:layout_width="@dimen/margin_8"
......
......@@ -19,6 +19,14 @@
android:layout_height="32dp"
android:layout_margin="8dp" />
<ImageView
android:id="@+id/userNotFoundAvatarImageView"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="8dp"
app:srcCompat="@drawable/ic_user_not_found_avatar_black_24dp"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
......
......@@ -14,20 +14,28 @@
<string name="start_of_conversation">Start of conversation</string>
<string name="users_of_room_title">Members List</string>
<string name="fmt_room_user_count">Total: %,d users</string>
<plurals name="fmt_room_user_count">
<item quantity="one">Total: %,d user</item>
<item quantity="other">Total: %,d users</item>
</plurals>
<string name="sending">Sending…</string>
<string name="not_synced">Not synced</string>
<string name="failed_to_sync">Failed to sync</string>
<string name="resend">Resend</string>
<string name="discard">Discard</string>
<string name="fmt_dialog_view_latest_message_title">New %d messages</string>
<plurals name="fmt_dialog_view_latest_message_title">
<item quantity="one">New %d message</item>
<item quantity="other">New %d messages</item>
</plurals>
<string name="dialog_view_latest_message_action">View</string>
<string name="file_uploading_title">Uploading…</string>
<string name="dialog_user_registration_email">Email</string>
<string name="dialog_user_registration_username">Username</string>
<string name="sub_username">\@%s</string>
<string name="user_not_found">User not found</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>
......
......@@ -7,7 +7,7 @@
machine:
environment:
ANDROID_HOME: /usr/local/android-sdk-linux
GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"'
GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx1536m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError"'
dependencies:
pre:
......
......@@ -2,7 +2,7 @@ package chat.rocket.persistence.realm.repositories;
import android.os.Looper;
import android.support.v4.util.Pair;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
......
......@@ -3,7 +3,7 @@ package chat.rocket.persistence.realm.repositories;
import android.os.Looper;
import android.support.v4.util.Pair;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
......
......@@ -2,7 +2,7 @@ package chat.rocket.persistence.realm.repositories;
import android.os.Looper;
import android.support.v4.util.Pair;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
......
......@@ -2,7 +2,7 @@ package chat.rocket.persistence.realm.repositories;
import android.os.Looper;
import android.support.v4.util.Pair;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
......
......@@ -2,7 +2,7 @@ package chat.rocket.persistence.realm.repositories;
import android.os.Looper;
import android.support.v4.util.Pair;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
......
......@@ -2,7 +2,7 @@ package chat.rocket.persistence.realm.repositories;
import android.os.Looper;
import android.support.v4.util.Pair;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
......
......@@ -2,7 +2,7 @@ package chat.rocket.persistence.realm.repositories;
import android.os.Looper;
import android.support.v4.util.Pair;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
......
......@@ -2,7 +2,7 @@ package chat.rocket.persistence.realm.repositories;
import android.os.Looper;
import android.support.v4.util.Pair;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
......
......@@ -2,7 +2,7 @@ package chat.rocket.persistence.realm.repositories;
import android.os.Looper;
import android.support.v4.util.Pair;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.realm.Case;
......
package chat.rocket.android.widget;
import android.content.Context;
import chat.rocket.android.widget.fresco.CustomImageFormatConfigurator;
import com.facebook.common.logging.FLog;
import com.facebook.drawee.backends.pipeline.DraweeConfig;
import com.facebook.drawee.backends.pipeline.Fresco;
......@@ -9,8 +9,11 @@ import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFact
import com.facebook.imagepipeline.core.ImagePipelineConfig;
import com.facebook.imagepipeline.listener.RequestListener;
import com.facebook.imagepipeline.listener.RequestLoggingListener;
import java.util.HashSet;
import java.util.Set;
import chat.rocket.android.widget.fresco.CustomImageFormatConfigurator;
import okhttp3.OkHttpClient;
public class RocketChatWidgets {
......
plugins {
id "org.jetbrains.kotlin.jvm" version "1.1.2-2"
id "org.jetbrains.kotlin.jvm" version "1.1.3-2"
}
apply plugin: 'idea'
......@@ -14,9 +14,9 @@ dependencies {
compile 'io.reactivex.rxjava2:rxjava:2.1.0'
compile 'com.fernandocejas:arrow:1.0.0'
compile 'com.hadisatrio:Optional:v1.0.1'
compile 'com.google.auto.value:auto-value:1.3'
compileOnly 'com.google.auto.value:auto-value:1.3'
kapt 'com.google.auto.value:auto-value:1.3'
kapt 'com.gabrielittner.auto.value:auto-value-with:1.0.0'
......
......@@ -6,7 +6,7 @@ import io.reactivex.Flowable
import io.reactivex.Single
import chat.rocket.core.repositories.UserRepository
import com.fernandocejas.arrow.optional.Optional
import com.hadisatrio.optional.Optional
import io.reactivex.functions.Function3
class CanCreateRoomInteractor(private val userRepository: UserRepository,
......
......@@ -5,7 +5,7 @@ import chat.rocket.core.PublicSettingsConstants
import chat.rocket.core.models.*
import chat.rocket.core.repositories.*
import chat.rocket.core.utils.Pair
import com.fernandocejas.arrow.optional.Optional
import com.hadisatrio.optional.Optional
import io.reactivex.Single
import io.reactivex.functions.Function4
......
package chat.rocket.core.interactors
import com.fernandocejas.arrow.optional.Optional
import com.hadisatrio.optional.Optional
import io.reactivex.Flowable
import io.reactivex.Single
import java.util.UUID
......
......@@ -5,7 +5,7 @@ import chat.rocket.core.models.Room
import chat.rocket.core.models.RoomRole
import chat.rocket.core.repositories.*
import chat.rocket.core.utils.Pair
import com.fernandocejas.arrow.optional.Optional
import com.hadisatrio.optional.Optional
import io.reactivex.Single
import io.reactivex.functions.BiFunction
......
package chat.rocket.core.interactors
import com.fernandocejas.arrow.optional.Optional
import com.hadisatrio.optional.Optional
import io.reactivex.Flowable
import io.reactivex.Single
......
package chat.rocket.core.repositories;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
......
package chat.rocket.core.repositories;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
......
package chat.rocket.core.repositories;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Single;
import chat.rocket.core.models.Permission;
......
package chat.rocket.core.repositories;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Single;
import chat.rocket.core.models.PublicSetting;
......
package chat.rocket.core.repositories;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
......
package chat.rocket.core.repositories;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Single;
import chat.rocket.core.models.Room;
......
package chat.rocket.core.repositories;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
......
package chat.rocket.core.repositories;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import io.reactivex.Single;
......
package chat.rocket.core.repositories;
import com.fernandocejas.arrow.optional.Optional;
import com.hadisatrio.optional.Optional;
import io.reactivex.Flowable;
import java.util.List;
......
......@@ -7,7 +7,7 @@ import chat.rocket.core.models.PublicSetting
import chat.rocket.core.models.Room
import chat.rocket.core.models.User
import chat.rocket.core.repositories.*
import com.fernandocejas.arrow.optional.Optional
import com.hadisatrio.optional.Optional
import io.reactivex.Flowable
import io.reactivex.Single
import io.reactivex.observers.TestObserver
......
......@@ -4,7 +4,7 @@ import chat.rocket.core.models.*
import chat.rocket.core.repositories.PermissionRepository
import chat.rocket.core.repositories.RoomRoleRepository
import chat.rocket.core.repositories.UserRepository
import com.fernandocejas.arrow.optional.Optional
import com.hadisatrio.optional.Optional
import io.reactivex.Flowable
import io.reactivex.Single
import io.reactivex.observers.TestObserver
......
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