Browse Source

Initial chat work

Signed-off-by: Mario Danic <mario@lovelyhq.com>
Mario Danic 7 years ago
parent
commit
09c4b83f82

+ 2 - 0
app/build.gradle

@@ -150,6 +150,8 @@ dependencies {
 
     implementation 'com.github.wooplr:Spotlight:1.2.3'
 
+    implementation 'com.github.stfalcon:chatkit:0.2.2'
+
     implementation 'com.github.Kennyc1012:BottomSheet:2.4.0'
     implementation 'eu.davidea:flipview:1.1.3'
     testImplementation 'junit:junit:4.12'

+ 26 - 0
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -24,6 +24,7 @@ import android.support.annotation.Nullable;
 
 import com.nextcloud.talk.models.json.call.CallOverall;
 import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
+import com.nextcloud.talk.models.json.chat.ChatOverall;
 import com.nextcloud.talk.models.json.generic.GenericOverall;
 import com.nextcloud.talk.models.json.generic.Status;
 import com.nextcloud.talk.models.json.participants.AddParticipantOverall;
@@ -39,6 +40,7 @@ import com.nextcloud.talk.models.json.userprofile.UserProfileOverall;
 import java.util.Map;
 
 import io.reactivex.Observable;
+import retrofit2.Response;
 import retrofit2.http.DELETE;
 import retrofit2.http.Field;
 import retrofit2.http.FieldMap;
@@ -256,4 +258,28 @@ public interface NcApi {
 
     @GET
     Observable<CapabilitiesOverall> getCapabilities(@Header("Authorization") String authorization, @Url String url);
+
+     /*
+        QueryMap items are as follows:
+          - "lookIntoFuture": int (0 or 1),
+          - "limit" : int, range 100-200,
+          - "timeout": used with look into future, 30 default, 60 at most
+          - "lastKnownMessageId", int, use one from X-Chat-Last-Given
+    */
+    @GET
+    Observable<Response<ChatOverall>> pullChatMessages(@Header("Authorization") String authorization, @Url String url,
+                                                       @QueryMap Map<String, Integer> fields);
+
+    /*
+        Fieldmap items are as follows:
+          - "message": ,
+          - "actorDisplayName"
+    */
+
+    @FormUrlEncoded
+    @PUT
+    Observable<GenericOverall> sendChatMessage(@Header("Authorization") String authorization, @Url String url,
+                                               @FieldMap Map<String, String> fields);
+
+
 }

+ 22 - 15
app/src/main/java/com/nextcloud/talk/controllers/CallsListController.java

@@ -113,7 +113,7 @@ public class CallsListController extends BaseController implements SearchView.On
     @BindView(R.id.fast_scroller)
     FastScroller fastScroller;
 
-    private UserEntity userEntity;
+    private UserEntity currentUser;
     private Disposable roomsQueryDisposable;
     private FlexibleAdapter<CallItem> adapter;
     private List<CallItem> callItems = new ArrayList<>();
@@ -144,9 +144,9 @@ public class CallsListController extends BaseController implements SearchView.On
             getActionBar().show();
         }
 
-        userEntity = userUtils.getCurrentUser();
+        currentUser = userUtils.getCurrentUser();
 
