Unverified Commit b9f79237 authored by Lucio Maciel's avatar Lucio Maciel Committed by GitHub

Merge branch 'develop' into fix-ghost-composer

parents 8706d0bc c9147d73
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="rocket_chat_images"
path="Android/data/chat.rocket.android.dev/files/Pictures" />
</paths>
\ No newline at end of file
......@@ -105,6 +105,15 @@
<meta-data
android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
</application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="chat.rocket.android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
package chat.rocket.android.authentication.server.ui
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
......@@ -11,6 +12,7 @@ import android.widget.ScrollView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.text.color
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
......@@ -51,6 +53,8 @@ class ServerFragment : Fragment(), ServerView {
lateinit var analyticsManager: AnalyticsManager
private var deepLinkInfo: LoginDeepLinkInfo? = null
private var protocol = "https://"
private var isDomainAppended = false
private var appendedText = ""
private lateinit var serverUrlDisposable: Disposable
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) {
......@@ -131,7 +135,7 @@ class ServerFragment : Fragment(), ServerView {
}
private fun setupOnClickListener() =
ui { _ ->
ui {
button_connect.setOnClickListener {
presenter.checkServer("$protocol${text_server_url.textContent.sanitize()}")
}
......@@ -244,17 +248,42 @@ class ServerFragment : Fragment(), ServerView {
private fun subscribeEditText() {
serverUrlDisposable = text_server_url.asObservable()
.filter { it.isNotBlank() }
.subscribe {
if ("$protocol${it.toString()}".isValidUrl()) {
enableButtonConnect()
} else {
disableButtonConnect()
}
}
.subscribe { processUserInput(it.toString()) }
}
private fun unsubscribeEditText() = serverUrlDisposable.dispose()
private fun processUserInput(text: String) {
if (text.last().toString() == "." && !isDomainAppended) {
addDomain()
} else if (isDomainAppended && text != appendedText) {
removeDomain()
}
if ("$protocol$text".isValidUrl()) {
enableButtonConnect()
} else {
disableButtonConnect()
}
}
private fun addDomain() {
val cursorPosition = text_server_url.length()
text_server_url.append(SpannableStringBuilder()
.color(R.color.colorAuthenticationSecondaryText) { append("rocket.chat") })
text_server_url.setSelection(cursorPosition)
appendedText = text_server_url.text.toString()
isDomainAppended = true
}
private fun removeDomain() {
text_server_url.setText(
text_server_url.text.toString().substring(0, text_server_url.selectionEnd)
)
text_server_url.setSelection(text_server_url.length())
isDomainAppended = false
}
private fun enableUserInput() {
enableButtonConnect()
text_server_url.isEnabled = true
......
package chat.rocket.android.chatroom.di
import android.app.Application
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.ui.ChatRoomFragment
import chat.rocket.android.chatrooms.adapter.RoomUiModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.db.UserDao
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetCurrentUserInteractor
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
......@@ -42,4 +49,30 @@ class ChatRoomFragmentModule {
@Provides
@PerFragment
fun provideChatRoomDao(manager: DatabaseManager): ChatRoomDao = manager.chatRoomDao()
@Provides
@PerFragment
fun provideUserDao(manager: DatabaseManager): UserDao = manager.userDao()
@Provides
@PerFragment
fun provideGetCurrentUserInteractor(
tokenRepository: TokenRepository,
@Named("currentServer") serverUrl: String,
userDao: UserDao
): GetCurrentUserInteractor {
return GetCurrentUserInteractor(tokenRepository, serverUrl, userDao)
}
@Provides
@PerFragment
fun provideRoomMapper(
context: Application,
repository: SettingsRepository,
userInteractor: GetCurrentUserInteractor,
@Named("currentServer") serverUrl: String,
permissionsInteractor: PermissionsInteractor
): RoomUiModelMapper {
return RoomUiModelMapper(context, repository.get(serverUrl), userInteractor, serverUrl, permissionsInteractor)
}
}
......@@ -5,6 +5,7 @@ import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.EmojiSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.socket.model.State
......@@ -131,12 +132,7 @@ interface ChatRoomView : LoadingView, MessageView {
fun populateEmojiSuggestions(emojis: List<EmojiSuggestionUiModel>)
/**
* This user has joined the chat callback.
*
* @param userCanPost Whether the user can post a message or not.
*/
fun onJoined(userCanPost: Boolean)
fun onJoined(roomUiModel: RoomUiModel)
fun showReactionsPopup(messageId: String)
......@@ -147,9 +143,6 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun populateCommandSuggestions(commands: List<CommandSuggestionUiModel>)
/**
* Communicate whether it's a broadcast channel and if current user can post to it.
*/
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean)
fun onRoomUpdated(roomUiModel: RoomUiModel)
}
......@@ -8,8 +8,10 @@ import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.provider.MediaStore
import android.text.SpannableStringBuilder
import android.view.KeyEvent
import android.view.LayoutInflater
......@@ -23,6 +25,7 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.core.content.FileProvider
import androidx.core.text.bold
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
......@@ -48,6 +51,7 @@ import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.EmojiSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.draw.main.ui.DRAWING_BYTE_ARRAY_EXTRA_DATA
import chat.rocket.android.draw.main.ui.DrawingActivity
import chat.rocket.android.emoji.ComposerEditText
......@@ -73,6 +77,8 @@ import chat.rocket.android.util.extensions.rotateBy
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.extension.createImageFile
import chat.rocket.android.util.extensions.getBitmpap
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import chat.rocket.core.internal.realtime.socket.model.State
......@@ -86,6 +92,9 @@ import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.*
import timber.log.Timber
import java.io.File
import java.io.IOException
import kotlinx.android.synthetic.main.reaction_praises_list_item.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
......@@ -125,6 +134,7 @@ private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val REQUEST_CODE_FOR_PERFORM_SAF = 42
private const val REQUEST_CODE_FOR_DRAW = 101
private const val REQUEST_CODE_FOR_PERFORM_CAMERA = 102
private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val BUNDLE_CHAT_ROOM_IS_SUBSCRIBED = "chat_room_is_subscribed"
private const val BUNDLE_CHAT_ROOM_IS_CREATOR = "chat_room_is_creator"
......@@ -196,6 +206,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
internal val description by lazy { dialogView.findViewById<EditText>(R.id.text_file_description) }
internal val audioVideoAttachment by lazy { dialogView.findViewById<FrameLayout>(R.id.audio_video_attachment) }
internal val textFile by lazy { dialogView.findViewById<TextView>(R.id.text_file_name) }
private var takenPhotoUri: Uri? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -284,12 +295,19 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (resultData != null && resultCode == Activity.RESULT_OK) {
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_CODE_FOR_PERFORM_SAF -> showFileAttachmentDialog(resultData.data)
REQUEST_CODE_FOR_DRAW -> showDrawAttachmentDialog(
resultData.getByteArrayExtra(DRAWING_BYTE_ARRAY_EXTRA_DATA)
)
REQUEST_CODE_FOR_PERFORM_CAMERA -> takenPhotoUri?.let { uri ->
uri.getBitmpap(requireContext())?.let { bitmap ->
presenter.uploadImage(chatRoomId, "image/png", uri, bitmap, "")
}
}
REQUEST_CODE_FOR_PERFORM_SAF -> resultData?.data?.let {
showFileAttachmentDialog(it)
}
REQUEST_CODE_FOR_DRAW -> resultData?.getByteArrayExtra(DRAWING_BYTE_ARRAY_EXTRA_DATA)?.let {
showDrawAttachmentDialog(it)
}
}
}
}
......@@ -384,17 +402,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
empty_chat_view.isVisible = adapter.itemCount == 0
}
override fun onRoomUpdated(
userCanPost: Boolean,
channelIsBroadcast: Boolean,
userCanMod: Boolean
) {
override fun onRoomUpdated(roomUiModel: RoomUiModel) {
// TODO: We should rely solely on the user being able to post, but we cannot guarantee
// that the "(channels|groups).roles" endpoint is supported by the server in use.
ui {
setupMessageComposer(userCanPost)
isBroadcastChannel = channelIsBroadcast
if (isBroadcastChannel && !userCanMod) {
setupToolbar(roomUiModel.name.toString())
setupMessageComposer(roomUiModel)
isBroadcastChannel = roomUiModel.broadcast
if (isBroadcastChannel && !roomUiModel.canModerate) {
disableMenu = true
activity?.invalidateOptionsMenu()
}
......@@ -772,12 +787,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun onJoined(userCanPost: Boolean) {
override fun onJoined(roomUiModel: RoomUiModel) {
ui {
input_container.isVisible = true
button_join_chat.isVisible = false
isSubscribed = true
setupMessageComposer(userCanPost)
}
}
......@@ -810,8 +824,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
private fun setupMessageComposer(canPost: Boolean) {
if (!canPost) {
private fun setupMessageComposer(roomUiModel: RoomUiModel) {
if (isReadOnly || !roomUiModel.writable) {
text_room_is_read_only.isVisible = true
input_container.isVisible = false
text_room_is_read_only.setText(
......@@ -827,6 +841,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
button_join_chat.isVisible = true
button_join_chat.setOnClickListener { presenter.joinChat(chatRoomId) }
} else {
input_container.isVisible = true
text_room_is_read_only.isVisible = false
button_send.isVisible = false
button_show_attachment_options.alpha = 1f
button_show_attachment_options.isVisible = true
......@@ -883,7 +899,19 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
hideAttachmentOptions()
}
button_files.setOnClickListener {
button_add_reaction.setOnClickListener { _ ->
openEmojiKeyboardPopup()
}
button_take_a_photo.setOnClickListener {
dispatchTakePictureIntent()
handler.postDelayed({
hideAttachmentOptions()
}, 400)
}
button_attach_a_file.setOnClickListener {
handler.postDelayed({
presenter.selectFile()
}, 200)
......@@ -893,10 +921,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}, 400)
}
button_add_reaction.setOnClickListener {
openEmojiKeyboardPopup()
}
button_drawing.setOnClickListener {
activity?.let { fragmentActivity ->
if (!ImageHelper.canWriteToExternalStorage(fragmentActivity)) {
......@@ -914,6 +938,26 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
private fun dispatchTakePictureIntent() {
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
// Create the File where the photo should go
val photoFile: File? = try {
activity?.createImageFile()
} catch (ex: IOException) {
Timber.e(ex)
null
}
// Continue only if the File was successfully created
photoFile?.also {
takenPhotoUri = FileProvider.getUriForFile(
requireContext(), "chat.rocket.android.fileprovider", it
)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, takenPhotoUri)
startActivityForResult(takePictureIntent, REQUEST_CODE_FOR_PERFORM_CAMERA)
}
}
}
private fun getUnfinishedMessage() {
val unfinishedMessage = presenter.getUnfinishedMessage(chatRoomId)
if (unfinishedMessage.isNotBlank()) {
......
......@@ -8,9 +8,8 @@ import androidx.core.text.color
import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.db.model.ChatRoom
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.checkIfMyself
import chat.rocket.android.server.domain.GetCurrentUserInteractor
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.showLastMessage
import chat.rocket.android.server.domain.useRealName
......@@ -30,7 +29,8 @@ class RoomUiModelMapper(
private val context: Application,
private val settings: PublicSettings,
private val userInteractor: GetCurrentUserInteractor,
private val serverUrl: String
private val serverUrl: String,
private val permissions: PermissionsInteractor
) {
private val nameUnreadColor = ContextCompat.getColor(context, R.color.colorPrimaryText)
private val nameColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
......@@ -97,7 +97,9 @@ class RoomUiModelMapper(
avatar = serverUrl.avatarUrl(name!!, isGroupOrChannel = true),
lastMessage = if(showLastMessage) { mapLastMessage(lastMessage?.sender?.id, lastMessage?.sender?.username,
lastMessage?.sender?.name, lastMessage?.message,
isDirectMessage = type is RoomType.DirectMessage)} else { null }
isDirectMessage = type is RoomType.DirectMessage)} else { null },
muted = muted.orEmpty(),
writable = isChannelWritable(muted)
)
}
}
......@@ -133,11 +135,18 @@ class RoomUiModelMapper(
alert = isUnread,
lastMessage = lastMessageMarkdown,
status = status,
username = if (type is RoomType.DirectMessage) name else null
username = if (type is RoomType.DirectMessage) name else null,
muted = muted.orEmpty(),
writable = isChannelWritable(muted)
)
}
}
private fun isChannelWritable(muted: List<String>?): Boolean {
val canWriteToReadOnlyChannels = permissions.canPostToReadOnlyChannels()
return canWriteToReadOnlyChannels || !muted.orEmpty().contains(currentUser?.username)
}
private fun roomType(type: String): String {
val resources = context.resources
return when (type) {
......@@ -205,4 +214,4 @@ class RoomUiModelMapper(
}
}
}
}
\ No newline at end of file
}
......@@ -15,5 +15,8 @@ data class RoomUiModel(
val lastMessage: CharSequence? = null,
val status: UserStatus? = null,
val username: String? = null,
val broadcast: Boolean = false,
val canModerate: Boolean = false,
val writable: Boolean = true,
val muted: List<String> = emptyList()
)
......@@ -12,6 +12,7 @@ import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.UserDao
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentUserInteractor
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
......@@ -88,9 +89,10 @@ class ChatRoomsFragmentModule {
context: Application,
repository: SettingsRepository,
userInteractor: GetCurrentUserInteractor,
@Named("currentServer") serverUrl: String
@Named("currentServer") serverUrl: String,
permissionsInteractor: PermissionsInteractor
): RoomUiModelMapper {
return RoomUiModelMapper(context, repository.get(serverUrl), userInteractor, serverUrl)
return RoomUiModelMapper(context, repository.get(serverUrl), userInteractor, serverUrl, permissionsInteractor)
}
@Provides
......
......@@ -3,9 +3,12 @@ package chat.rocket.android.chatrooms.infrastructure
import androidx.lifecycle.LiveData
import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.model.ChatRoom
import chat.rocket.android.util.retryDB
import javax.inject.Inject
class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao){
class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao) {
// TODO - check how to use retryDB here - suspend
fun getChatRooms(order: Order): LiveData<List<ChatRoom>> {
return when(order) {
Order.ACTIVITY -> dao.getAll()
......@@ -15,9 +18,10 @@ class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao){
}
}
fun search(query: String) = dao.searchSync(query)
suspend fun search(query: String) =
retryDB("roomSearch($query)") { dao.searchSync(query) }
fun count() = dao.count()
suspend fun count() = retryDB("roomsCount") { dao.count() }
enum class Order {
ACTIVITY,
......
......@@ -13,6 +13,7 @@ import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.domain.useSpecialCharsOnRoom
import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryDB
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType
......@@ -41,11 +42,31 @@ class ChatRoomsPresenter @Inject constructor(
private val client = manager.client
private val settings = settingsRepository.get(currentServer)
fun loadChatRoom(roomId: String) {
launchUI(strategy) {
view.showLoadingRoom("")
try {
val room = dbManager.getRoom(roomId)
if (room != null) {
loadChatRoom(room.chatRoom, true)
} else {
Timber.d("Error loading channel")
view.showGenericErrorMessage()
}
} catch (ex: Exception) {
Timber.d(ex, "Error loading channel")
view.showGenericErrorMessage()
} finally {
view.hideLoadingRoom()
}
}
}
fun loadChatRoom(chatRoom: RoomUiModel) {
launchUI(strategy) {
view.showLoadingRoom(chatRoom.name)
try {
val room = dbManager.getRoom(chatRoom.id)
val room = retryDB("getRoom(${chatRoom.id}") { dbManager.getRoom(chatRoom.id) }
if (room != null) {
loadChatRoom(room.chatRoom, true)
} else {
......
......@@ -84,8 +84,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomId?.let {
// TODO - bring back support to load a room from id.
//presenter.goToChatRoomWithId(it)
presenter.loadChatRoom(it)
chatRoomId = null
}
}
......
......@@ -19,6 +19,7 @@ import chat.rocket.android.util.extensions.exhaustive
import chat.rocket.android.util.extensions.removeTrailingSlash
import chat.rocket.android.util.extensions.toEntity
import chat.rocket.android.util.extensions.userId
import chat.rocket.android.util.retryDB
import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.SimpleUser
......@@ -91,12 +92,14 @@ class DatabaseManager(val context: Application, val serverUrl: String) {
}
}
fun logout() {
database.clearAllTables()
suspend fun logout() {
retryDB("clearAllTables") { database.clearAllTables() }
}
suspend fun getRoom(id: String) = withContext(dbManagerContext) {
chatRoomDao().get(id)
retryDB("getRoom($id)") {
chatRoomDao().get(id)
}
}
fun processUsersBatch(users: List<User>) {
......@@ -151,7 +154,7 @@ class DatabaseManager(val context: Application, val serverUrl: String) {
fun updateSelfUser(myself: Myself) {
launch(dbManagerContext) {
val user = userDao().getUser(myself.id)
val user = retryDB("getUser(${myself.id})") { userDao().getUser(myself.id) }
val entity = user?.copy(
name = myself.name ?: user.name,
username = myself.username ?: user.username,
......@@ -335,7 +338,7 @@ class DatabaseManager(val context: Application, val serverUrl: String) {
}
private suspend fun updateRoom(data: Room): ChatRoomEntity? {
return chatRoomDao().get(data.id)?.let { current ->
return retryDB("getChatRoom(${data.id})") { chatRoomDao().get(data.id) }?.let { current ->
with(data) {
val chatRoom = current.chatRoom
......@@ -373,7 +376,7 @@ class DatabaseManager(val context: Application, val serverUrl: String) {
context.getString(R.string.msg_sent_attachment)
private suspend fun updateSubscription(data: Subscription): ChatRoomEntity? {
return chatRoomDao().get(data.roomId)?.let { current ->
return retryDB("getRoom(${data.roomId}") { chatRoomDao().get(data.roomId) }?.let { current ->
with(data) {
val userId = if (type is RoomType.DirectMessage) {
......@@ -539,39 +542,42 @@ class DatabaseManager(val context: Application, val serverUrl: String) {
}
}
private fun findUser(userId: String): String? = userDao().findUser(userId)
private suspend fun findUser(userId: String): String? =
retryDB("findUser($userId)") { userDao().findUser(userId) }
private fun doOperation(operation: Operation) {
when (operation) {
is Operation.ClearStatus -> userDao().clearStatus()
is Operation.UpdateRooms -> {
Timber.d("Running ChatRooms transaction: remove: ${operation.toRemove} - insert: ${operation.toInsert} - update: ${operation.toUpdate}")
private suspend fun doOperation(operation: Operation) {
retryDB(description = "doOperation($operation)") {
when (operation) {
is Operation.ClearStatus -> userDao().clearStatus()
is Operation.UpdateRooms -> {
Timber.d("Running ChatRooms transaction: remove: ${operation.toRemove} - insert: ${operation.toInsert} - update: ${operation.toUpdate}")
chatRoomDao().update(operation.toInsert, operation.toUpdate, operation.toRemove)
}
is Operation.InsertRooms -> {
chatRoomDao().insertOrReplace(operation.chatRooms)
}
is Operation.CleanInsertRooms -> {
chatRoomDao().cleanInsert(operation.chatRooms)
}
is Operation.InsertUsers -> {
val time = measureTimeMillis { userDao().upsert(operation.users) }
Timber.d("Upserted users batch(${operation.users.size}) in $time MS")
}
is Operation.InsertUser -> {
userDao().insert(operation.user)
}
is Operation.UpsertUser -> {
userDao().upsert(operation.user)
}
is Operation.InsertMessages -> {
messageDao().insert(operation.list)
}
is Operation.SaveLastSync -> {
messageDao().saveLastSync(operation.sync)
}
}.exhaustive
chatRoomDao().update(operation.toInsert, operation.toUpdate, operation.toRemove)
}
is Operation.InsertRooms -> {
chatRoomDao().insertOrReplace(operation.chatRooms)
}
is Operation.CleanInsertRooms -> {
chatRoomDao().cleanInsert(operation.chatRooms)
}
is Operation.InsertUsers -> {
val time = measureTimeMillis { userDao().upsert(operation.users) }
Timber.d("Upserted users batch(${operation.users.size}) in $time MS")
}
is Operation.InsertUser -> {
userDao().insert(operation.user)
}
is Operation.UpsertUser -> {
userDao().upsert(operation.user)
}
is Operation.InsertMessages -> {
messageDao().insert(operation.list)
}
is Operation.SaveLastSync -> {
messageDao().saveLastSync(operation.sync)
}
}.exhaustive
}
}
}
......
......@@ -5,6 +5,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryDB
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
......@@ -36,7 +37,7 @@ class FavoriteMessagesPresenter @Inject constructor(
view.showLoading()
dbManager.getRoom(roomId)?.let {
val favoriteMessages =
client.getFavoriteMessages(roomId, roomTypeOf(it.chatRoom.type), offset)
client.getFavoriteMessages(roomId, roomTypeOf(it.chatRoom.type), offset)
val messageList = mapper.map(favoriteMessages.result, asNotReversed = true)
view.showFavoriteMessages(messageList)
offset += 1 * 30
......
......@@ -7,6 +7,7 @@ import chat.rocket.android.files.uimodel.FileUiModel
import chat.rocket.android.files.uimodel.FileUiModelMapper
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryDB
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
......
......@@ -62,10 +62,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
setContentView(R.layout.activity_main)
refreshPushToken()
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
println("ChatRoomId: $chatRoomId")
presenter.clearNotificationsForChatroom(chatRoomId)
presenter.connect()
......
......@@ -6,6 +6,7 @@ import chat.rocket.android.members.uimodel.MemberUiModel
import chat.rocket.android.members.uimodel.MemberUiModelMapper
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryDB
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
......@@ -38,7 +39,7 @@ class MembersPresenter @Inject constructor(
view.showLoading()
dbManager.getRoom(roomId)?.let {
val members =
client.getMembers(roomId, roomTypeOf(it.chatRoom.type), offset, 60)
client.getMembers(roomId, roomTypeOf(it.chatRoom.type), offset, 60)
val memberUiModels = mapper.mapToUiModelList(members.result)
view.showMembers(memberUiModels, members.total)
offset += 1 * 60L
......
......@@ -5,6 +5,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryDB
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
......@@ -36,7 +37,7 @@ class PinnedMessagesPresenter @Inject constructor(
view.showLoading()
dbManager.getRoom(roomId)?.let {
val pinnedMessages =
client.getPinnedMessages(roomId, roomTypeOf(it.chatRoom.type), offset)
client.getPinnedMessages(roomId, roomTypeOf(it.chatRoom.type), offset)
val messageList = mapper.map(pinnedMessages.result, asNotReversed = true)
view.showPinnedMessages(messageList)
offset += 1 * 30
......
......@@ -29,7 +29,6 @@ import chat.rocket.core.internal.rest.setAvatar
import chat.rocket.core.internal.rest.updateProfile
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import java.lang.Exception
import java.util.*
import javax.inject.Inject
......@@ -58,20 +57,17 @@ class ProfilePresenter @Inject constructor(
) {
private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl)
private val myselfId = userHelper.user()?.id ?: ""
private var myselfName = userHelper.user()?.name ?: ""
private var myselfUsername = userHelper.username() ?: ""
private var myselfEmailAddress = userHelper.user()?.emails?.getOrNull(0)?.address ?: ""
private val user = userHelper.user()
fun loadUserProfile() {
launchUI(strategy) {
view.showLoading()
try {
view.showProfile(
serverUrl.avatarUrl(myselfUsername),
myselfName,
myselfUsername,
myselfEmailAddress
serverUrl.avatarUrl(user?.username ?: ""),
user?.name ?: "",
user?.username ?: "",
user?.emails?.getOrNull(0)?.address ?: ""
)
} catch (exception: RocketChatException) {
view.showMessage(exception)
......@@ -85,14 +81,16 @@ class ProfilePresenter @Inject constructor(
launchUI(strategy) {
view.showLoading()
try {
retryIO { client.updateProfile(myselfId, email, name, username) }
myselfEmailAddress = email
myselfName = name
myselfUsername = username
view.showProfileUpdateSuccessfullyMessage()
loadUserProfile()
user?.id?.let { id ->
retryIO { client.updateProfile(id, email, name, username) }
view.showProfileUpdateSuccessfullyMessage()
view.showProfile(
serverUrl.avatarUrl(user.username ?: ""),
name,
username,
email
)
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
......@@ -117,7 +115,7 @@ class ProfilePresenter @Inject constructor(
uriInteractor.getInputStream(uri)
}
}
view.reloadUserAvatar(serverUrl.avatarUrl(myselfUsername))
user?.username?.let { view.reloadUserAvatar(it) }
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
......@@ -144,7 +142,8 @@ class ProfilePresenter @Inject constructor(
byteArray?.inputStream()
}
}
view.reloadUserAvatar(serverUrl.avatarUrl(myselfUsername))
user?.username?.let { view.reloadUserAvatar(it) }
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
......@@ -161,8 +160,10 @@ class ProfilePresenter @Inject constructor(
launchUI(strategy) {
view.showLoading()
try {
retryIO { client.resetAvatar(myselfId) }
view.reloadUserAvatar(serverUrl.avatarUrl(myselfUsername))
user?.id?.let { id ->
retryIO { client.resetAvatar(id) }
}
user?.username?.let { view.reloadUserAvatar(it) }
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
......
......@@ -217,7 +217,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
hideUpdateAvatarOptions()
}
button_take_photo.setOnClickListener {
button_take_a_photo.setOnClickListener {
dispatchTakePicture(REQUEST_CODE_FOR_PERFORM_CAMERA)
hideUpdateAvatarOptions()
}
......
......@@ -296,11 +296,8 @@ class PushManager @Inject constructor(
}
private fun getContentIntent(context: Context, notificationId: Int, pushMessage: PushMessage, grouped: Boolean = false): PendingIntent {
val notificationIntent = context.changeServerIntent(pushMessage.info.host, chatRoomId = pushMessage.info.roomId)
// TODO - add support to go directly to the chatroom
/*if (!isGrouped) {
notificationIntent.putExtra(EXTRA_ROOM_ID, pushMessage.info.roomId)
}*/
val roomId = if (!grouped) pushMessage.info.roomId else null
val notificationIntent = context.changeServerIntent(pushMessage.info.host, chatRoomId = roomId)
return PendingIntent.getActivity(context, random.nextInt(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
......
......@@ -22,6 +22,7 @@ import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.Message
import chat.rocket.core.model.Myself
import chat.rocket.core.model.Room
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel
......@@ -45,6 +46,7 @@ class ConnectionManager(
private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>()
private val userDataChannels = ArrayList<Channel<Myself>>()
private val roomsChannels = LinkedHashMap<String, Channel<Room>>()
private val subscriptionIdMap = HashMap<String, String>()
private var subscriptionId: String? = null
......@@ -127,6 +129,18 @@ class ConnectionManager(
maxSize = 10) { batch ->
Timber.d("processing Stream batch: ${batch.size} - $batch")
dbManager.processChatRoomsBatch(batch)
batch.forEach {
//TODO - Do we need to handle Type.Removed and Type.Inserted here?
if (it.type == Type.Updated) {
if (it.data is Room) {
val room = it.data as Room
roomsChannels[it.data.id]?.let { channel ->
channel.offer(room)
}
}
}
}
}
val messagesActor = createBatchActor<Message>(messagesContext, parent = connectJob,
......@@ -241,6 +255,14 @@ class ConnectionManager(
fun removeUserDataChannel(channel: Channel<Myself>) = userDataChannels.remove(channel)
fun addRoomChannel(roomId: String, channel: Channel<Room>) {
roomsChannels[roomId] = channel
}
fun removeRoomChannel(roomId: String) {
roomsChannels.remove(roomId)
}
fun subscribeRoomMessages(roomId: String, channel: Channel<Message>) {
val oldSub = roomMessagesChannels.put(roomId, channel)
if (oldSub != null) {
......
......@@ -7,6 +7,7 @@ import chat.rocket.android.db.model.FullMessage
import chat.rocket.android.db.model.ReactionEntity
import chat.rocket.android.db.model.UrlEntity
import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.util.retryDB
import chat.rocket.common.model.SimpleRoom
import chat.rocket.common.model.SimpleUser
import chat.rocket.core.model.Message
......@@ -135,14 +136,18 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
with(attachment) {
val fields = if (hasFields) {
withContext(CommonPool) {
dbManager.messageDao().getAttachmentFields(attachment._id)
retryDB("getAttachmentFields(${attachment._id})") {
dbManager.messageDao().getAttachmentFields(attachment._id)
}
}.map { Field(it.title, it.value) }
} else {
null
}
val actions = if (hasActions) {
withContext(CommonPool) {
dbManager.messageDao().getAttachmentActions(attachment._id)
retryDB("getAttachmentActions(${attachment._id})") {
dbManager.messageDao().getAttachmentActions(attachment._id)
}
}.mapNotNull { mapAction(it) }
} else {
null
......@@ -183,29 +188,6 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
return list
}
/*private suspend fun mapColorAttachmentWithFields(entity: AttachmentEntity): ColorAttachment {
val fields = withContext(CommonPool) {
dbManager.messageDao().getAttachmentFields(entity._id)
}.map { Field(it.title, it.value) }
return with(entity) {
ColorAttachment(
color = Color.Custom(color ?: DEFAULT_COLOR_STR),
text = text ?: "",
fallback = fallback,
fields = fields)
}
}
private suspend fun mapActionAttachment(attachment: AttachmentEntity): ActionsAttachment {
val actions = withContext(CommonPool) {
dbManager.messageDao().getAttachmentActions(attachment._id)
}.mapNotNull { mapAction(it) }
return with(attachment) {
// TODO - remove the default "vertical" value from here...
ActionsAttachment(title, actions, buttonAlignment ?: "vertical")
}
}*/
private fun mapAction(action: AttachmentActionEntity): Action? {
return when (action.type) {
"button" -> ButtonAction(action.type, action.text, action.url, action.isWebView,
......@@ -214,13 +196,4 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
else -> null
}
}
/*private suspend fun mapAuthorAttachment(attachment: AttachmentEntity): AuthorAttachment {
val fields = withContext(CommonPool) {
dbManager.messageDao().getAttachmentFields(attachment._id)
}.map { Field(it.title, it.value) }
return with(attachment) {
AuthorAttachment(authorLink!!, authorIcon, authorName, fields)
}
}*/
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.Operation
import chat.rocket.android.db.model.MessagesSync
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.util.retryDB
import chat.rocket.core.model.Message
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
......@@ -14,25 +15,31 @@ class DatabaseMessagesRepository(
) : MessagesRepository {
override suspend fun getById(id: String): Message? = withContext(CommonPool) {
dbManager.messageDao().getMessageById(id)?.let { message -> mapper.map(message) }
retryDB("getMessageById($id)") {
dbManager.messageDao().getMessageById(id)?.let { message -> mapper.map(message) }
}
}
override suspend fun getByRoomId(roomId: String): List<Message> = withContext(CommonPool) {
// FIXME - investigate how to avoid this distinctBy here, since DAO is returning a lot of
// duplicate rows (something related to our JOINS and relations on Room)
dbManager.messageDao().getMessagesByRoomId(roomId)
.distinctBy { it.message.message.id }
.let { messages ->
mapper.map(messages)
}
retryDB("getMessagesByRoomId($roomId)") {
dbManager.messageDao().getMessagesByRoomId(roomId)
.distinctBy { it.message.message.id }
.let { messages ->
mapper.map(messages)
}
}
}
override suspend fun getRecentMessages(roomId: String, count: Long): List<Message> = withContext(CommonPool) {
dbManager.messageDao().getRecentMessagesByRoomId(roomId, count)
.distinctBy { it.message.message.id }
.let { messages ->
mapper.map(messages)
}
retryDB("getRecentMessagesByRoomId($roomId, $count)") {
dbManager.messageDao().getRecentMessagesByRoomId(roomId, count)
.distinctBy { it.message.message.id }
.let { messages ->
mapper.map(messages)
}
}
}
override suspend fun save(message: Message) {
......@@ -45,20 +52,24 @@ class DatabaseMessagesRepository(
override suspend fun removeById(id: String) {
withContext(CommonPool) {
dbManager.messageDao().delete(id)
retryDB("delete($id)") { dbManager.messageDao().delete(id) }
}
}
override suspend fun removeByRoomId(roomId: String) {
withContext(CommonPool) {
dbManager.messageDao().deleteByRoomId(roomId)
retryDB("deleteByRoomId($roomId)") {
dbManager.messageDao().deleteByRoomId(roomId)
}
}
}
override suspend fun getAllUnsent(): List<Message> = withContext(CommonPool) {
dbManager.messageDao().getUnsentMessages()
.distinctBy { it.message.message.id }
.let { mapper.map(it) }
retryDB("getUnsentMessages") {
dbManager.messageDao().getUnsentMessages()
.distinctBy { it.message.message.id }
.let { mapper.map(it) }
}
}
override suspend fun saveLastSyncDate(roomId: String, timeMillis: Long) {
......@@ -66,6 +77,8 @@ class DatabaseMessagesRepository(
}
override suspend fun getLastSyncDate(roomId: String): Long? = withContext(CommonPool) {
dbManager.messageDao().getLastSync(roomId)?.let { it.timestamp }
retryDB("getLastSync($roomId)") {
dbManager.messageDao().getLastSync(roomId)?.let { it.timestamp }
}
}
}
\ No newline at end of file
package chat.rocket.android.util
import android.database.sqlite.SQLiteDatabaseLockedException
import chat.rocket.common.RocketChatNetworkErrorException
import kotlinx.coroutines.experimental.TimeoutCancellationException
import kotlinx.coroutines.experimental.delay
......@@ -8,6 +9,7 @@ import timber.log.Timber
import kotlin.coroutines.experimental.coroutineContext
const val DEFAULT_RETRY = 3
private const val DEFAULT_DB_RETRY = 5
suspend fun <T> retryIO(
description: String = "<missing description>",
......@@ -32,6 +34,33 @@ suspend fun <T> retryIO(
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
return block() // last attempt
}
suspend fun <T> retryDB(
description: String = "<missing description>",
times: Int = DEFAULT_DB_RETRY,
initialDelay: Long = 100, // 0.1 second
maxDelay: Long = 500, // 0.5 second
factor: Double = 1.2,
block: suspend () -> T): T
{
var currentDelay = initialDelay
repeat(times - 1) { currentTry ->
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
try {
return block()
} catch (e: SQLiteDatabaseLockedException) {
Timber.d(e, "failed call($currentTry): $description")
e.printStackTrace()
}
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
return block() // last attempt
}
\ No newline at end of file
......@@ -6,14 +6,24 @@
android:orientation="vertical">
<Button
android:id="@+id/button_files"
android:id="@+id/button_take_a_photo"
style="?android:attr/borderlessButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="20dp"
android:drawableStart="@drawable/ic_photo_camera_black_24dp"
android:gravity="start|center"
android:text="@string/action_take_a_photo" />
<Button
android:id="@+id/button_attach_a_file"
style="?android:attr/borderlessButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="20dp"
android:drawableStart="@drawable/ic_files_24dp"
android:gravity="start|center"
android:text="@string/action_files" />
android:text="@string/action_attach_a_files" />
<Button
android:id="@+id/button_drawing"
......
......@@ -16,14 +16,14 @@
android:text="@string/action_select_photo_from_gallery" />
<Button
android:id="@+id/button_take_photo"
android:id="@+id/button_take_a_photo"
style="?android:attr/borderlessButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_photo_camera_black_24dp"
android:drawablePadding="20dp"
android:gravity="start|center"
android:text="@string/action_take_photo" />
android:text="@string/action_take_a_photo" />
<Button
android:id="@+id/button_reset_avatar"
......
......@@ -35,7 +35,7 @@
<string name="action_create_channel">Erstelle Raum</string>
<string name="action_create">Erstelle</string>
<string name="action_logout">Abmelden</string>
<string name="action_files">Dateien</string>
<string name="action_attach_a_files">Attach a file</string> <!-- TODO Add translation -->
<string name="action_confirm_password">Bestätige Passwort Änderung</string>
<string name="action_join_chat">Trete Chat bei</string>
<string name="action_add_account">Erstelle Account</string>
......@@ -46,7 +46,7 @@
<string name="action_drawing">Zeichnung</string>
<string name="action_save_to_gallery">Sichern in Gallerie</string>
<string name="action_select_photo_from_gallery">Select photo from gallery</string> <!-- TODO Add translation -->
<string name="action_take_photo">Select photo from gallery</string> <!-- TODO Add translation -->
<string name="action_take_a_photo">Take a photo from galery</string> <!-- TODO Add translation -->
<string name="action_reset_avatar">Reset avatar</string> <!-- TODO Add translation -->
<string name="action_connect_server">Connect with a server</string> <!-- TODO Add translation -->
<string name="action_join_community">Join in the community</string> <!-- TODO Add translation -->
......
......@@ -34,7 +34,7 @@
<string name="action_create_channel">Crear canal</string>
<string name="action_create">Create</string>
<string name="action_logout">Cerrar sesión</string>
<string name="action_files">Archivos</string>
<string name="action_attach_a_files">Attach a file</string> <!-- TODO Add translation -->
<string name="action_confirm_password">Confirmar cambio de contraseña</string>
<string name="action_join_chat">Unirse al chat</string>
<string name="action_add_account">Añadir cuenta</string>
......@@ -45,7 +45,7 @@
<string name="action_drawing">Dibujo</string>
<string name="action_save_to_gallery">Guardar en la galería</string>
<string name="action_select_photo_from_gallery">Select photo from gallery</string> <!-- TODO Add translation -->
<string name="action_take_photo">Select photo from gallery</string> <!-- TODO Add translation -->
<string name="action_take_a_photo">Take a photo</string> <!-- TODO Add translation -->
<string name="action_reset_avatar">Reset avatar</string> <!-- TODO Add translation -->
<string name="action_connect_server">Connect with a server</string> <!-- TODO Add translation -->
<string name="action_join_community">Join in the community</string> <!-- TODO Add translation -->
......
......@@ -35,7 +35,7 @@
<string name="action_create_channel">Créer salon</string>
<string name="action_create">Créer</string>
<string name="action_logout">Se déconnecter</string>
<string name="action_files">Fichiers</string>
<string name="action_attach_a_files">Attach a file</string> <!-- TODO Add translation -->
<string name="action_confirm_password">Confirmer le mot de passe</string>
<string name="action_join_chat">Rejoignez le chat</string>
<string name="action_add_account">Ajouter un compte</string>
......@@ -46,7 +46,7 @@
<string name="action_drawing">Dessin</string>
<string name="action_save_to_gallery">Sauvegarder vers la gallerie</string>
<string name="action_select_photo_from_gallery">Sélectionner depuis la gallerie</string>
<string name="action_take_photo">Prendre une photo</string>
<string name="action_take_a_photo">Prendre une photo</string>
<string name="action_reset_avatar">Réinitialiser l\'avatar</string>
<string name="action_connect_server">Connect with a server</string> <!-- TODO Add translation -->
<string name="action_join_community">Join in the community</string> <!-- TODO Add translation -->
......
......@@ -35,7 +35,7 @@
<string name="action_create_channel">चैनल बनाएं</string>
<string name="action_create">बनाएं</string>
<string name="action_logout">लोग आउट करें</string>
<string name="action_files">फ़ाइलें</string>
<string name="action_attach_a_files">Attach a file</string> <!-- TODO Add translation -->
<string name="action_confirm_password">पासवर्ड परिवर्तन की पुष्टि करें</string>
<string name="action_join_chat">चैट में शामिल हों</string>
<string name="action_add_account">खाता जोड़ो</string>
......@@ -46,7 +46,7 @@
<string name="action_save_to_gallery">गैलरी में सहेजें</string>
<string name="action_drawing">चित्रकारी</string>
<string name="action_select_photo_from_gallery">गैलरी से फोटो का चयन करें</string>
<string name="action_take_photo">फोटो खेचिये</string>
<string name="action_take_a_photo">फोटो खेचिये</string>
<string name="action_reset_avatar">अवतार रीसेट करें</string>
<string name="action_connect_server">सर्वर से कनेक्ट करें</string>
<string name="action_join_community">समुदाय में शामिल हों</string>
......
......@@ -35,7 +35,7 @@
<string name="action_create_channel">Crea canale</string>
<string name="action_create">Crea</string>
<string name="action_logout">Disconnettersi</string>
<string name="action_files">Documenti</string>
<string name="action_attach_a_files">Attach a file</string> <!-- TODO ADD translation -->
<string name="action_confirm_password">Conferma Cambio Password</string>
<string name="action_join_chat">Iscriviti alla stanza</string>
<string name="action_add_account">Aggiungi utente</string>
......@@ -46,7 +46,7 @@
<string name="action_drawing">Disegno</string>
<string name="action_save_to_gallery">Salva in galleria</string>
<string name="action_select_photo_from_gallery">Seleziona foto dalla galleria</string>
<string name="action_take_photo">Fare foto</string>
<string name="action_take_a_photo">Take a photo</string> <!-- TODO ADD translation -->
<string name="action_reset_avatar">Reimposta avatar</string>
<string name="action_connect_server">Collegati a un server</string>
<string name="action_join_community">Entra nella community</string>
......
......@@ -26,9 +26,6 @@
<string name="title_create_channel">新しいチャネルを作成</string>
<string name="title_are_you_sure">本気ですか?</string>
<!-- Actions -->
<string name="action_connect">接続</string>
<string name="action_use_this_username">このユーザー名を使用する</string>
......@@ -40,7 +37,7 @@
<string name="action_create_channel">チャンネル作成</string>
<string name="action_create">作ります</string>
<string name="action_logout">ログアウト</string>
<string name="action_files">ファイル</string>
<string name="action_attach_a_files">Attach a file</string> <!-- TODO Add translation -->
<string name="action_confirm_password">変更したパスワードの確認</string>
<string name="action_join_chat">チャットに参加</string>
<string name="action_add_account">サーバーの追加</string>
......@@ -51,7 +48,7 @@
<string name="action_drawing">絵を描く</string>
<string name="action_save_to_gallery">ギャラリーに保存</string>
<string name="action_select_photo_from_gallery">ギャラリーの写真を選択</string>
<string name="action_take_photo">写真を撮る</string>
<string name="action_take_a_photo">写真を撮る</string>
<string name="action_reset_avatar">アバターをリセット</string>
<string name="action_connect_server">サーバーに接続</string>
<string name="action_join_community">コミュニティに参加</string>
......
......@@ -35,7 +35,7 @@
<string name="action_create_channel">Criar chat</string>
<string name="action_create">Criar</string>
<string name="action_logout">Sair</string>
<string name="action_files">Arquivos</string>
<string name="action_attach_a_files">Anexar um arquivo</string>
<string name="action_confirm_password">Confirme a nova senha</string>
<string name="action_join_chat">Entrar no Chat</string>
<string name="action_add_account">Adicionar conta</string>
......@@ -46,7 +46,7 @@
<string name="action_drawing">Desenhando</string>
<string name="action_save_to_gallery">Salvar na galeria</string>
<string name="action_select_photo_from_gallery">Escolher foto da galeria</string>
<string name="action_take_photo">Tirar foto</string>
<string name="action_take_a_photo">Tirar uma foto</string>
<string name="action_reset_avatar">Resetar avatar</string>
<string name="action_connect_server">Conectar com um servidor</string>
<string name="action_join_community">Junte-se à comunidade</string>
......
......@@ -35,7 +35,7 @@
<string name="action_create_channel">Создать канал</string>
<string name="action_create">Создать</string>
<string name="action_logout">Выйти</string>
<string name="action_files">Файлы</string>
<string name="action_attach_a_files">Attach a file</string> <!-- TODO Add translation -->
<string name="action_confirm_password">Подтверждение изменения пароля</string>
<string name="action_join_chat">Присоединиться к чату</string>
<string name="action_add_account">Добавить аккаунт</string>
......@@ -46,7 +46,7 @@
<string name="action_drawing">Рисунок</string>
<string name="action_save_to_gallery">Сохранить в галерею</string>
<string name="action_select_photo_from_gallery">Выбрать из галереи</string>
<string name="action_take_photo">Сделать снимок</string>
<string name="action_take_a_photo">Сделать снимок</string>
<string name="action_reset_avatar">Восстановить аватар</string>
<string name="action_connect_server">Соединиться с сервером</string>
<string name="action_join_community">Сообщество</string>
......
......@@ -35,7 +35,7 @@
<string name="action_create_channel">Yeni Kanal Oluştur</string>
<string name="action_create">Oluştur</string>
<string name="action_logout">Çıkış Yap</string>
<string name="action_files">Dosyalar</string>
<string name="action_attach_a_files">Attach a file</string> <!-- TODO Add translation -->
<string name="action_confirm_password">Şifre Değişikliğini Onaylayın</string>
<string name="action_join_chat">Sohbete Bağlan</string>
<string name="action_add_account">Hesap Ekle</string>
......@@ -46,7 +46,7 @@
<string name="action_drawing">Çizim</string>
<string name="action_save_to_gallery">Galeriye kaydet</string>
<string name="action_select_photo_from_gallery">Galeriden resim seç</string>
<string name="action_take_photo">Fotoğraf çek</string>
<string name="action_take_a_photo">Fotoğraf çek</string>
<string name="action_reset_avatar">Avatarı kaldır</string>
<string name="action_connect_server">Connect with a server</string> <!-- TODO Add translation -->
<string name="action_join_community">Join in the community</string> <!-- TODO Add translation -->
......
......@@ -35,7 +35,7 @@
<string name="action_create_channel">Створити канал</string>
<string name="action_create">Створити</string>
<string name="action_logout">Вийти</string>
<string name="action_files">Файли</string>
<string name="action_attach_a_files">Attach a file</string> <!-- TODO Add translation -->
<string name="action_confirm_password">Підтвердження зміни пароля</string>
<string name="action_join_chat">Приєднатися до чату</string>
<string name="action_add_account">Додати аккаунт</string>
......@@ -46,7 +46,7 @@
<string name="action_drawing">Малюнок</string>
<string name="action_save_to_gallery">Зберегти до галереї</string>
<string name="action_select_photo_from_gallery">Вибрати з галереї</string>
<string name="action_take_photo">Зробити знімок</string>
<string name="action_take_a_photo">Зробити знімок</string>
<string name="action_reset_avatar">Відновити аватар</string>
<string name="action_connect_server">Connect with a server</string> <!-- TODO Add translation -->
<string name="action_join_community">Join in the community</string> <!-- TODO Add translation -->
......
......@@ -47,7 +47,7 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin
<string name="action_create_channel">Create channel</string>
<string name="action_create">Create</string>
<string name="action_logout">Logout</string>
<string name="action_files">Files</string>
<string name="action_attach_a_files">Attach a file</string>
<string name="action_confirm_password">Confirm Password Change</string>
<string name="action_join_chat">Join Chat</string>
<string name="action_add_account">Add account</string>
......@@ -58,7 +58,7 @@ https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md#strin
<string name="action_drawing">Drawing</string>
<string name="action_save_to_gallery">Save to gallery</string>
<string name="action_select_photo_from_gallery">Select photo from gallery</string>
<string name="action_take_photo">Take photo</string>
<string name="action_take_a_photo">Take a photo</string>
<string name="action_reset_avatar">Reset avatar</string>
<string name="action_connect_server">Connect with a server</string>
<string name="action_join_community">Join in the community</string>
......
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="rocket_chat_images"
path="Android/data/chat.rocket.android/files/Pictures" />
</paths>
\ No newline at end of file
package chat.rocket.android.push
import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.work.Constraints
import androidx.work.NetworkType
......@@ -23,9 +24,12 @@ class FirebaseMessagingService : FirebaseMessagingService() {
}
override fun onMessageReceived(message: RemoteMessage) {
// XXX - for now this is ok, if we start to do network calls, use a Worker instead
message.data?.let {
pushManager.handle(bundleOf(*(it.map { Pair(it.key, it.value) }).toTypedArray()))
message.data?.let { data ->
val bundle = Bundle()
data.entries.forEach { entry ->
bundle.putString(entry.key, entry.value)
}
pushManager.handle(bundle)
}
}
......
......@@ -10,7 +10,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
......@@ -22,6 +21,7 @@ import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.EmojiPagerAdapter
import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE
import com.google.android.material.tabs.TabLayout
import kotlinx.android.synthetic.main.dialog_skin_tone_chooser.view.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
......@@ -77,59 +77,50 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
}
private fun showSkinToneChooser() {
val view = LayoutInflater.from(context).inflate(R.layout.color_select_popup, null)
val view = LayoutInflater.from(context).inflate(R.layout.dialog_skin_tone_chooser, null)
val dialog = AlertDialog.Builder(context, R.style.Dialog)
.setView(view)
.setTitle(context.getString(R.string.alert_title_default_skin_tone))
.setCancelable(true)
.create()
view.findViewById<TextView>(R.id.default_tone_text).also {
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.Default)
}
with (view) {
image_view_default_tone.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.Default)
}
view.findViewById<TextView>(R.id.light_tone_text).also {
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.LightTone)
}
image_view_light_tone.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.LightTone)
}
view.findViewById<TextView>(R.id.medium_light_text).also {
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumLightTone)
}
image_view_medium_light.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumLightTone)
}
view.findViewById<TextView>(R.id.medium_tone_text).also {
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumTone)
}
image_view_medium_tone.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumTone)
}
view.findViewById<TextView>(R.id.medium_dark_tone_text).also {
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumDarkTone)
}
image_view_medium_dark_tone.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumDarkTone)
}
view.findViewById<TextView>(R.id.dark_tone_text).also {
it.text = EmojiParser.parse(context, it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.DarkTone)
image_view_dark_tone.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.DarkTone)
}
}
dialog.show()
}
private fun changeSkinTone(tone: Fitzpatrick) {
val drawable = ContextCompat.getDrawable(context, R.drawable.color_change_circle)!!
val drawable = ContextCompat.getDrawable(context, R.drawable.bg_skin_tone)!!
val wrappedDrawable = DrawableCompat.wrap(drawable)
DrawableCompat.setTint(wrappedDrawable, getFitzpatrickColor(tone))
(changeColorView as ImageView).setImageDrawable(wrappedDrawable)
......
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/tone_default" />
<size
android:width="24dp"
android:height="24dp" />
</shape>
\ No newline at end of file
</shape>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/default_tone_text"
<ImageView
android:id="@+id/image_view_default_tone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:text="👌"
android:textSize="32sp"
android:layout_marginBottom="16dp"
android:tint="@color/tone_default"
app:layout_constraintEnd_toStartOf="@+id/light_tone_text"
app:layout_constraintBottom_toTopOf="@+id/image_view_medium_tone"
app:layout_constraintEnd_toStartOf="@+id/image_view_light_tone"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle" />
app:srcCompat="@drawable/bg_skin_tone" />
<TextView
android:id="@+id/light_tone_text"
<ImageView
android:id="@+id/image_view_light_tone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="👌🏻"
android:textSize="32sp"
android:tint="@color/tone_light"
app:layout_constraintEnd_toStartOf="@+id/medium_light_text"
app:layout_constraintEnd_toStartOf="@+id/image_view_medium_light"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/default_tone_text"
app:layout_constraintStart_toEndOf="@+id/image_view_default_tone"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
app:srcCompat="@drawable/bg_skin_tone" />
<TextView
android:id="@+id/medium_light_text"
<ImageView
android:id="@+id/image_view_medium_light"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="👌🏼"
android:textSize="32sp"
android:layout_marginEnd="24dp"
android:tint="@color/tone_medium_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/light_tone_text"
app:layout_constraintStart_toEndOf="@+id/image_view_light_tone"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
app:srcCompat="@drawable/bg_skin_tone" />
<TextView
android:id="@+id/medium_tone_text"
<ImageView
android:id="@+id/image_view_medium_tone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="👌🏽"
android:textSize="32sp"
android:tint="@color/tone_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/medium_dark_tone_text"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/default_tone_text"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
app:layout_constraintEnd_toStartOf="@+id/image_view_medium_dark_tone"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@+id/image_view_default_tone"
app:layout_constraintTop_toBottomOf="@+id/image_view_default_tone"
app:srcCompat="@drawable/bg_skin_tone" />
<TextView
android:id="@+id/medium_dark_tone_text"
<ImageView
android:id="@+id/image_view_medium_dark_tone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="16dp"
android:text="👌🏾"
android:textSize="32sp"
android:tint="@color/tone_medium_dark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/dark_tone_text"
app:layout_constraintBottom_toBottomOf="@+id/image_view_medium_tone"
app:layout_constraintEnd_toStartOf="@+id/image_view_dark_tone"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/medium_tone_text"
app:layout_constraintTop_toBottomOf="@+id/light_tone_text"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
app:layout_constraintStart_toEndOf="@+id/image_view_medium_tone"
app:layout_constraintTop_toTopOf="@+id/image_view_medium_tone"
app:srcCompat="@drawable/bg_skin_tone" />
<TextView
android:id="@+id/dark_tone_text"
<ImageView
android:id="@+id/image_view_dark_tone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="👌🏿"
android:textSize="32sp"
android:tint="@color/tone_dark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/medium_dark_tone_text"
app:layout_constraintTop_toBottomOf="@+id/medium_light_text"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
app:layout_constraintBottom_toBottomOf="@+id/image_view_medium_dark_tone"
app:layout_constraintEnd_toEndOf="@+id/image_view_medium_light"
app:layout_constraintStart_toEndOf="@+id/image_view_medium_dark_tone"
app:layout_constraintTop_toTopOf="@+id/image_view_medium_dark_tone"
app:srcCompat="@drawable/bg_skin_tone" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -43,7 +43,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle" />
app:srcCompat="@drawable/bg_skin_tone" />
<ImageView
android:id="@+id/emoji_search"
......
......@@ -2,13 +2,19 @@ package chat.rocket.android.util.extension
import android.content.Intent
import android.graphics.Bitmap
import android.os.Environment
import android.provider.MediaStore
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.IOException
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
/**
* Compress a [Bitmap] image.
......@@ -104,4 +110,16 @@ fun Fragment.dispatchTakePicture(requestCode: Int) {
if (takePictureIntent.resolveActivity(context?.packageManager) != null) {
startActivityForResult(takePictureIntent, requestCode)
}
}
@Throws(IOException::class)
fun FragmentActivity.createImageFile(): File {
// Create an image file name
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val storageDir: File = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(
"PNG_${timeStamp}_", /* prefix */
".png", /* suffix */
storageDir /* directory */
)
}
\ No newline at end of file
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