Unverified Commit 03387f6c authored by Lucio Maciel's avatar Lucio Maciel Committed by GitHub

Merge pull request #733 from RocketChat/message-actions

[NEW][WIP] Message actions - Reply, Quote, Copy, Delete, Edit and Pin/Unpin
parents 6a133806 86bba856
......@@ -105,6 +105,8 @@ dependencies {
implementation libraries.moshiLazyAdapters
implementation libraries.sheetMenu
implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
transitive = true
}
......
package chat.rocket.android;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("chat.rocket.android", appContext.getPackageName());
}
}
......@@ -47,6 +47,10 @@
android:name=".chatroom.ui.ChatRoomActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".chatroom.ui.PinnedMessagesActivity"
android:theme="@style/AppTheme" />
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
......
......@@ -13,6 +13,7 @@ import chat.rocket.common.RocketChatTwoFactorException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken
import javax.inject.Inject
......@@ -93,7 +94,9 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
try {
val token = client.login(usernameOrEmail, password)
val me = client.me()
multiServerRepository.save(server, TokenModel(token.userId, token.authToken))
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
......
......@@ -6,7 +6,10 @@ import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.text.style.ClickableSpan
import android.view.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.ImageButton
import android.widget.ScrollView
import android.widget.Toast
......@@ -18,6 +21,7 @@ import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.showToast
import chat.rocket.android.util.textContent
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
......@@ -167,8 +171,9 @@ class LoginFragment : Fragment(), LoginView {
enableUserInput(true)
}
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
......@@ -23,7 +23,8 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private var settingsFilter = arrayOf(SITE_URL, SITE_NAME, FAVICON_512, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS, FAVORITE_ROOMS,
ACCOUNT_LOGIN_FORM, ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB, ACCOUNT_GITLAB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR,
ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, LDAP_ENABLE, ACCOUNT_REGISTRATION, STORAGE_TYPE, HIDE_USER_JOIN, HIDE_USER_LEAVE, HIDE_TYPE_AU,
HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ACCOUNT_CUSTOM_FIELDS)
HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ACCOUNT_CUSTOM_FIELDS, ALLOW_MESSAGE_DELETING, ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING,
SHOW_DELETED_STATUS, SHOW_EDITED_STATUS)
fun connect(server: String) {
if (!UrlHelper.isValidUrl(server)) {
......
......@@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.authentication.server.presentation.ServerPresenter
import chat.rocket.android.authentication.server.presentation.ServerView
......@@ -56,7 +55,9 @@ class ServerFragment : Fragment(), ServerView {
enableUserInput(true)
}
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
......@@ -12,6 +12,7 @@ import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.internal.rest.signup
import javax.inject.Inject
......@@ -51,6 +52,8 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
try {
client.signup(email, name, username, password) // TODO This function returns a user so should we save it?
client.login(username, password) // TODO This function returns a user token so should we save it?
val me = client.me()
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
......
......@@ -15,6 +15,7 @@ import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.showToast
import chat.rocket.android.util.textContent
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_sign_up.*
......@@ -100,9 +101,9 @@ class SignupFragment : Fragment(), SignupView {
enableUserInput(true)
}
override fun showMessage(message: String) {
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
}
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
......
......@@ -15,6 +15,7 @@ import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter
import chat.rocket.android.authentication.twofactor.presentation.TwoFAView
import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.showToast
import chat.rocket.android.util.textContent
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_two_fa.*
......@@ -83,7 +84,9 @@ class TwoFAFragment : Fragment(), TwoFAView {
enableUserInput(true)
}
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
package chat.rocket.android.chatroom.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.PinnedMessagesView
import chat.rocket.android.chatroom.ui.PinnedMessagesFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class PinnedMessagesFragmentModule {
@Provides
fun provideLifecycleOwner(frag: PinnedMessagesFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
@Provides
fun providePinnedMessageView(frag: PinnedMessagesFragment): PinnedMessagesView {
return frag
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.di
import chat.rocket.android.chatroom.ui.PinnedMessagesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class PinnedMessagesFragmentProvider {
@ContributesAndroidInjector(modules = [PinnedMessagesFragmentModule::class])
abstract fun providePinnedMessageFragment(): PinnedMessagesFragment
}
\ No newline at end of file
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.MessageViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.connect
import chat.rocket.core.internal.realtime.subscribeRoomMessages
import chat.rocket.core.internal.realtime.unsubscibre
import chat.rocket.core.internal.rest.messages
import chat.rocket.core.internal.rest.sendMessage
import chat.rocket.core.internal.rest.*
import chat.rocket.core.model.Message
import chat.rocket.core.model.Value
import kotlinx.coroutines.experimental.CommonPool
......@@ -27,17 +27,13 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val strategy: CancelStrategy,
getSettingsInteractor: GetSettingsInteractor,
private val serverInteractor: GetCurrentServerInteractor,
private val permissions: GetPermissionsInteractor,
private val messagesRepository: MessagesRepository,
factory: RocketChatClientFactory,
private val mapper: MessageViewModelMapper) {
private val client = factory.create(serverInteractor.get()!!)
private val roomMessages = ArrayList<Message>()
private var subId: String? = null
private var settings: Map<String, Value<Any>>? = null
init {
settings = getSettingsInteractor.get(serverInteractor.get()!!)
}
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private val stateChannel = Channel<State>()
......@@ -46,13 +42,10 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
view.showLoading()
try {
val messages = client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
synchronized(roomMessages) {
roomMessages.addAll(messages)
messagesRepository.saveAll(messages)
}
val messagesViewModels = mapper.mapToViewModelList(messages, settings)
view.showMessages(messagesViewModels, serverInteractor.get()!!)
view.showMessages(messagesViewModels)
// Subscribe after getting the first page of messages from REST
if (offset == 0L) {
......@@ -71,11 +64,16 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
}
fun sendMessage(chatRoomId: String, text: String) {
fun sendMessage(chatRoomId: String, text: String, messageId: String?) {
launchUI(strategy) {
view.disableMessageInput()
try {
val message = client.sendMessage(chatRoomId, text)
val message: Message
if (messageId == null) {
message = client.sendMessage(chatRoomId, text)
} else {
message = client.updateMessage(chatRoomId, messageId, text)
}
// ignore message for now, will receive it on the stream
view.enableMessageInput(clear = true)
} catch (ex: Exception) {
......@@ -142,6 +140,125 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
}
/**
* Delete the message with the given id.
*
* @param roomId The room id of the message to be deleted.
* @param id The id of the message to be deleted.
*/
fun deleteMessage(roomId: String, id: String) {
launchUI(strategy) {
if (!permissions.allowedMessageDeleting()) {
return@launchUI
}
//TODO: Default delete message always to true. Until we have the permissions system
//implemented, a user will only be able to delete his own messages.
try {
client.deleteMessage(roomId, id, true)
// if Message_ShowDeletedStatus == true an update to that message will be dispatched.
// Otherwise we signalize that we just want the message removed.
if (!permissions.showDeletedStatus()) {
view.dispatchDeleteMessage(id)
}
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
/**
* Quote or reply a message.
*
* @param roomType The current room type.
* @param roomName The name of the current room.
* @param messageId The id of the message to make citation for.
* @param mentionAuthor true means the citation is a reply otherwise it's a quote.
*/
fun citeMessage(roomType: String, roomName: String, messageId: String, mentionAuthor: Boolean) {
launchUI(strategy) {
val message = messagesRepository.getById(messageId)
val me = client.me() //TODO: Cache this and use an interactor
val serverUrl = serverInteractor.get()!!
message?.let { m ->
val id = m.id
val username = m.sender?.username
val user = "@" + if (settings.useRealName()) m.sender?.name ?: m.sender?.username else m.sender?.username
val mention = if (mentionAuthor && me.username != username) user else ""
val type = roomTypeOf(roomType)
val room = when (type) {
is RoomType.Channel -> "channel"
is RoomType.DirectMessage -> "direct"
is RoomType.PrivateGroup -> "group"
is RoomType.Livechat -> "livechat"
is RoomType.Custom -> "custom" //TODO: put appropriate callback string here.
}
view.showReplyingAction(user, "[ ](${serverUrl}/${room}/${roomName}?msg=${id}) ${mention} ", m.message)
}
}
}
/**
* Copy message to clipboard.
*
* @param messageId The id of the message to copy to clipboard.
*/
fun copyMessage(messageId: String) {
launchUI(strategy) {
try {
messagesRepository.getById(messageId)?.let { m ->
view.copyToClipboard(m.message)
}
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
/**
* Update message identified by given id with given text.
*
* @param roomId The id of the room of the message.
* @param messageId The id of the message to update.
* @param text The updated text.
*/
fun editMessage(roomId: String, messageId: String, text: String) {
launchUI(strategy) {
if (!permissions.allowedMessageEditing()) {
view.showMessage(R.string.permission_editing_not_allowed)
return@launchUI
}
view.showEditingAction(roomId, messageId, text)
}
}
fun pinMessage(messageId: String) {
launchUI(strategy) {
if (!permissions.allowedMessagePinning()) {
view.showMessage(R.string.permission_pinning_not_allowed)
return@launchUI
}
try {
client.pinMessage(messageId)
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
fun unpinMessage(messageId: String) {
launchUI(strategy) {
if (!permissions.allowedMessagePinning()) {
view.showMessage(R.string.permission_pinning_not_allowed)
return@launchUI
}
try {
client.unpinMessage(messageId)
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
private suspend fun listenMessages(roomId: String) {
launch(CommonPool + strategy.jobs) {
for (message in client.messagesChannel) {
......@@ -157,18 +274,17 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) {
val viewModelStreamedMessage = mapper.mapToViewModel(streamedMessage, settings)
synchronized(roomMessages) {
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
if (index != -1) {
Timber.d("Updatind message at $index")
roomMessages[index] = streamedMessage
if (index > -1) {
Timber.d("Updating message at $index")
messagesRepository.save(streamedMessage)
view.dispatchUpdateMessage(index, viewModelStreamedMessage)
} else {
Timber.d("Adding new message")
roomMessages.add(0, streamedMessage)
messagesRepository.save(streamedMessage)
view.showNewMessage(viewModelStreamedMessage)
}
}
}
}
}
\ No newline at end of file
......@@ -10,9 +10,8 @@ interface ChatRoomView : LoadingView, MessageView {
* Shows the chat room messages.
*
* @param dataSet The data set to show.
* @param serverUrl The server URL.
*/
fun showMessages(dataSet: List<MessageViewModel>, serverUrl: String)
fun showMessages(dataSet: List<MessageViewModel>)
/**
* Send a message to a chat room.
......@@ -28,6 +27,13 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun showNewMessage(message: MessageViewModel)
/**
* Dispatch to the recycler views adapter that we should remove a message.
*
* @param msgId The id of the message to be removed.
*/
fun dispatchDeleteMessage(msgId: String)
/**
* Dispatch a update to the recycler views adapter about a changed message.
*
......@@ -35,7 +41,27 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun dispatchUpdateMessage(index: Int, message: MessageViewModel)
fun disableMessageInput()
/**
* Show reply status above the message composer.
*
* @param username The username or name of the user to reply/quote to.
* @param replyMarkdown The markdown of the message reply.
* @param quotedMessage The message to quote.
*/
fun showReplyingAction(username: String, replyMarkdown: String, quotedMessage: String)
/**
* Copy message to clipboard.
*
* @param message The message to copy.
*/
fun copyToClipboard(message: String)
/**
* Show edit status above the message composer.
*/
fun showEditingAction(roomId: String, messageId: String, text: String)
fun disableMessageInput()
fun enableMessageInput(clear: Boolean = false)
}
\ No newline at end of file
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.viewmodel.MessageViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getRoomPinnedMessages
import chat.rocket.core.model.Value
import timber.log.Timber
import javax.inject.Inject
class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessagesView,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor,
private val mapper: MessageViewModelMapper,
factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor) {
private val client = factory.create(serverInteractor.get()!!)
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private var pinnedMessagesListOffset: Int = 0
/**
* Load all pinned messages for the given room id.
*
* @param roomId The id of the room to get pinned messages from.
*/
fun loadPinnedMessages(roomId: String) {
launchUI(strategy) {
try {
val serverUrl = serverInteractor.get()!!
val chatRoom = roomsInteractor.getById(serverUrl, roomId)
chatRoom?.let { room ->
view.showLoading()
val pinnedMessages = client.getRoomPinnedMessages(roomId, room.type, pinnedMessagesListOffset)
pinnedMessagesListOffset = pinnedMessages.offset.toInt()
val messageList = mapper.mapToViewModelList(pinnedMessages.result, settings).filterNot { it.isSystemMessage }
view.showPinnedMessages(messageList)
view.hideLoading()
}.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server.")
}
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface PinnedMessagesView : MessageView, LoadingView {
/**
* Show list of pinned messages for the current room.
*
* @param pinnedMessages The list of pinned messages.
*/
fun showPinnedMessages(pinnedMessages: List<MessageViewModel>)
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import android.graphics.drawable.Drawable
import android.support.design.widget.BaseTransientBottomBar
import android.support.v4.view.ViewCompat
import android.text.Spannable
import android.text.SpannableString
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.content
import ru.noties.markwon.Markwon
import javax.inject.Inject
class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
companion object {
fun make(parentViewGroup: ViewGroup, content: String = "", parser: MessageParser): ActionSnackbar {
val context = parentViewGroup.context
val view = LayoutInflater.from(context).inflate(R.layout.message_action_bar, parentViewGroup, false)
val actionSnackbar = ActionSnackbar(parentViewGroup, view, CallbackImpl(view))
actionSnackbar.parser = parser
actionSnackbar.messageTextView = view.findViewById(R.id.text_view_action_text) as TextView
actionSnackbar.titleTextView = view.findViewById(R.id.text_view_action_title) as TextView
actionSnackbar.cancelView = view.findViewById(R.id.image_view_action_cancel_quote) as ImageView
actionSnackbar.duration = BaseTransientBottomBar.LENGTH_INDEFINITE
val spannable = Markwon.markdown(context, content).trim()
actionSnackbar.marginDrawable = context.getDrawable(R.drawable.quote)
actionSnackbar.messageTextView.content = spannable
return actionSnackbar
}
}
lateinit var parser: MessageParser
lateinit var cancelView: View
private lateinit var messageTextView: TextView
private lateinit var titleTextView: TextView
private lateinit var marginDrawable: Drawable
var text: String = ""
set(value) {
val spannable = parser.renderMarkdown(value) as Spannable
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
messageTextView.content = spannable
}
var title: String = ""
set(value) {
val spannable = Markwon.markdown(this.context, value) as Spannable
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
titleTextView.content = spannable
}
override fun dismiss() {
super.dismiss()
text = ""
title = ""
}
private constructor(parentViewGroup: ViewGroup, content: View, contentViewCallback: BaseTransientBottomBar.ContentViewCallback) :
super(parentViewGroup, content, contentViewCallback)
class CallbackImpl(val content: View) : BaseTransientBottomBar.ContentViewCallback {
override fun animateContentOut(delay: Int, duration: Int) {
ViewCompat.setScaleY(content, 1f)
ViewCompat.animate(content)
.scaleY(0f)
.setDuration(duration.toLong())
.setStartDelay(delay.toLong())
}
override fun animateContentIn(delay: Int, duration: Int) {
ViewCompat.setScaleY(content, 0f)
ViewCompat.animate(content)
.scaleY(1f).setDuration(duration.toLong())
.setStartDelay(delay.toLong())
}
}
}
\ No newline at end of file
......@@ -15,6 +15,7 @@ import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject
fun Context.chatRoomIntent(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Intent {
return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
......@@ -67,6 +68,8 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
}
private fun setupToolbar(chatRoomName: String) {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName
toolbar.setNavigationOnClickListener {
finishActivity()
......
......@@ -2,26 +2,33 @@ package chat.rocket.android.chatroom.ui
import android.support.v7.widget.RecyclerView
import android.text.method.LinkMovementMethod
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
import chat.rocket.android.chatroom.viewmodel.AttachmentType
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.content
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
import chat.rocket.common.util.ifNull
import com.facebook.drawee.view.SimpleDraweeView
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
import kotlinx.android.synthetic.main.message_attachment.view.*
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
class ChatRoomAdapter(private val serverUrl: String) : RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {
class ChatRoomAdapter(private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter) : RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {
init {
setHasStableIds(true)
......@@ -30,7 +37,7 @@ class ChatRoomAdapter(private val serverUrl: String) : RecyclerView.Adapter<Chat
val dataSet = ArrayList<MessageViewModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(parent.inflate(R.layout.item_message), serverUrl)
ViewHolder(parent.inflate(R.layout.item_message), roomType, roomName, presenter)
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(dataSet[position])
......@@ -53,18 +60,35 @@ class ChatRoomAdapter(private val serverUrl: String) : RecyclerView.Adapter<Chat
notifyItemInserted(0)
}
fun updateItem(index: Int, message: MessageViewModel) {
fun updateItem(message: MessageViewModel) {
val index = dataSet.indexOfFirst { it.id == message.id }
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
}
}
fun removeItem(messageId: String) {
val index = dataSet.indexOfFirst { it.id == messageId }
if (index > -1) {
dataSet.removeAt(index)
notifyItemRemoved(index)
}
}
override fun getItemId(position: Int): Long {
return dataSet[position].id.hashCode().toLong()
}
class ViewHolder(itemView: View, val serverUrl: String) : RecyclerView.ViewHolder(itemView) {
class ViewHolder(itemView: View,
val roomType: String,
val roomName: String,
val presenter: ChatRoomPresenter) : RecyclerView.ViewHolder(itemView), MenuItem.OnMenuItemClickListener {
private lateinit var messageViewModel: MessageViewModel
fun bind(message: MessageViewModel) = with(itemView) {
messageViewModel = message
bindUserAvatar(message, image_avatar, image_unknown_avatar)
text_user_name.content = message.sender
text_message_time.content = message.time
......@@ -73,6 +97,45 @@ class ChatRoomAdapter(private val serverUrl: String) : RecyclerView.Adapter<Chat
bindAttachment(message, message_attachment, image_attachment, audio_video_attachment,
file_name)
text_content.setOnClickListener {
if (!message.isSystemMessage) {
val menuItems = it.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply {
val isPinned = message.isPinned
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin)
setChecked(isPinned)
}
val adapter = ActionListAdapter(menuItems, this@ViewHolder)
BottomSheetMenu(adapter).apply {
}.show(it.context)
}
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
messageViewModel.apply {
when (item.itemId) {
R.id.action_menu_msg_delete -> presenter.deleteMessage(roomId, id)
R.id.action_menu_msg_quote -> presenter.citeMessage(roomType, roomName, id, false)
R.id.action_menu_msg_reply -> presenter.citeMessage(roomType, roomName, id, true)
R.id.action_menu_msg_copy -> presenter.copyMessage(id)
R.id.action_menu_msg_edit -> presenter.editMessage(roomId, id, getOriginalMessage())
R.id.action_menu_msg_pin_unpin -> {
with(item) {
if (!isChecked) {
presenter.pinMessage(id)
} else {
presenter.unpinMessage(id)
}
}
}
else -> TODO("Not implemented")
}
}
return true
}
private fun bindAttachment(message: MessageViewModel,
......@@ -118,7 +181,7 @@ class ChatRoomAdapter(private val serverUrl: String) : RecyclerView.Adapter<Chat
}
}
private fun bindUserAvatar(message: MessageViewModel, drawee: SimpleDraweeView, imageUnknownAvatar: ImageView) = message.getAvatarUrl(serverUrl).let {
private fun bindUserAvatar(message: MessageViewModel, drawee: SimpleDraweeView, imageUnknownAvatar: ImageView) = message.getAvatarUrl().let {
drawee.setImageURI(it.toString())
drawee.setVisible(true)
imageUnknownAvatar.setVisible(false)
......
package chat.rocket.android.chatroom.ui
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import android.view.*
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.showToast
import chat.rocket.android.util.textContent
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_composer.*
import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Fragment {
return ChatRoomFragment().apply {
arguments = Bundle(1).apply {
......@@ -39,13 +43,16 @@ private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
class ChatRoomFragment : Fragment(), ChatRoomView {
@Inject lateinit var presenter: ChatRoomPresenter
@Inject lateinit var parser: MessageParser
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false
private lateinit var adapter: ChatRoomAdapter
private lateinit var actionSnackbar: ActionSnackbar
private var citation: String? = null
private var editingMessageId: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -60,6 +67,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_chat_room)
......@@ -68,6 +76,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
super.onViewCreated(view, savedInstanceState)
presenter.loadMessages(chatRoomId, chatRoomType)
setupComposer()
setupActionSnackbar()
}
override fun onDestroyView() {
......@@ -75,10 +84,29 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
super.onDestroyView()
}
override fun showMessages(dataSet: List<MessageViewModel>, serverUrl: String) {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.chatroom_actions, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_pinned_messages -> {
val intent = Intent(activity, PinnedMessagesActivity::class.java).apply {
putExtra(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putExtra(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
putExtra(BUNDLE_CHAT_ROOM_NAME, chatRoomName)
}
startActivity(intent)
}
}
return true
}
override fun showMessages(dataSet: List<MessageViewModel>) {
activity?.apply {
if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(serverUrl)
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter)
recycler_view.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
recycler_view.layoutManager = linearLayoutManager
......@@ -98,7 +126,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun sendMessage(text: String) {
if (!text.isBlank()) {
presenter.sendMessage(chatRoomId, text)
presenter.sendMessage(chatRoomId, text, editingMessageId)
}
}
......@@ -120,23 +148,75 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
}
override fun dispatchUpdateMessage(index: Int, message: MessageViewModel) {
adapter.updateItem(index, message)
adapter.updateItem(message)
}
override fun dispatchDeleteMessage(msgId: String) {
adapter.removeItem(msgId)
}
override fun showReplyingAction(username: String, replyMarkdown: String, quotedMessage: String) {
activity?.apply {
citation = replyMarkdown
actionSnackbar.title = username
actionSnackbar.text = quotedMessage
actionSnackbar.show()
}
}
override fun showLoading() = view_loading.setVisible(true)
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
override fun showMessage(message: String) = showToast(message)
override fun showMessage(resId: Int) = showToast(resId)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun copyToClipboard(message: String) {
activity?.apply {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("", message))
}
}
override fun showEditingAction(roomId: String, messageId: String, text: String) {
activity?.apply {
actionSnackbar.title = getString(R.string.action_title_editing)
actionSnackbar.text = text
actionSnackbar.show()
text_message.textContent = text
editingMessageId = messageId
}
}
private fun setupComposer() {
if (isChatRoomReadOnly) {
text_room_is_read_only.setVisible(true)
top_container.setVisible(false)
} else {
text_send.setOnClickListener { sendMessage(text_message.textContent) }
text_send.setOnClickListener {
var textMessage = citation ?: ""
textMessage = textMessage + text_message.textContent
sendMessage(textMessage)
clearActionMessage()
}
}
}
private fun setupActionSnackbar() {
actionSnackbar = ActionSnackbar.make(message_list_container, parser = parser)
actionSnackbar.cancelView.setOnClickListener({
clearActionMessage()
})
}
private fun clearActionMessage() {
citation = null
editingMessageId = null
text_message.text.clear()
actionSnackbar.dismiss()
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.util.addFragment
import chat.rocket.android.util.textContent
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject
private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
class PinnedMessagesActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pinned_messages)
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
chatRoomName = intent.getStringExtra(INTENT_CHAT_ROOM_NAME)
requireNotNull(chatRoomName) { "no chat_room_name provided in Intent extras" }
chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE)
requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" }
setupToolbar()
addFragment("PinnedMessagesFragment", R.id.fragment_container) {
newPinnedMessagesFragment(chatRoomId, chatRoomName, chatRoomType)
}
}
override fun onBackPressed() = finishActivity()
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = getString(R.string.title_pinned_messages)
toolbar.setNavigationOnClickListener {
finishActivity()
}
}
private fun finishActivity() {
super.onBackPressed()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import android.support.v7.widget.RecyclerView
import android.text.method.LinkMovementMethod
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.AttachmentType
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.content
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisible
import chat.rocket.common.util.ifNull
import com.facebook.drawee.view.SimpleDraweeView
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
import kotlinx.android.synthetic.main.message_attachment.view.*
class PinnedMessagesAdapter : RecyclerView.Adapter<PinnedMessagesAdapter.ViewHolder>() {
init {
setHasStableIds(true)
}
val dataSet = ArrayList<MessageViewModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(parent.inflate(R.layout.item_message))
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(dataSet[position])
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
onBindViewHolder(holder, position)
}
override fun getItemCount(): Int = dataSet.size
override fun getItemViewType(position: Int): Int = position
fun addDataSet(dataSet: List<MessageViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(previousDataSetSize, dataSet)
notifyItemRangeInserted(previousDataSetSize, dataSet.size)
}
fun addItem(message: MessageViewModel) {
dataSet.add(0, message)
notifyItemInserted(0)
}
fun updateItem(message: MessageViewModel) {
val index = dataSet.indexOfFirst { it.id == message.id }
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
}
}
fun removeItem(messageId: String) {
val index = dataSet.indexOfFirst { it.id == messageId }
if (index > -1) {
dataSet.removeAt(index)
notifyItemRemoved(index)
}
}
override fun getItemId(position: Int): Long {
return dataSet[position].id.hashCode().toLong()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private lateinit var messageViewModel: MessageViewModel
fun bind(message: MessageViewModel) = with(itemView) {
messageViewModel = message
bindUserAvatar(message, image_avatar, image_unknown_avatar)
text_user_name.content = message.sender
text_message_time.content = message.time
text_content.content = message.content
text_content.movementMethod = LinkMovementMethod()
bindAttachment(message, message_attachment, image_attachment, audio_video_attachment,
file_name)
}
private fun bindAttachment(message: MessageViewModel,
attachment_container: View,
image_attachment: SimpleDraweeView,
audio_video_attachment: View,
file_name: TextView) {
with(message) {
if (attachmentUrl == null || attachmentType == null) {
attachment_container.setVisible(false)
return
}
var imageVisible = false
var videoVisible = false
attachment_container.setVisible(true)
when (message.attachmentType) {
is AttachmentType.Image -> {
imageVisible = true
image_attachment.setImageURI(message.attachmentUrl)
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
ImageViewer.Builder(view.context, listOf(message.attachmentUrl))
.setStartPosition(0)
.show()
}
}
is AttachmentType.Video,
is AttachmentType.Audio -> {
videoVisible = true
audio_video_attachment.setOnClickListener { view ->
message.attachmentUrl?.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
image_attachment.setVisible(imageVisible)
audio_video_attachment.setVisible(videoVisible)
file_name.text = message.attachmentTitle
}
}
private fun bindUserAvatar(message: MessageViewModel, drawee: SimpleDraweeView, imageUnknownAvatar: ImageView) {
message.getAvatarUrl().let {
drawee.setImageURI(it.toString())
drawee.setVisible(true)
imageUnknownAvatar.setVisible(false)
}.ifNull {
drawee.setVisible(false)
imageUnknownAvatar.setVisible(true)
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.PinnedMessagesPresenter
import chat.rocket.android.chatroom.presentation.PinnedMessagesView
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.showToast
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_pinned_messages.*
import javax.inject.Inject
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
private const val BUNDLE_CHAT_ROOM_NAME = "chat_room_name"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
fun newPinnedMessagesFragment(chatRoomId: String, chatRoomType: String, chatRoomName: String): Fragment {
return PinnedMessagesFragment().apply {
arguments = Bundle(3).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_NAME, chatRoomName)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
}
}
}
class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
@Inject lateinit var presenter: PinnedMessagesPresenter
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private lateinit var adapter: PinnedMessagesAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomName = bundle.getString(BUNDLE_CHAT_ROOM_NAME)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
inflater.inflate(R.layout.fragment_pinned_messages, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.loadPinnedMessages(chatRoomId)
}
override fun showLoading() = view_loading.setVisible(true)
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showPinnedMessages(pinnedMessages: List<MessageViewModel>) {
activity?.apply {
if (recycler_view_pinned.adapter == null) {
adapter = PinnedMessagesAdapter()
recycler_view_pinned.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view_pinned.layoutManager = linearLayoutManager
recycler_view_pinned.itemAnimator = DefaultItemAnimator()
if (pinnedMessages.size > 10) {
recycler_view_pinned.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
presenter.loadPinnedMessages(chatRoomId)
}
})
}
}
adapter.addDataSet(pinnedMessages)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui.bottomsheet
import android.support.design.widget.BottomSheetDialog
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.MenuItem
import ru.whalemare.sheetmenu.SheetMenu
import ru.whalemare.sheetmenu.adapter.MenuAdapter
class BottomSheetMenu(adapter: MenuAdapter) : SheetMenu(adapter = adapter) {
override fun processRecycler(recycler: RecyclerView, dialog: BottomSheetDialog) {
if (layoutManager == null) {
layoutManager = LinearLayoutManager(recycler.context, LinearLayoutManager.VERTICAL, false)
}
// Superclass SheetMenu adapter property is nullable MenuAdapter? but this class enforces
// passing one at the constructor, so we assume it's always non-null.
val adapter = adapter!!
val callback = adapter.callback
adapter.callback = MenuItem.OnMenuItemClickListener {
callback?.onMenuItemClick(it)
dialog.cancel()
true
}
recycler.adapter = adapter
recycler.layoutManager = layoutManager
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui.bottomsheet.adapter
import android.view.MenuItem
import chat.rocket.android.R
import chat.rocket.android.util.setVisible
/**
* An adapter for bottomsheet menu that lists all the actions that could be taken over a chat message.
*/
class ActionListAdapter(menuItems: List<MenuItem> = emptyList(), callback: MenuItem.OnMenuItemClickListener) :
ListBottomSheetAdapter(menuItems = menuItems, callback = callback) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = menuItems[position]
if (showIcons) {
holder.imageIcon.setVisible(item.icon != null)
} else {
holder.imageIcon.setVisible(false)
}
holder.imageIcon.setImageDrawable(item.icon)
holder.textTitle.text = item.title
holder.itemView.setOnClickListener {
callback?.onMenuItemClick(item)
}
val deleteTextColor = holder.itemView.context.resources.getColor(R.color.red)
val color = if (item.itemId == R.id.action_menu_msg_delete) deleteTextColor else textColors.get(item.itemId)
holder.textTitle.setTextColor(color)
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui.bottomsheet.adapter
import android.graphics.Color
import android.support.annotation.ColorInt
import android.support.annotation.IdRes
import android.util.SparseIntArray
import android.view.MenuItem
import chat.rocket.android.R
import ru.whalemare.sheetmenu.adapter.MenuAdapter
/**
* A regular bottomsheet adapter with added possibility to hide or show a menu item given its item id.
* Also added the possibility to change text colors for the menu items.
*/
open class ListBottomSheetAdapter(menuItems: List<MenuItem> = emptyList(), callback: MenuItem.OnMenuItemClickListener) :
MenuAdapter(menuItems = menuItems, callback = callback, itemLayoutId = R.layout.item_linear, showIcons = true) {
// Maps menu item ids to colors.
protected val textColors: SparseIntArray = SparseIntArray(menuItems.size)
init {
for (item in menuItems) {
textColors.put(item.itemId, Color.BLACK)
}
}
/**
* Hide a menu item and disable it.
*
* @param itemId The id of the menu item to disable and hide.
*/
fun hideMenuItem(@IdRes itemId: Int) {
menuItems.firstOrNull { it.itemId == itemId }?.apply {
setVisible(false)
setEnabled(false)
}
}
/**
* Show a menu item and enable it.
*
* @param itemId The id of the menu item to enable and show.
*/
fun showMenuItem(@IdRes itemId: Int) {
menuItems.firstOrNull { it.itemId == itemId }?.apply {
setVisible(true)
setEnabled(true)
}
}
/**
* Change a menu item text color given by its id to the given color.
*
* @param itemId The id of menu item.
* @param color The color (not the resource color id) of the menu item.
*/
fun setMenuItemTextColor(@IdRes itemId: Int, @ColorInt color: Int) {
val itemIndex = menuItems.indexOfFirst { it.itemId == itemId }
if (itemIndex > -1) {
textColors.put(itemId, color)
}
}
}
\ No newline at end of file
......@@ -5,33 +5,36 @@ import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import chat.rocket.android.R
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.domain.SITE_URL
import chat.rocket.android.server.domain.USE_REALNAME
import chat.rocket.android.server.domain.useRealName
import chat.rocket.common.model.Token
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType.*
import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.AudioAttachment
import chat.rocket.core.model.attachment.FileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.VideoAttachment
import chat.rocket.core.model.attachment.*
import chat.rocket.core.model.url.Url
import okhttp3.HttpUrl
import timber.log.Timber
data class MessageViewModel(val context: Context,
private val token: Token?,
private val message: Message,
private val settings: Map<String, Value<Any>>?,
private val settings: Map<String, Value<Any>>,
private val parser: MessageParser,
private val messagesRepository: MessagesRepository) {
private val messagesRepository: MessagesRepository,
private val localRepository: LocalRepository,
private val currentServerRepository: CurrentServerRepository) {
val id: String = message.id
val roomId: String = message.roomId
val time: CharSequence
val sender: CharSequence
val content: CharSequence
......@@ -40,12 +43,21 @@ data class MessageViewModel(val context: Context,
var attachmentUrl: String? = null
var attachmentTitle: CharSequence? = null
var attachmentType: AttachmentType? = null
var attachmentMessageText: String? = null
var attachmentMessageAuthor: String? = null
var attachmentMessageIcon: String? = null
var attachmentTimestamp: Long? = null
var isSystemMessage: Boolean = false
var isPinned: Boolean = false
var currentUsername: String? = null
init {
currentUsername = localRepository.get(LocalRepository.USERNAME_KEY)
sender = getSenderName()
time = getTime()
time = getTime(message.timestamp)
isPinned = message.pinned
val baseUrl = settings?.get(SITE_URL)
val baseUrl = settings.get(SITE_URL)
message.urls?.let {
if (it.isEmpty()) return@let
for (url in it) {
......@@ -62,9 +74,11 @@ data class MessageViewModel(val context: Context,
}
}
message.attachments?.let {
if (it.isEmpty() || it[0] == null) return@let
val attachment = it[0] as FileAttachment
message.attachments?.let { attachments ->
val attachment = attachments.firstOrNull()
if (attachments.isEmpty() || attachment == null) return@let
when (attachment) {
is FileAttachment -> {
baseUrl?.let {
attachmentUrl = attachmentUrl("${baseUrl.value}${attachment.url}")
attachmentTitle = attachment.title
......@@ -77,6 +91,15 @@ data class MessageViewModel(val context: Context,
}
}
}
is MessageAttachment -> {
attachmentType = AttachmentType.Message
attachmentMessageText = attachment.text ?: ""
attachmentMessageAuthor = attachment.author ?: ""
attachmentMessageIcon = attachment.icon
attachmentTimestamp = attachment.timestamp
}
}
}
content = getContent(context)
}
......@@ -84,28 +107,29 @@ data class MessageViewModel(val context: Context,
private fun makeQuote(quoteUrl: HttpUrl, serverUrl: HttpUrl) {
if (quoteUrl.host() == serverUrl.host()) {
val msgIdToQuote = quoteUrl.queryParameter("msg")
Timber.d("Will quote message Id: $msgIdToQuote")
if (msgIdToQuote != null) {
quote = messagesRepository.getById(msgIdToQuote)
}
}
}
fun getAvatarUrl(serverUrl: String): String? {
fun getAvatarUrl(): String? {
return message.sender?.username.let {
return@let UrlHelper.getAvatarUrl(serverUrl, it.toString())
return@let UrlHelper.getAvatarUrl(currentServerRepository.get()!!, it.toString())
}
}
/**
* Get the original message as a String.
*/
fun getOriginalMessage() = message.message
private fun getTime() = DateTimeHelper.getTime(DateTimeHelper.getLocalDateTime(message.timestamp))
private fun getTime(timestamp: Long) = DateTimeHelper.getTime(DateTimeHelper.getLocalDateTime(timestamp))
private fun getSenderName(): CharSequence {
val useRealName = settings?.get(USE_REALNAME)?.value as Boolean
val username = message.sender?.username
val realName = message.sender?.name
val senderName = if (useRealName) realName else username
val senderName = if (settings.useRealName()) realName else username
return senderName ?: username.toString()
}
......@@ -122,6 +146,8 @@ data class MessageViewModel(val context: Context,
context.getString(R.string.message_room_name_changed, message.message, message.sender?.username))
is UserRemoved -> contentMessage = getSystemMessage(
context.getString(R.string.message_user_removed_by, message.message, message.sender?.username))
is MessagePinned -> contentMessage = getSystemMessage(
context.getString(R.string.message_pinned))
else -> contentMessage = getNormalMessage()
}
return contentMessage
......@@ -131,18 +157,21 @@ data class MessageViewModel(val context: Context,
var quoteViewModel: MessageViewModel? = null
if (quote != null) {
val quoteMessage: Message = quote!!
quoteViewModel = MessageViewModel(context, token, quoteMessage, settings, parser, messagesRepository)
quoteViewModel = MessageViewModel(context, token, quoteMessage, settings, parser,
messagesRepository, localRepository, currentServerRepository)
}
return parser.renderMarkdown(message.message, quoteViewModel, urlsWithMeta)
return parser.renderMarkdown(message.message, quoteViewModel, currentUsername)
}
private fun getSystemMessage(content: String): CharSequence {
val spannableMsg = SpannableString(content)
isSystemMessage = true
val spannableMsg = SpannableStringBuilder(content)
spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length,
0)
spannableMsg.setSpan(ForegroundColorSpan(Color.GRAY), 0, spannableMsg.length,
0)
if (attachmentType == null) {
val username = message.sender?.username
val message = message.message
......@@ -160,10 +189,29 @@ data class MessageViewModel(val context: Context,
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), messageTextStartIndex, messageTextEndIndex,
0)
}
} else if (attachmentType == AttachmentType.Message) {
spannableMsg.append(quoteMessage(attachmentMessageAuthor!!, attachmentMessageText!!, attachmentTimestamp!!))
}
return spannableMsg
}
private fun quoteMessage(author: String, text: String, timestamp: Long): CharSequence {
return SpannableStringBuilder().apply {
val header = "\n$author ${getTime(timestamp)}\n"
append(SpannableString(header).apply {
setSpan(StyleSpan(Typeface.BOLD), 1, author.length + 1, 0)
setSpan(MessageParser.QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), 1, length, 0)
setSpan(AbsoluteSizeSpan(context.resources.getDimensionPixelSize(R.dimen.message_time_text_size)),
author.length + 1, length, 0)
})
append(SpannableString(parser.renderMarkdown(text)).apply {
setSpan(MessageParser.QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), 0, length, 0)
})
}
}
private fun attachmentUrl(url: String): String {
var response = url
val httpUrl = HttpUrl.parse(url)
......@@ -182,4 +230,5 @@ sealed class AttachmentType {
object Image : AttachmentType()
object Video : AttachmentType()
object Audio : AttachmentType()
object Message : AttachmentType()
}
\ No newline at end of file
......@@ -2,6 +2,8 @@ package chat.rocket.android.chatroom.viewmodel
import android.content.Context
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message
......@@ -13,20 +15,25 @@ import javax.inject.Inject
class MessageViewModelMapper @Inject constructor(private val context: Context,
private val tokenRepository: TokenRepository,
private val messageParser: MessageParser,
private val messagesRepository: MessagesRepository) {
private val messagesRepository: MessagesRepository,
private val localRepository: LocalRepository,
private val currentServerRepository: CurrentServerRepository) {
suspend fun mapToViewModel(message: Message, settings: Map<String, Value<Any>>?): MessageViewModel = withContext(CommonPool) {
suspend fun mapToViewModel(message: Message, settings: Map<String, Value<Any>>): MessageViewModel = withContext(CommonPool) {
MessageViewModel(
this@MessageViewModelMapper.context,
tokenRepository.get(),
message,
settings,
messageParser,
messagesRepository
messagesRepository,
localRepository,
currentServerRepository
)
}
suspend fun mapToViewModelList(messageList: List<Message>, settings: Map<String, Value<Any>>?): List<MessageViewModel> {
return messageList.map { MessageViewModel(context, tokenRepository.get(), it, settings, messageParser, messagesRepository) }
suspend fun mapToViewModelList(messageList: List<Message>, settings: Map<String, Value<Any>>): List<MessageViewModel> {
return messageList.map { MessageViewModel(context, tokenRepository.get(), it, settings,
messageParser, messagesRepository, localRepository, currentServerRepository) }
}
}
\ No newline at end of file
......@@ -38,11 +38,17 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
fun loadChatRooms() {
launchUI(strategy) {
view.showLoading()
try {
view.updateChatRooms(loadRooms())
subscribeRoomUpdates()
} catch (e: RocketChatException) {
Timber.e(e)
view.showMessage(e.message!!)
} finally {
view.hideLoading()
}
}
}
fun loadChatRoom(chatRoom: ChatRoom) = navigator.toChatRoom(chatRoom.id, chatRoom.name,
chatRoom.type.toString(), chatRoom.readonly ?: false)
......
......@@ -16,6 +16,7 @@ import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.showToast
import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection
......@@ -99,7 +100,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
package chat.rocket.android.core.behaviours
import android.support.annotation.StringRes
interface MessageView {
/**
* Show message given by resource id.
*
* @param resId The resource id on strings.xml of the message.
*/
fun showMessage(@StringRes resId: Int)
fun showMessage(message: String)
fun showGenericErrorMessage()
......
......@@ -7,11 +7,13 @@ import chat.rocket.android.authentication.signup.di.SignupFragmentProvider
import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider
import chat.rocket.android.chatroom.di.PinnedMessagesFragmentProvider
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.PinnedMessagesActivity
import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider
import chat.rocket.android.chatrooms.di.ChatRoomsModule
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.di.ProfileFragmentProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -35,4 +37,8 @@ abstract class ActivityBuilder {
@PerActivity
@ContributesAndroidInjector(modules = [ChatRoomFragmentProvider::class])
abstract fun bindChatRoomActivity(): ChatRoomActivity
@PerActivity
@ContributesAndroidInjector(modules = [PinnedMessagesFragmentProvider::class])
abstract fun bindPinnedMessagesActivity(): PinnedMessagesActivity
}
\ No newline at end of file
......@@ -224,4 +224,10 @@ class AppModule {
fun provideMessageParser(context: Application, configuration: SpannableConfiguration): MessageParser {
return MessageParser(context, configuration)
}
@Provides
@Singleton
fun providePermissionInteractor(settingsRepository: SettingsRepository, serverRepository: CurrentServerRepository): GetPermissionsInteractor {
return GetPermissionsInteractor(settingsRepository, serverRepository)
}
}
\ No newline at end of file
......@@ -15,7 +15,6 @@ import android.text.style.*
import android.view.View
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.core.model.url.Url
import org.commonmark.node.BlockQuote
import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder
......@@ -28,7 +27,9 @@ import javax.inject.Inject
class MessageParser @Inject constructor(val context: Application, private val configuration: SpannableConfiguration) {
private val parser = Markwon.createParser()
private val userPattern = Pattern.compile("(@[\\w.]+)",
private val regexUsername = Pattern.compile("([^\\S]|^)+(@[\\w.]+)",
Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
private val regexLink = Pattern.compile("(https?:\\/\\/)?(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&/=]*)",
Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
/**
......@@ -40,17 +41,24 @@ class MessageParser @Inject constructor(val context: Application, private val co
*
* @return A Spannable with the parsed markdown.
*/
fun renderMarkdown(text: String, quote: MessageViewModel?, urls: List<Url>): CharSequence {
fun renderMarkdown(text: String, quote: MessageViewModel? = null, selfUsername: String? = null): CharSequence {
val builder = SpannableBuilder()
var content: String = text
// Replace all url links to markdown url syntax.
for (url in urls) {
content = content.replace(url.url, "[${url.url}](${url.url})")
val matcher = regexLink.matcher(content)
val consumed = mutableListOf<String>()
while (matcher.find()) {
val link = matcher.group(0)
// skip usernames
if (!link.startsWith("@") && !consumed.contains(link)) {
content = content.replace(link, "[$link]($link)")
consumed.add(link)
}
}
val parentNode = parser.parse(toLenientMarkdown(content))
parentNode.accept(SpannableMarkdownVisitor(configuration, builder))
parentNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
quote?.apply {
var quoteNode = parser.parse("> $sender $time")
parentNode.appendChild(quoteNode)
......@@ -60,18 +68,22 @@ class MessageParser @Inject constructor(val context: Application, private val co
}
val result = builder.text()
applySpans(result)
applySpans(result, selfUsername)
return result
}
private fun applySpans(text: CharSequence) {
val matcher = userPattern.matcher(text)
private fun applySpans(text: CharSequence, currentUser: String?) {
val matcher = regexUsername.matcher(text)
val result = text as Spannable
while (matcher.find()) {
val user = matcher.group(1)
val start = matcher.start()
val user = matcher.group(2)
val start = matcher.start(2)
//TODO: should check if username actually exists prior to applying.
result.setSpan(UsernameClickableSpan(), start, start + user.length, 0)
val linkColor = context.resources.getColor(R.color.linkTextColor)
val linkBackgroundColor = context.resources.getColor(R.color.linkBackgroundColor)
val referSelf = currentUser != null && "@$currentUser" == user
val usernameSpan = UsernameClickableSpan(linkBackgroundColor, linkColor, referSelf)
result.setSpan(usernameSpan, start, start + user.length, 0)
}
}
......@@ -162,13 +174,23 @@ class MessageParser @Inject constructor(val context: Application, private val co
}
}
class UsernameClickableSpan : ClickableSpan() {
class UsernameClickableSpan(private val linkBackgroundColor: Int,
private val linkTextColor: Int,
private val referSelf: Boolean) : ClickableSpan() {
override fun onClick(widget: View) {
//TODO: Implement action when clicking on username, like showing user profile.
}
override fun updateDrawState(ds: TextPaint) {
ds.color = ds.linkColor
if (referSelf) {
ds.color = Color.WHITE
ds.typeface = Typeface.DEFAULT_BOLD
ds.bgColor = linkTextColor
} else {
ds.color = linkTextColor
ds.bgColor = linkBackgroundColor
}
ds.isUnderlineText = false
}
......
......@@ -6,6 +6,7 @@ interface LocalRepository {
const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
const val TOKEN_KEY = "token_"
const val SETTINGS_KEY = "settings_"
const val USERNAME_KEY = "my_username"
}
fun save(key: String, value: String?)
......
......@@ -20,5 +20,6 @@ class SharedPrefsLocalRepository(private val preferences: SharedPreferences) : L
clear(LocalRepository.KEY_PUSH_TOKEN)
clear(LocalRepository.TOKEN_KEY + server)
clear(LocalRepository.SETTINGS_KEY + server)
clear(LocalRepository.USERNAME_KEY + server)
}
}
\ No newline at end of file
......@@ -11,10 +11,7 @@ import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.presentation.ProfilePresenter
import chat.rocket.android.profile.presentation.ProfileView
import chat.rocket.android.util.getObservable
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
import chat.rocket.android.util.*
import dagger.android.support.AndroidSupportInjection
import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.app_bar.*
......@@ -79,7 +76,9 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
enableUserInput(true)
}
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
package chat.rocket.android.server.domain
import chat.rocket.core.model.ChatRoom
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject
class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
fun get(url: String) = repository.get(url)
suspend fun getByName(url: String, name: String): List<ChatRoom> {
val chatRooms = async {
/**
* Get a list of chat rooms that contains the name parameter.
*
* @param url The server url.
* @param name The name of chat room to look for or a chat room that contains this name.
* @return A list of ChatRoom objects with the given name.
*/
suspend fun getByName(url: String, name: String): List<ChatRoom> = withContext(CommonPool) {
val allChatRooms = repository.get(url)
if (name.isEmpty()) {
return@async allChatRooms
return@withContext allChatRooms
}
return@async allChatRooms.filter {
return@withContext allChatRooms.filter {
it.name.contains(name, true)
}
}
return chatRooms.await()
/**
* Get a specific room by its id.
*
* @param serverUrl The server url where the room is.
* @param roomId The id of the room to get.
* @return The ChatRoom object or null if we couldn't find any.
*/
suspend fun getById(serverUrl: String, roomId: String): ChatRoom? = withContext(CommonPool) {
val allChatRooms = repository.get(serverUrl)
return@withContext allChatRooms.first {
it.id == roomId
}
}
}
\ No newline at end of file
package chat.rocket.android.server.domain
import javax.inject.Inject
class GetPermissionsInteractor @Inject constructor(private val settingsRepository: SettingsRepository,
private val currentServerRepository: CurrentServerRepository) {
private fun publicSettings(): PublicSettings? = settingsRepository.get(currentServerRepository.get()!!)
/**
* Check whether user is allowed to delete a message.
*/
fun allowedMessageDeleting() = publicSettings()?.allowedMessageDeleting() ?: false
/**
* Checks whether user is allowed to edit a message.
*/
fun allowedMessageEditing() = publicSettings()?.allowedMessageEditing() ?: false
/**
* Checks whether user is allowed to pin a message to a channel.
*/
fun allowedMessagePinning() = publicSettings()?.allowedMessagePinning() ?: false
/**
* Checks whether should show deleted message status.
*/
fun showDeletedStatus() = publicSettings()?.showDeletedStatus() ?: false
/**
* Checks whether should show edited message status.
*/
fun showEditedStatus() = publicSettings()?.showEditedStatus() ?: false
}
\ No newline at end of file
......@@ -2,9 +2,11 @@ package chat.rocket.android.server.domain
import chat.rocket.core.model.Value
typealias PublicSettings = Map<String, Value<Any>>
interface SettingsRepository {
fun save(url: String, settings: Map<String, Value<Any>>)
fun get(url: String): Map<String, Value<Any>>?
fun save(url: String, settings: PublicSettings)
fun get(url: String): PublicSettings?
}
const val ACCOUNT_FACEBOOK = "Accounts_OAuth_Facebook"
......@@ -32,6 +34,11 @@ const val HIDE_USER_LEAVE = "Message_HideType_ul"
const val HIDE_TYPE_AU = "Message_HideType_au"
const val HIDE_TYPE_RU = "Message_HideType_ru"
const val HIDE_MUTE_UNMUTE = "Message_HideType_mute_unmute"
const val ALLOW_MESSAGE_DELETING = "Message_AllowDeleting"
const val ALLOW_MESSAGE_EDITING = "Message_AllowEditing"
const val SHOW_DELETED_STATUS = "Message_ShowDeletedStatus"
const val SHOW_EDITED_STATUS = "Message_ShowEditedStatus"
const val ALLOW_MESSAGE_PINNING = "Message_AllowPinning"
/*
* Extension functions for Public Settings.
*
......@@ -47,6 +54,15 @@ fun Map<String, Value<Any>>.twitterEnabled(): Boolean = this[ACCOUNT_TWITTER]?.v
fun Map<String, Value<Any>>.gitlabEnabled(): Boolean = this[ACCOUNT_GITLAB]?.value == true
fun Map<String, Value<Any>>.wordpressEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun Map<String, Value<Any>>.useRealName(): Boolean = this[USE_REALNAME]?.value == true
// Message settings
fun Map<String, Value<Any>>.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true
fun Map<String, Value<Any>>.showEditedStatus(): Boolean = this[SHOW_EDITED_STATUS]?.value == true
fun Map<String, Value<Any>>.allowedMessagePinning(): Boolean = this[ALLOW_MESSAGE_PINNING]?.value == true
fun Map<String, Value<Any>>.allowedMessageEditing(): Boolean = this[ALLOW_MESSAGE_EDITING]?.value == true
fun Map<String, Value<Any>>.allowedMessageDeleting(): Boolean = this[ALLOW_MESSAGE_DELETING]?.value == true
fun Map<String, Value<Any>>.registrationEnabled(): Boolean {
val value = this[ACCOUNT_REGISTRATION]
return value?.value == "Public"
......
......@@ -2,9 +2,11 @@ package chat.rocket.android.util
import android.app.Activity
import android.content.Context
import android.support.annotation.StringRes
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import chat.rocket.android.R
fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) {
......@@ -27,3 +29,9 @@ fun Activity.hideKeyboard() {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(currentFocus.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN)
}
fun Activity.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration)
fun Activity.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = Toast.makeText(this, message, duration).show()
fun Fragment.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration)
fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = activity!!.showToast(message, duration)
\ No newline at end of file
<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="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"
android:fillColor="@color/actionMenuColor"/>
</vector>
<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="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"
android:fillColor="#FF0000"/>
</vector>
<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="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"
android:fillColor="@color/actionMenuColor"/>
</vector>
<vector android:height="24dp" android:viewportHeight="800.0"
android:viewportWidth="800.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/actionMenuColor" android:pathData="M511.8,390H520V84C567,67.8 596.3,42 596.3,16H203.7c0,26 29.3,51.8 76.3,68V390h8.2c-92.9,16 -157.5,61 -157.5,108H377v288h48V498h244.3C669.3,451 604.7,406 511.8,390z"/>
</vector>
<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="M6,17h3l2,-4L11,7L5,7v6h3zM14,17h3l2,-4L19,7h-6v6h3z"
android:fillColor="@color/actionMenuColor"/>
</vector>
<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="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z"
android:fillColor="@color/actionMenuColor"/>
</vector>
<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="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"
android:fillColor="@color/actionMenuColor"/>
</vector>
<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="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"
android:fillColor="@color/actionMenuColor"/>
</vector>
......@@ -3,7 +3,6 @@
android:shape="rectangle">
<solid android:color="@color/darkGray" />
<corners android:radius="2dp" />
<size
android:width="4dp"
android:height="4dp" />
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/AppTheme"
tools:context=".chatroom.ui.PinnedMessagesActivity">
<include
android:id="@+id/layout_app_bar"
layout="@layout/app_bar_chat_room" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/layout_app_bar" />
</RelativeLayout>
\ No newline at end of file
......@@ -6,13 +6,20 @@
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/message_list_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/layout_message_composer">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/layout_message_composer"
android:scrollbars="vertical" />
</FrameLayout>
<include
android:id="@+id/layout_message_composer"
layout="@layout/message_composer"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view_pinned"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="48dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:visibility="gone"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="@+id/recycler_view_pinned"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary">
<TextView
android:id="@+id/text_view_action_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/white"
android:textSize="14sp"
android:typeface="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_view_action_cancel_quote"
app:layout_constraintTop_toBottomOf="@+id/text_view_action_title"
tools:text="action text" />
<ImageView
android:id="@+id/image_view_action_cancel_quote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_close_white_24dp" />
<TextView
android:id="@+id/text_view_action_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textColor="@color/colorAccent"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/text_view_action_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_view_action_cancel_quote"
app:layout_constraintTop_toTopOf="parent"
tools:text="Edit message" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_pinned_messages"
android:title="@string/title_pinned_messages"
app:showAsAction="never" />
</menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:id="@+id/common_actions">
<item
android:id="@+id/action_menu_msg_reply"
android:icon="@drawable/ic_reply_black_24px"
android:title="@string/action_msg_reply" />
<item
android:id="@+id/action_menu_msg_quote"
android:icon="@drawable/ic_quote_black_24px"
android:title="@string/action_msg_quote" />
<item
android:id="@+id/action_menu_msg_edit"
android:icon="@drawable/ic_edit_black_24px"
android:title="@string/action_msg_edit" />
<item
android:id="@+id/action_menu_msg_copy"
android:icon="@drawable/ic_content_copy_black_24px"
android:title="@string/action_msg_copy" />
<!--<item-->
<!--android:id="@+id/action_menu_msg_share"-->
<!--android:icon="@drawable/ic_share_black_24px"-->
<!--android:title="@string/action_msg_share" />-->
<item
android:id="@+id/action_menu_msg_pin_unpin"
android:icon="@drawable/ic_pin_black_24dp"
android:title="@string/action_msg_pin" />
<!--<item-->
<!--android:id="@+id/action_menu_msg_star"-->
<!--android:icon="@drawable/ic_star_black_24px"-->
<!--android:title="@string/action_msg_star" />-->
</group>
<group android:id="@+id/dangerous_actions">
<item
android:id="@+id/action_menu_msg_delete"
android:icon="@drawable/ic_delete_black_24px"
android:title="@string/action_msg_delete" />
</group>
</menu>
\ No newline at end of file
......@@ -55,5 +55,26 @@
<string name="message_user_joined_channel">Entrou no sala.</string>
<string name="message_welcome">Bem-vindo, %s</string>
<string name="message_removed">Mensagem removida</string>
<string name="message_pinned">Pinou uma mensagem:</string>
<!-- Message actions -->
<string name="action_msg_reply">Responder</string>
<string name="action_msg_edit">Editar</string>
<string name="action_msg_copy">Copiar</string>
<string name="action_msg_quote">Citar</string>
<string name="action_msg_delete">Remover</string>
<string name="action_msg_pin">Fixar Mensagem</string>
<string name="action_msg_unpin">Desafixar Messagem</string>
<string name="action_msg_star">Favoritar Mensagem</string>
<string name="action_msg_share">Compartilhar</string>
<string name="action_title_editing">Editando Mensagem</string>
<!-- Permission messages -->
<string name="permission_editing_not_allowed">Edição não permitida</string>
<string name="permission_deleting_not_allowed">Remoção não permitida</string>
<string name="permission_pinning_not_allowed">Fixar não permitido</string>
<!-- Pinned Messages -->
<string name="title_pinned_messages">Mensagens Pinadas</string>
</resources>
\ No newline at end of file
......@@ -17,6 +17,11 @@
<color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
<color name="red">#FFFF0000</color>
<color name="darkGray">#a0a0a0</color>
<color name="actionMenuColor">#727272</color>
<color name="linkTextColor">#FF074481</color>
<color name="linkBackgroundColor">#30074481</color>
</resources>
\ No newline at end of file
......@@ -57,5 +57,26 @@
<string name="message_user_joined_channel">Has joined the channel.</string>
<string name="message_welcome">Welcome %s</string>
<string name="message_removed">Message removed</string>
<string name="message_pinned">Pinned a message:</string>
<!-- Message actions -->
<string name="action_msg_reply">Reply</string>
<string name="action_msg_edit">Edit</string>
<string name="action_msg_copy">Copy</string>
<string name="action_msg_quote">Quote</string>
<string name="action_msg_delete">Delete</string>
<string name="action_msg_pin">Pin Message</string>
<string name="action_msg_unpin">Unpin Message</string>
<string name="action_msg_star">Star Message</string>
<string name="action_msg_share">Share</string>
<string name="action_title_editing">Editing Message</string>
<!-- Permission messages -->
<string name="permission_editing_not_allowed">Editing is not allowed</string>
<string name="permission_deleting_not_allowed">Deleting is not allowed</string>
<string name="permission_pinning_not_allowed">Pinning is not allowed</string>
<!-- Pinned Messages -->
<string name="title_pinned_messages">Pinned Messages</string>
</resources>
\ No newline at end of file
......@@ -30,6 +30,7 @@ ext {
markwon : '1.0.3',
textDrawable : '1.0.2', // Remove this library after https://github.com/RocketChat/Rocket.Chat/pull/9492 is merged
moshiLazyAdapters : '2.1', // Even declared on the SDK we need to add this library here because java.lang.NoClassDefFoundError: Failed resolution of: Lcom/serjltt/moshi/adapters/FallbackEnum;
sheetMenu : '1.3.3',
// For testing
junit : '4.12',
......@@ -95,6 +96,8 @@ ext {
moshiLazyAdapters : "com.serjltt.moshi:moshi-lazy-adapters:${versions.moshiLazyAdapters}",
sheetMenu : "com.github.whalemare:sheetmenu:${versions.sheetMenu}",
// For testing
junit : "junit:junit:$versions.junit",
expressoCore : "com.android.support.test.espresso:espresso-core:${versions.expresso}",
......
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