ChatController.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. package com.nextcloud.talk.controllers;
  21. import android.os.Bundle;
  22. import android.support.annotation.NonNull;
  23. import android.support.annotation.Nullable;
  24. import android.support.v7.widget.LinearLayoutManager;
  25. import android.support.v7.widget.RecyclerView;
  26. import android.view.LayoutInflater;
  27. import android.view.MenuItem;
  28. import android.view.View;
  29. import android.view.ViewGroup;
  30. import android.widget.ImageView;
  31. import com.bumptech.glide.load.engine.DiskCacheStrategy;
  32. import com.bumptech.glide.load.resource.bitmap.CircleCrop;
  33. import com.bumptech.glide.request.RequestOptions;
  34. import com.nextcloud.talk.R;
  35. import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder;
  36. import com.nextcloud.talk.api.NcApi;
  37. import com.nextcloud.talk.application.NextcloudTalkApplication;
  38. import com.nextcloud.talk.controllers.base.BaseController;
  39. import com.nextcloud.talk.models.database.UserEntity;
  40. import com.nextcloud.talk.models.json.call.CallOverall;
  41. import com.nextcloud.talk.models.json.chat.ChatMessage;
  42. import com.nextcloud.talk.models.json.chat.ChatOverall;
  43. import com.nextcloud.talk.models.json.generic.GenericOverall;
  44. import com.nextcloud.talk.utils.ApiUtils;
  45. import com.nextcloud.talk.utils.bundle.BundleKeys;
  46. import com.nextcloud.talk.utils.database.user.UserUtils;
  47. import com.nextcloud.talk.utils.glide.GlideApp;
  48. import com.stfalcon.chatkit.commons.ImageLoader;
  49. import com.stfalcon.chatkit.messages.MessageInput;
  50. import com.stfalcon.chatkit.messages.MessagesList;
  51. import com.stfalcon.chatkit.messages.MessagesListAdapter;
  52. import com.stfalcon.chatkit.utils.DateFormatter;
  53. import org.parceler.Parcels;
  54. import java.util.Date;
  55. import java.util.HashMap;
  56. import java.util.List;
  57. import java.util.Map;
  58. import javax.inject.Inject;
  59. import autodagger.AutoInjector;
  60. import butterknife.BindView;
  61. import io.reactivex.Observer;
  62. import io.reactivex.android.schedulers.AndroidSchedulers;
  63. import io.reactivex.disposables.Disposable;
  64. import io.reactivex.schedulers.Schedulers;
  65. import retrofit2.Response;
  66. @AutoInjector(NextcloudTalkApplication.class)
  67. public class ChatController extends BaseController implements MessagesListAdapter.OnLoadMoreListener,
  68. MessagesListAdapter.Formatter<Date> {
  69. @Inject
  70. NcApi ncApi;
  71. @Inject
  72. UserUtils userUtils;
  73. @BindView(R.id.input)
  74. MessageInput messageInput;
  75. @BindView(R.id.messagesList)
  76. MessagesList messagesList;
  77. private String conversationName;
  78. private String roomToken;
  79. private UserEntity currentUser;
  80. private boolean inChat = false;
  81. private boolean historyRead = false;
  82. private int globalLastKnownFutureMessageId = -1;
  83. private int globalLastKnownPastMessageId = -1;
  84. private MessagesListAdapter<ChatMessage> adapter;
  85. private RecyclerView.LayoutManager layoutManager;
  86. public ChatController(Bundle args) {
  87. super(args);
  88. setHasOptionsMenu(true);
  89. this.conversationName = args.getString(BundleKeys.KEY_CONVERSATION_NAME);
  90. this.currentUser = Parcels.unwrap(args.getParcelable(BundleKeys.KEY_USER_ENTITY));
  91. this.roomToken = args.getString(BundleKeys.KEY_ROOM_TOKEN);
  92. }
  93. @Override
  94. protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
  95. return inflater.inflate(R.layout.controller_chat, container, false);
  96. }
  97. @Override
  98. protected void onViewBound(@NonNull View view) {
  99. super.onViewBound(view);
  100. NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this);
  101. boolean adapterWasNull = false;
  102. if (adapter == null) {
  103. adapterWasNull = true;
  104. MessagesListAdapter.HoldersConfig holdersConfig = new MessagesListAdapter.HoldersConfig();
  105. holdersConfig.setIncoming(MagicIncomingTextMessageViewHolder.class,
  106. R.layout.item_custom_incoming_text_message);
  107. adapter = new MessagesListAdapter<>(currentUser.getUserId(), holdersConfig, new ImageLoader() {
  108. @Override
  109. public void loadImage(ImageView imageView, String url) {
  110. GlideApp.with(NextcloudTalkApplication.getSharedApplication().getApplicationContext())
  111. .asBitmap()
  112. .diskCacheStrategy(DiskCacheStrategy.NONE)
  113. .load(url)
  114. .centerInside()
  115. .override(imageView.getMeasuredWidth(), imageView.getMeasuredHeight())
  116. .apply(RequestOptions.bitmapTransform(new CircleCrop()))
  117. .into(imageView);
  118. }
  119. });
  120. }
  121. messagesList.setAdapter(adapter);
  122. adapter.setLoadMoreListener(this);
  123. adapter.setDateHeadersFormatter(this::format);
  124. messageInput.setInputListener(input -> {
  125. sendMessage(input.toString());
  126. return true;
  127. });
  128. if (adapterWasNull) {
  129. joinRoomWithPassword(null);
  130. }
  131. }
  132. @Override
  133. protected void onAttach(@NonNull View view) {
  134. super.onAttach(view);
  135. if (getActionBar() != null) {
  136. getActionBar().setDisplayHomeAsUpEnabled(true);
  137. }
  138. }
  139. @Override
  140. protected String getTitle() {
  141. return conversationName;
  142. }
  143. @Override
  144. public boolean onOptionsItemSelected(@NonNull MenuItem item) {
  145. switch (item.getItemId()) {
  146. case android.R.id.home:
  147. inChat = false;
  148. getRouter().popCurrentController();
  149. return true;
  150. default:
  151. return super.onOptionsItemSelected(item);
  152. }
  153. }
  154. @Override
  155. public void onDestroy() {
  156. inChat = false;
  157. super.onDestroy();
  158. }
  159. private void joinRoomWithPassword(@Nullable String password) {
  160. ncApi.joinRoom(ApiUtils.getCredentials(currentUser.getUserId(), currentUser.getToken()), ApiUtils
  161. .getUrlForRoomParticipants(currentUser.getBaseUrl(), roomToken), password)
  162. .subscribeOn(Schedulers.newThread())
  163. .observeOn(AndroidSchedulers.mainThread())
  164. .retry(3)
  165. .subscribe(new Observer<CallOverall>() {
  166. @Override
  167. public void onSubscribe(Disposable d) {
  168. }
  169. @Override
  170. public void onNext(CallOverall callOverall) {
  171. inChat = true;
  172. pullChatMessages(0);
  173. }
  174. @Override
  175. public void onError(Throwable e) {
  176. }
  177. @Override
  178. public void onComplete() {
  179. }
  180. });
  181. }
  182. private void sendMessage(String message) {
  183. Map<String, String> fieldMap = new HashMap<>();
  184. fieldMap.put("message", message);
  185. fieldMap.put("actorDisplayName", currentUser.getDisplayName());
  186. ncApi.sendChatMessage(ApiUtils.getCredentials(currentUser.getUserId(), currentUser.getToken()),
  187. ApiUtils.getUrlForChat(currentUser.getBaseUrl(), roomToken), fieldMap)
  188. .subscribeOn(Schedulers.newThread())
  189. .observeOn(AndroidSchedulers.mainThread())
  190. .retry(3, observable -> inChat)
  191. .subscribe(new Observer<GenericOverall>() {
  192. @Override
  193. public void onSubscribe(Disposable d) {
  194. }
  195. @Override
  196. public void onNext(GenericOverall genericOverall) {
  197. }
  198. @Override
  199. public void onError(Throwable e) {
  200. }
  201. @Override
  202. public void onComplete() {
  203. }
  204. });
  205. }
  206. private void pullChatMessages(int lookIntoFuture) {
  207. Map<String, Integer> fieldMap = new HashMap<>();
  208. fieldMap.put("lookIntoFuture", lookIntoFuture);
  209. fieldMap.put("limit", 25);
  210. int lastKnown;
  211. if (lookIntoFuture == 1) {
  212. lastKnown = globalLastKnownFutureMessageId;
  213. } else {
  214. lastKnown = globalLastKnownPastMessageId;
  215. }
  216. fieldMap.put("lastKnownMessageId", lastKnown);
  217. if (lookIntoFuture == 1) {
  218. ncApi.pullChatMessages(ApiUtils.getCredentials(currentUser.getUserId(), currentUser.getToken()),
  219. ApiUtils.getUrlForChat(currentUser.getBaseUrl(), roomToken), fieldMap)
  220. .subscribeOn(Schedulers.newThread())
  221. .observeOn(AndroidSchedulers.mainThread())
  222. .takeWhile(observable -> inChat)
  223. .retry(3, observable -> inChat)
  224. .subscribe(new Observer<Response>() {
  225. @Override
  226. public void onSubscribe(Disposable d) {
  227. }
  228. @Override
  229. public void onNext(Response response) {
  230. processMessages(response, true);
  231. pullChatMessages(1);
  232. }
  233. @Override
  234. public void onError(Throwable e) {
  235. }
  236. @Override
  237. public void onComplete() {
  238. }
  239. });
  240. } else {
  241. ncApi.pullChatMessages(ApiUtils.getCredentials(currentUser.getUserId(), currentUser.getToken()),
  242. ApiUtils.getUrlForChat(currentUser.getBaseUrl(), roomToken), fieldMap)
  243. .subscribeOn(Schedulers.newThread())
  244. .observeOn(AndroidSchedulers.mainThread())
  245. .retry(3, observable -> inChat)
  246. .subscribe(new Observer<Response>() {
  247. @Override
  248. public void onSubscribe(Disposable d) {
  249. }
  250. @Override
  251. public void onNext(Response response) {
  252. processMessages(response, false);
  253. }
  254. @Override
  255. public void onError(Throwable e) {
  256. }
  257. @Override
  258. public void onComplete() {
  259. }
  260. });
  261. }
  262. }
  263. private void processMessages(Response response, boolean isFromTheFuture) {
  264. if (response.code() == 200) {
  265. boolean shouldForceFuture = false;
  266. if (globalLastKnownFutureMessageId == -1) {
  267. shouldForceFuture = true;
  268. }
  269. ChatOverall chatOverall = (ChatOverall) response.body();
  270. List<ChatMessage> chatMessageList = chatOverall.getOcs().getData();
  271. if (!isFromTheFuture) {
  272. for (int i = 0; i < chatMessageList.size(); i++) {
  273. chatMessageList.get(i).setBaseUrl(currentUser.getBaseUrl());
  274. if (globalLastKnownPastMessageId == -1 || chatMessageList.get(i).getJsonMessageId() <
  275. globalLastKnownPastMessageId) {
  276. globalLastKnownPastMessageId = chatMessageList.get(i).getJsonMessageId();
  277. }
  278. if (shouldForceFuture) {
  279. if (chatMessageList.get(i).getJsonMessageId() > globalLastKnownFutureMessageId) {
  280. globalLastKnownFutureMessageId = chatMessageList.get(i).getJsonMessageId();
  281. }
  282. }
  283. }
  284. adapter.addToEnd(chatMessageList, false);
  285. } else {
  286. LinearLayoutManager layoutManager = (LinearLayoutManager) messagesList.getLayoutManager();
  287. for (int i = 0; i < chatMessageList.size(); i++) {
  288. chatMessageList.get(i).setBaseUrl(currentUser.getBaseUrl());
  289. adapter.addToStart(chatMessageList.get(i),
  290. layoutManager.findLastVisibleItemPosition() <= adapter.getItemCount() - 3);
  291. }
  292. globalLastKnownFutureMessageId = Integer.parseInt(response.headers().get("X-Chat-Last-Given"));
  293. }
  294. if (shouldForceFuture) {
  295. pullChatMessages(1);
  296. }
  297. } else if (response.code() == 304 && !isFromTheFuture) {
  298. historyRead = true;
  299. }
  300. }
  301. @Override
  302. public void onLoadMore(int page, int totalItemsCount) {
  303. if (!historyRead) {
  304. pullChatMessages(0);
  305. }
  306. }
  307. @Override
  308. public String format(Date date) {
  309. if (DateFormatter.isToday(date)) {
  310. return getResources().getString(R.string.nc_date_header_today);
  311. } else if (DateFormatter.isYesterday(date)) {
  312. return getResources().getString(R.string.nc_date_header_yesterday);
  313. } else {
  314. return DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR);
  315. }
  316. }}