-        if (userEntity == null &&
+        if (currentUser == null &&
                 getParentController() != null && getParentController().getRouter() != null) {
             getParentController().getRouter().setRoot((RouterTransaction.with(new ServerSelectionController())
                     .pushChangeHandler(new HorizontalChangeHandler())
@@ -155,7 +155,7 @@ public class CallsListController extends BaseController implements SearchView.On
 
         if (adapter == null) {
             adapter = new FlexibleAdapter<>(callItems, getActivity(), false);
-            if (userEntity != null) {
+            if (currentUser != null) {
                 fetchData(false);
             }
         }
@@ -172,7 +172,7 @@ public class CallsListController extends BaseController implements SearchView.On
             getActionBar().setDisplayHomeAsUpEnabled(false);
         }
 
-        userEntity = userUtils.getCurrentUser();
+        currentUser = userUtils.getCurrentUser();
 
     }
 
@@ -268,15 +268,15 @@ public class CallsListController extends BaseController implements SearchView.On
 
         callItems = new ArrayList<>();
 
-        roomsQueryDisposable = ncApi.getRooms(ApiUtils.getCredentials(userEntity.getUsername(),
-                userEntity.getToken()), ApiUtils.getUrlForGetRooms(userEntity.getBaseUrl()))
+        roomsQueryDisposable = ncApi.getRooms(ApiUtils.getCredentials(currentUser.getUsername(),
+                currentUser.getToken()), ApiUtils.getUrlForGetRooms(currentUser.getBaseUrl()))
                 .subscribeOn(Schedulers.newThread())
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(roomsOverall -> {
 
                     if (roomsOverall != null) {
                         for (int i = 0; i < roomsOverall.getOcs().getData().size(); i++) {
-                            callItems.add(new CallItem(roomsOverall.getOcs().getData().get(i), userEntity));
+                            callItems.add(new CallItem(roomsOverall.getOcs().getData().get(i), currentUser));
                         }
 
                         adapter.updateDataSet(callItems, true);
@@ -305,7 +305,7 @@ public class CallsListController extends BaseController implements SearchView.On
                                 if (getParentController() != null &&
                                         getParentController().getRouter() != null) {
                                     getParentController().getRouter().pushController((RouterTransaction.with
-                                            (new WebViewLoginController(userEntity.getBaseUrl(),
+                                            (new WebViewLoginController(currentUser.getBaseUrl(),
                                                     true))
                                             .pushChangeHandler(new VerticalChangeHandler())
                                             .popChangeHandler(new VerticalChangeHandler())));
@@ -494,18 +494,25 @@ public class CallsListController extends BaseController implements SearchView.On
             Room room = callItem.getModel();
             Bundle bundle = new Bundle();
             bundle.putString(BundleKeys.KEY_ROOM_TOKEN, callItem.getModel().getToken());
-            bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(userEntity));
+            bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(currentUser));
 
             if (room.hasPassword && (room.participantType.equals(Participant.ParticipantType.GUEST) ||
                     room.participantType.equals(Participant.ParticipantType.USER_FOLLOWING_LINK))) {
                 bundle.putInt(BundleKeys.KEY_OPERATION_CODE, 99);
                 prepareAndShowBottomSheetWithBundle(bundle, false);
             } else {
-                overridePushHandler(new NoOpControllerChangeHandler());
-                overridePopHandler(new NoOpControllerChangeHandler());
-                Intent callIntent = new Intent(getActivity(), CallActivity.class);
-                callIntent.putExtras(bundle);
-                startActivity(callIntent);
+                if (currentUser.hasSpreedCapabilityWithName("chat-v2")) {
+                    bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, room.getDisplayName());
+                    getParentController().getRouter().pushController((RouterTransaction.with(new ChatController(bundle))
+                            .pushChangeHandler(new HorizontalChangeHandler())
+                            .popChangeHandler(new HorizontalChangeHandler())));
+                } else {
+                    overridePushHandler(new NoOpControllerChangeHandler());
+                    overridePopHandler(new NoOpControllerChangeHandler());
+                    Intent callIntent = new Intent(getActivity(), CallActivity.class);
+                    callIntent.putExtras(bundle);
+                    startActivity(callIntent);
+                }
             }
         }
 

+ 318 - 0
app/src/main/java/com/nextcloud/talk/controllers/ChatController.java

@@ -0,0 +1,318 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.controllers;
+
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.load.resource.bitmap.CircleCrop;
+import com.bumptech.glide.request.RequestOptions;
+import com.nextcloud.talk.R;
+import com.nextcloud.talk.api.NcApi;
+import com.nextcloud.talk.application.NextcloudTalkApplication;
+import com.nextcloud.talk.controllers.base.BaseController;
+import com.nextcloud.talk.models.database.UserEntity;
+import com.nextcloud.talk.models.json.call.CallOverall;
+import com.nextcloud.talk.models.json.chat.ChatMessage;
+import com.nextcloud.talk.models.json.chat.ChatOverall;
+import com.nextcloud.talk.utils.ApiUtils;
+import com.nextcloud.talk.utils.bundle.BundleKeys;
+import com.nextcloud.talk.utils.database.user.UserUtils;
+import com.nextcloud.talk.utils.glide.GlideApp;
+import com.stfalcon.chatkit.commons.ImageLoader;
+import com.stfalcon.chatkit.messages.MessageInput;
+import com.stfalcon.chatkit.messages.MessagesList;
+import com.stfalcon.chatkit.messages.MessagesListAdapter;
+
+import org.parceler.Parcels;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import autodagger.AutoInjector;
+import butterknife.BindView;
+import io.reactivex.Observer;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import retrofit2.Response;
+
+@AutoInjector(NextcloudTalkApplication.class)
+public class ChatController extends BaseController implements MessagesListAdapter.OnLoadMoreListener {
+    @Inject
+    NcApi ncApi;
+    @Inject
+    UserUtils userUtils;
+    @BindView(R.id.input)
+    MessageInput messageInput;
+    @BindView(R.id.messagesList)
+    MessagesList messagesList;
+
+    private String conversationName;
+    private String roomToken;
+    private UserEntity currentUser;
+
+    private boolean inChat = false;
+    private boolean historyRead = false;
+    private int globalLastKnownFutureMessageId = -1;
+    private int globalLastKnownPastMessageId = -1;
+
+    private MessagesListAdapter<ChatMessage> adapter;
+
+    public ChatController(Bundle args) {
+        super(args);
+        setHasOptionsMenu(true);
+        this.conversationName = args.getString(BundleKeys.KEY_CONVERSATION_NAME);
+        this.currentUser = Parcels.unwrap(args.getParcelable(BundleKeys.KEY_USER_ENTITY));
+        this.roomToken = args.getString(BundleKeys.KEY_ROOM_TOKEN);
+    }
+
+    @Override
+    protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
+        return inflater.inflate(R.layout.controller_chat, container, false);
+    }
+
+    @Override
+    protected void onViewBound(@NonNull View view) {
+        super.onViewBound(view);
+        NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this);
+
+        adapter = new MessagesListAdapter<>(currentUser.getUserId(), new ImageLoader() {
+            @Override
+            public void loadImage(ImageView imageView, String url) {
+                GlideApp.with(NextcloudTalkApplication.getSharedApplication().getApplicationContext())
+                        .asBitmap()
+                        .diskCacheStrategy(DiskCacheStrategy.NONE)
+                        .load(url)
+                        .centerInside()
+                        .override(imageView.getMeasuredWidth(), imageView.getMeasuredHeight())
+                        .apply(RequestOptions.bitmapTransform(new CircleCrop()))
+                        .into(imageView);
+            }
+        });
+
+        messagesList.setAdapter(adapter);
+        adapter.setLoadMoreListener(this);
+        joinRoomWithPassword(null);
+    }
+
+    @Override
+    protected void onAttach(@NonNull View view) {
+        super.onAttach(view);
+        if (getActionBar() != null) {
+            getActionBar().setDisplayHomeAsUpEnabled(true);
+        }
+    }
+
+    @Override
+    protected String getTitle() {
+        return conversationName;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                inChat = false;
+                getRouter().popCurrentController();
+                return true;
+            default:
+                return super.onOptionsItemSelected(item);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        inChat = false;
+        super.onDestroy();
+    }
+
+    private void joinRoomWithPassword(@Nullable String password) {
+        ncApi.joinRoom(ApiUtils.getCredentials(currentUser.getUserId(), currentUser.getToken()), ApiUtils
+                .getUrlForRoomParticipants(currentUser.getBaseUrl(), roomToken), password)
+                .subscribeOn(Schedulers.newThread())
+                .observeOn(AndroidSchedulers.mainThread())
+                .retry(3)
+                .subscribe(new Observer<CallOverall>() {
+                    @Override
+                    public void onSubscribe(Disposable d) {
+
+                    }
+
+                    @Override
+                    public void onNext(CallOverall callOverall) {
+                        inChat = true;
+                        pullChatMessages(0);
+                    }
+
+                    @Override
+                    public void onError(Throwable e) {
+
+                    }
+
+                    @Override
+                    public void onComplete() {
+
+                    }
+                });
+    }
+
+    private void pullChatMessages(int lookIntoFuture) {
+        Map<String, Integer> fieldMap = new HashMap<>();
+        fieldMap.put("lookIntoFuture", lookIntoFuture);
+        fieldMap.put("limit", 2);
+
+        int lastKnown;
+        if (lookIntoFuture == 1) {
+            lastKnown = globalLastKnownFutureMessageId;
+        } else {
+            lastKnown = globalLastKnownPastMessageId;
+        }
+
+        fieldMap.put("lastKnownMessageId", lastKnown);
+
+        if (lookIntoFuture == 1) {
+            ncApi.pullChatMessages(ApiUtils.getCredentials(currentUser.getUserId(), currentUser.getToken()),
+                    ApiUtils.getUrlForChat(currentUser.getBaseUrl(), roomToken), fieldMap)
+                    .subscribeOn(Schedulers.newThread())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .takeWhile(observable -> inChat)
+                    .retry(3, observable -> inChat)
+                    .subscribe(new Observer<Response>() {
+                        @Override
+                        public void onSubscribe(Disposable d) {
+
+                        }
+
+                        @Override
+                        public void onNext(Response response) {
+                            processMessages(response, true);
+                            pullChatMessages(1);
+                        }
+
+                        @Override
+                        public void onError(Throwable e) {
+
+                        }
+
+                        @Override
+                        public void onComplete() {
+
+                        }
+                    });
+
+        } else {
+            ncApi.pullChatMessages(ApiUtils.getCredentials(currentUser.getUserId(), currentUser.getToken()),
+                    ApiUtils.getUrlForChat(currentUser.getBaseUrl(), roomToken), fieldMap)
+                    .subscribeOn(Schedulers.newThread())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .retry(3, observable -> inChat)
+                    .subscribe(new Observer<Response>() {
+                        @Override
+                        public void onSubscribe(Disposable d) {
+
+                        }
+
+                        @Override
+                        public void onNext(Response response) {
+                            processMessages(response, false);
+                        }
+
+                        @Override
+                        public void onError(Throwable e) {
+
+                        }
+
+                        @Override
+                        public void onComplete() {
+
+                        }
+                    });
+        }
+    }
+
+    private void processMessages(Response response, boolean isFromTheFuture) {
+        if (response.code() == 200) {
+            boolean shouldForceFuture = false;
+            if (globalLastKnownFutureMessageId == -1) {
+                shouldForceFuture = true;
+            }
+
+            ChatOverall chatOverall = (ChatOverall) response.body();
+            List<ChatMessage> chatMessageList = chatOverall.getOcs().getData();
+
+            if (!isFromTheFuture) {
+                for (int i = 0; i < chatMessageList.size(); i++) {
+                    chatMessageList.get(i).setBaseUrl(currentUser.getBaseUrl());
+                    if (globalLastKnownPastMessageId == -1 || chatMessageList.get(i).getJsonMessageId() <
+                            globalLastKnownPastMessageId) {
+                        globalLastKnownPastMessageId = chatMessageList.get(i).getJsonMessageId();
+                    }
+
+                    if (shouldForceFuture) {
+                        if (chatMessageList.get(i).getJsonMessageId() > globalLastKnownFutureMessageId) {
+                            globalLastKnownFutureMessageId = chatMessageList.get(i).getJsonMessageId();
+                        }
+                    }
+                }
+
+
+                adapter.addToEnd(chatMessageList, false);
+
+            } else {
+                for (int i = 0; i < chatMessageList.size(); i++) {
+                    chatMessageList.get(i).setBaseUrl(currentUser.getBaseUrl());
+                    if (i == chatMessageList.size() - 1) {
+                        adapter.addToStart(chatMessageList.get(i), true);
+                    } else {
+                        adapter.addToStart(chatMessageList.get(i), false);
+                    }
+                }
+
+                globalLastKnownFutureMessageId = Integer.parseInt(response.headers().get("X-Chat-Last-Given"));
+            }
+
+            if (shouldForceFuture) {
+                pullChatMessages(1);
+            }
+        } else if (response.code() == 304 && !isFromTheFuture) {
+            historyRead = true;
+        }
+    }
+
+    @Override
+    public void onLoadMore(int page, int totalItemsCount) {
+        if (!historyRead) {
+            pullChatMessages(0);
+        }
+    }
+}

+ 111 - 0
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java

@@ -0,0 +1,111 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.chat;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+import com.nextcloud.talk.utils.ApiUtils;
+import com.nextcloud.talk.utils.TimeUtils;
+import com.stfalcon.chatkit.commons.models.IMessage;
+import com.stfalcon.chatkit.commons.models.IUser;
+
+import org.parceler.Parcel;
+
+import java.util.Date;
+
+import lombok.Data;
+
+@Parcel
+@Data
+@JsonObject
+public class ChatMessage implements IMessage {
+    String baseUrl;
+
+    public String getBaseUrl() {
+        return baseUrl;
+    }
+
+    public void setBaseUrl(String baseUrl) {
+        this.baseUrl = baseUrl;
+    }
+
+    @JsonField(name = "id")
+    int jsonMessageId;
+
+    @JsonField(name = "token")
+    String token;
+
+    // guests or users
+    @JsonField(name = "actorType")
+    String actorType;
+
+    @JsonField(name = "actorId")
+    String actorId;
+
+    // send when crafting a message
+    @JsonField(name = "actorDisplayName")
+    String actorDisplayName;
+
+    @JsonField(name = "timestamp")
+    long timestamp;
+
+    // send when crafting a message, max 1000 lines
+    @JsonField(name = "message")
+    String message;
+
+    @Override
+    public String getId() {
+        return Integer.toString(jsonMessageId);
+    }
+
+    @Override
+    public String getText() {
+        return message;
+    }
+
+    @Override
+    public IUser getUser() {
+        return new IUser() {
+            @Override
+            public String getId() {
+                return actorId;
+            }
+
+            @Override
+            public String getName() {
+                return actorDisplayName;
+            }
+
+            @Override
+            public String getAvatar() {
+                if ("guests".equals(actorType)) {
+                    return null;
+                } else {
+                    return ApiUtils.getUrlForAvatarWithName(getBaseUrl(), actorId, false);
+                }
+            }
+        };
+    }
+
+    @Override
+    public Date getCreatedAt() {
+        return TimeUtils.getDateCurrentTimeZone(timestamp);
+    }
+}

+ 38 - 0
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOCS.java

@@ -0,0 +1,38 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.chat;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+import com.nextcloud.talk.models.json.generic.GenericOCS;
+
+import org.parceler.Parcel;
+
+import java.util.List;
+
+import lombok.Data;
+
+@Data
+@Parcel
+@JsonObject
+public class ChatOCS extends GenericOCS {
+    @JsonField(name = "data")
+    List<ChatMessage> data;
+}

+ 36 - 0
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatOverall.java

@@ -0,0 +1,36 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.models.json.chat;
+
+import com.bluelinelabs.logansquare.annotation.JsonField;
+import com.bluelinelabs.logansquare.annotation.JsonObject;
+
+import org.parceler.Parcel;
+
+import lombok.Data;
+
+@Data
+@Parcel
+@JsonObject
+public class ChatOverall {
+    @JsonField(name = "ocs")
+    ChatOCS ocs;
+}

+ 4 - 0
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -126,6 +126,10 @@ public class ApiUtils {
         return getUrlForCall(baseUrl, token) + "/ping";
     }
 
+    public static String getUrlForChat(String baseUrl, String token) {
+        return baseUrl + ocsApiVersion + spreedApiVersion + "/chat/" + token;
+    }
+
     public static String getUrlForSignaling(String baseUrl, @Nullable String token) {
         String signalingUrl = baseUrl + ocsApiVersion + spreedApiVersion + "/signaling";
         if (token == null) {

+ 52 - 0
app/src/main/java/com/nextcloud/talk/utils/TimeUtils.java

@@ -0,0 +1,52 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils;
+
+import android.util.Log;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class TimeUtils {
+    private static final String TAG = "TimeUtils";
+
+    public static Date getDateCurrentTimeZone(long timestamp) {
+        try{
+            Calendar calendar = Calendar.getInstance();
+            TimeZone tz = Calendar.getInstance().getTimeZone();
+            calendar.setTimeInMillis(timestamp * 1000);
+            calendar.add(Calendar.MILLISECOND, tz.getOffset(calendar.getTimeInMillis()));
+            Date currentTimeZone = calendar.getTime();
+            return currentTimeZone;
+        } catch (Exception e) {
+            Log.d(TAG, "Failed to convert time to local timezone");
+        }
+        return new Date();
+    }
+
+    public static String getDateStringCurrentTimeZone(long timestamp) {
+        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
+        return simpleDateFormat.format(getDateCurrentTimeZone(timestamp));
+    }
+}

+ 55 - 0
app/src/main/res/layout/controller_chat.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:app="http://schemas.android.com/apk/res-auto"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent">
+
+    <com.stfalcon.chatkit.messages.MessagesList
+        android:id="@+id/messagesList"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_above="@+id/input"
+        app:textAutoLink="all"/>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_above="@+id/input"
+        android:layout_marginLeft="16dp"
+        android:layout_marginRight="16dp"
+        android:background="@color/nc_light_grey"/>
+
+    <com.stfalcon.chatkit.messages.MessageInput
+        android:id="@+id/input"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:maxLength="1000"
+        app:inputButtonHeight="30dp"
+        app:inputButtonMargin="16dp"
+        app:inputButtonWidth="30dp"
+        app:inputHint="@string/hint_enter_a_message"
+        app:inputTextColor="@color/black"
+        app:inputTextSize="18sp"/>
+
+</RelativeLayout>

+ 1 - 0
app/src/main/res/values/colors.xml

@@ -10,5 +10,6 @@
     <color name="nc_white_color_complete">#FFFFFF</color>
     <color name="nc_light_blue_color">#7FC0E3</color>
     <color name="nc_material_yellow">#FFEB3B</color>
+    <color name="nc_light_grey">#E8E8E8</color>
 </resources>
 

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -164,4 +164,5 @@ Millions of users use Nextcloud daily at businesses and homes around the world.
 Learn more on https://nextcloud.com/talk
 
 Find Nextcloud on https://nextcloud.com</string>
+    <string name="hint_enter_a_message">Enter a message…</string>
 </resources>