Unverified Commit 1a3b7ba9 authored by Filipe de Lima Brito's avatar Filipe de Lima Brito Committed by GitHub

Merge pull request #2150 from RocketChat/develop

[RELEASE] Merge DEVELOP into BETA
parents 3525893c 6f2f5c88
......@@ -2,7 +2,9 @@ def taskRequests = getGradle().getStartParameter().getTaskRequests().toString()
def isPlay = !(taskRequests.contains("Foss") || taskRequests.contains("foss"))
apply plugin: 'com.android.application'
if (isPlay) { apply plugin: 'io.fabric' }
if (isPlay) {
apply plugin: 'io.fabric'
}
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
......@@ -16,13 +18,12 @@ android {
applicationId "chat.rocket.android"
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
versionCode 2057
versionName "3.2.0"
versionCode 2058
versionName "3.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
def gitSha = 'git rev-parse --short HEAD'.execute([], project.rootDir).text.trim()
def buildTime = new GregorianCalendar().format("MM-dd-yyyy' 'h:mm:ss a z")
buildConfigField "String", "GIT_SHA", "\"${gitSha}\""
javaCompileOptions {
......@@ -30,6 +31,17 @@ android {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
// For Jitsi
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
// For Jitsi
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
signingConfigs {
......@@ -73,7 +85,7 @@ android {
dimension "type"
}
// only foss
// only FOSS
foss {
dimension "type"
}
......@@ -83,6 +95,10 @@ android {
exclude 'META-INF/core.kotlin_module'
exclude 'META-INF/main.kotlin_module'
}
lintOptions {
lintConfig file("src/main/res/xml/lint.xml")
}
}
dependencies {
......@@ -96,7 +112,7 @@ dependencies {
implementation project(':suggestions')
implementation libraries.kotlin
implementation libraries.coroutines
implementation libraries.coroutinesCore
implementation libraries.coroutinesAndroid
implementation libraries.appCompat
......@@ -123,6 +139,8 @@ dependencies {
implementation libraries.viewmodelKtx
implementation libraries.workmanager
implementation libraries.livedataKtx
implementation libraries.rxKotlin
implementation libraries.rxAndroid
......@@ -133,25 +151,25 @@ dependencies {
implementation libraries.timber
implementation libraries.threeTenABP
kapt libraries.kotshiCompiler
implementation libraries.kotshiApi
implementation libraries.fresco
api libraries.frescoOkHttp
implementation libraries.frescoAnimatedGif
implementation libraries.frescoWebP
implementation libraries.frescoAnimatedWebP
implementation libraries.glide
implementation libraries.glideTransformations
kapt libraries.kotshiCompiler
implementation libraries.kotshiApi
implementation libraries.frescoImageViewer
implementation libraries.markwon
implementation libraries.aVLoadingIndicatorView
implementation libraries.livedataKtx
implementation libraries.glide
implementation libraries.glideTransformations
implementation(libraries.jitsi) { transitive = true }
implementation 'com.google.code.findbugs:jsr305:3.0.2'
......@@ -159,8 +177,8 @@ dependencies {
playImplementation libraries.fcm
playImplementation libraries.firebaseAnalytics
playImplementation libraries.playServicesAuth
playImplementation('com.crashlytics.sdk.android:crashlytics:2.9.5@aar') { transitive = true }
playImplementation('com.crashlytics.sdk.android:answers:1.4.3@aar') { transitive = true }
playImplementation('com.crashlytics.sdk.android:crashlytics:2.9.8@aar') { transitive = true }
playImplementation('com.crashlytics.sdk.android:answers:1.4.6@aar') { transitive = true }
testImplementation libraries.junit
testImplementation libraries.truth
......@@ -168,12 +186,6 @@ dependencies {
androidTestImplementation libraries.espressoIntents
}
kotlin {
experimental {
coroutines "enable"
}
}
androidExtensions {
experimental = true
}
......@@ -181,8 +193,8 @@ androidExtensions {
// FIXME - build and install the sdk into the app/libs directory
// We were having some issues with the kapt generated files from the sdk when importing as a module
def sdk_location=project.properties['sdk_location'] ?: ""
task compileSdk(type:Exec) {
def sdk_location = project.properties['sdk_location'] ?: ""
task compileSdk(type: Exec) {
if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
commandLine 'cmd', '/c', 'build-sdk.sh', sdk_location
} else {
......
......@@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:name=".app.RocketChatApplication"
......@@ -73,6 +75,11 @@
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".videoconference.ui.VideoConferenceActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".chatroom.ui.ChatRoomActivity"
android:theme="@style/AppTheme"
......
......@@ -71,4 +71,13 @@ interface Analytics {
* @param resetPasswordSucceeded True if successful reset password, false otherwise.
*/
fun logResetPassword(resetPasswordSucceeded: Boolean) {}
/**
* Logs the video conference event.
*
* @param event The [SubscriptionTypeEvent] to log.
* @param serverUrl The server URL to log.
*/
fun logVideoConference(event: SubscriptionTypeEvent, serverUrl: String) {}
}
......@@ -76,4 +76,12 @@ class AnalyticsManager @Inject constructor(
analytics.forEach { it.logResetPassword(resetPasswordSucceeded) }
}
}
fun logVideoConference(event: SubscriptionTypeEvent) {
if (analyticsTrackingInteractor.get() && serverUrl != null) {
analytics.forEach { it.logVideoConference(event, serverUrl) }
}
}
}
......@@ -6,7 +6,8 @@ import androidx.lifecycle.OnLifecycleEvent
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.common.model.UserStatus
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class AppLifecycleObserver @Inject constructor(
......@@ -28,7 +29,7 @@ class AppLifecycleObserver @Inject constructor(
}
private fun changeTemporaryStatus(userStatus: UserStatus) {
launch {
GlobalScope.launch {
serverInteractor.get()?.let { currentServer ->
factory.create(currentServer).setTemporaryStatus(userStatus)
}
......
......@@ -91,7 +91,7 @@ object DrawableHelper {
}
/**
* Compounds a Drawable (to appear to the left of the text) into a TextView.
* Compounds a Drawable (to appear on the left side of a text) into a TextView.
*
* @param textView The TextView.
* @param drawable The Drawable.
......@@ -100,6 +100,16 @@ object DrawableHelper {
fun compoundDrawable(textView: TextView, drawable: Drawable) =
textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
/**
* Compounds a Drawable (to appear on the right side of a text) into a TextView.
*
* @param textView The TextView.
* @param drawable The Drawable.
* @see compoundDrawable
*/
fun compoundRightDrawable(textView: TextView, drawable: Drawable) =
textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
/**
* Returns the user status drawable.
*
......
......@@ -37,7 +37,8 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import dagger.android.HasBroadcastReceiverInjector
import dagger.android.HasServiceInjector
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.ref.WeakReference
import javax.inject.Inject
......@@ -174,7 +175,7 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
EmojiRepository.init(this)
val currentServer = getCurrentServerInteractor.get()
currentServer?.let { server ->
launch {
GlobalScope.launch {
val client = factory.create(server)
EmojiRepository.setCurrentServerUrl(server)
val customEmojiList = mutableListOf<Emoji>()
......
......@@ -7,7 +7,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.Job
@Module
class AuthenticationModule {
......
......@@ -90,7 +90,7 @@ class LoginFragment : Fragment(), LoginView {
text_username_or_email.setText(credential.first)
text_password.setText(credential.second)
}
REQUEST_CODE_FOR_SAVE_RESOLUTION -> showMessage(getString(R.string.message_credentials_saved_successfully))
REQUEST_CODE_FOR_SAVE_RESOLUTION -> showMessage(getString(R.string.msg_credentials_saved_successfully))
}
}
}
......
......@@ -31,8 +31,7 @@ import chat.rocket.core.internal.rest.loginWithCas
import chat.rocket.core.internal.rest.loginWithOauth
import chat.rocket.core.internal.rest.loginWithSaml
import chat.rocket.core.internal.rest.me
import kotlinx.coroutines.experimental.delay
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.delay
import javax.inject.Inject
private const val TYPE_LOGIN_OAUTH = 1
......@@ -109,11 +108,11 @@ class LoginOptionsPresenter @Inject constructor(
when (loginType) {
TYPE_LOGIN_OAUTH -> client.loginWithOauth(credentialToken, credentialSecret)
TYPE_LOGIN_CAS -> {
delay(3, TimeUnit.SECONDS)
delay(3000)
client.loginWithCas(credentialToken)
}
TYPE_LOGIN_SAML -> {
delay(3, TimeUnit.SECONDS)
delay(3000)
client.loginWithSaml(credentialToken)
}
TYPE_LOGIN_DEEP_LINK -> {
......
......@@ -10,8 +10,8 @@ import chat.rocket.android.server.domain.SaveConnectingServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extension.launchUI
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class OnBoardingPresenter @Inject constructor(
......@@ -19,7 +19,7 @@ class OnBoardingPresenter @Inject constructor(
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val serverInteractor: SaveConnectingServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor,
refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
val settingsInteractor: GetSettingsInteractor,
val factory: RocketChatClientFactory
......@@ -80,7 +80,7 @@ class OnBoardingPresenter @Inject constructor(
}
view.showLoading()
try {
withContext(DefaultDispatcher) {
withContext(Dispatchers.Default) {
setupConnectionInfo(serverUrl)
// preparing next fragment before showing it
......
......@@ -12,8 +12,8 @@ import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.isValidUrl
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class ServerPresenter @Inject constructor(
......@@ -98,7 +98,7 @@ class ServerPresenter @Inject constructor(
}
view.showLoading()
try {
withContext(DefaultDispatcher) {
withContext(Dispatchers.Default) {
// preparing next fragment before showing it
refreshServerAccounts()
checkEnabledAccounts(serverUrl)
......
......@@ -63,7 +63,7 @@ class SignupFragment : Fragment(), SignupView {
if (resultCode == Activity.RESULT_OK) {
if (data != null) {
if (requestCode == SAVE_CREDENTIALS) {
showMessage(getString(R.string.message_credentials_saved_successfully))
showMessage(getString(R.string.msg_credentials_saved_successfully))
}
}
}
......
......@@ -7,8 +7,10 @@ import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.favorite
import chat.rocket.core.internal.rest.getInfo
import chat.rocket.core.model.Room
import timber.log.Timber
......@@ -25,6 +27,31 @@ class ChatDetailsPresenter @Inject constructor(
private val manager = factory.create(currentServer)
private val client = manager.client
fun toggleFavoriteChatRoom(roomId: String, isFavorite: Boolean) {
launchUI(strategy) {
try {
// Note: If it is favorite then the user wants to remove the favorite - and vice versa.
retryIO("favorite($roomId, ${!isFavorite})") {
client.favorite(roomId, !isFavorite)
}
view.showFavoriteIcon(!isFavorite)
} catch (e: RocketChatException) {
Timber.e(
e,
"Error while trying to favorite or removing the favorite of a chat room."
)
e.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
}
fun toVideoConference(roomId: String, chatRoomType: String) =
navigator.toVideoConference(roomId, chatRoomType)
fun getDetails(chatRoomId: String, chatRoomType: String) {
launchUI(strategy) {
try {
......@@ -32,7 +59,7 @@ class ChatDetailsPresenter @Inject constructor(
client.getInfo(chatRoomId, null, roomTypeOf(chatRoomType))
}
view.displayDetails(roomToChatDetails(room))
} catch(exception: Exception) {
} catch (exception: Exception) {
Timber.e(exception)
exception.message?.let {
view.showMessage(it)
......
......@@ -5,5 +5,16 @@ import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface ChatDetailsView: MessageView {
/**
* Shows the corresponding favorite icon for a favorite or non-favorite chat room.
*
* @param isFavorite True if a chat room is favorite, false otherwise.
*/
fun showFavoriteIcon(isFavorite: Boolean)
/**
* Shows the details of a chat room.
*/
fun displayDetails(room: ChatDetails)
}
\ No newline at end of file
......@@ -3,6 +3,8 @@ package chat.rocket.android.chatdetails.ui
import DrawableHelper
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
......@@ -17,6 +19,8 @@ import chat.rocket.android.chatdetails.presentation.ChatDetailsView
import chat.rocket.android.chatdetails.viewmodel.ChatDetailsViewModel
import chat.rocket.android.chatdetails.viewmodel.ChatDetailsViewModelFactory
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
......@@ -31,21 +35,28 @@ fun newInstance(
chatRoomId: String,
chatRoomType: String,
isSubscribed: Boolean,
isFavorite: Boolean,
disableMenu: Boolean
): ChatDetailsFragment = ChatDetailsFragment().apply {
arguments = Bundle(4).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
putBoolean(BUNDLE_IS_SUBSCRIBED, isSubscribed)
putBoolean(BUNDLE_DISABLE_MENU, disableMenu)
}
): ChatDetailsFragment {
return ChatDetailsFragment().apply {
arguments = Bundle(5).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
putBoolean(BUNDLE_IS_SUBSCRIBED, isSubscribed)
putBoolean(BUNDLE_IS_FAVORITE, isFavorite)
putBoolean(BUNDLE_DISABLE_MENU, disableMenu)
}
}
}
internal const val TAG_CHAT_DETAILS_FRAGMENT = "ChatDetailsFragment"
internal const val MENU_ACTION_FAVORITE_REMOVE_FAVORITE = 1
internal const val MENU_ACTION_VIDEO_CALL = 2
private const val BUNDLE_CHAT_ROOM_ID = "BUNDLE_CHAT_ROOM_ID"
private const val BUNDLE_CHAT_ROOM_TYPE = "BUNDLE_CHAT_ROOM_TYPE"
private const val BUNDLE_IS_SUBSCRIBED = "BUNDLE_IS_SUBSCRIBED"
private const val BUNDLE_IS_FAVORITE = "BUNDLE_IS_FAVORITE"
private const val BUNDLE_DISABLE_MENU = "BUNDLE_DISABLE_MENU"
class ChatDetailsFragment : Fragment(), ChatDetailsView {
......@@ -53,12 +64,17 @@ class ChatDetailsFragment : Fragment(), ChatDetailsView {
lateinit var presenter: ChatDetailsPresenter
@Inject
lateinit var factory: ChatDetailsViewModelFactory
@Inject
lateinit var serverUrl: CurrentServerRepository
@Inject
lateinit var settings: GetSettingsInteractor
private var adapter: ChatDetailsAdapter? = null
private lateinit var viewModel: ChatDetailsViewModel
private var chatRoomId: String? = null
private var chatRoomType: String? = null
internal lateinit var chatRoomId: String
internal lateinit var chatRoomType: String
private var isSubscribed: Boolean = true
internal var isFavorite: Boolean = false
private var disableMenu: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
......@@ -69,8 +85,11 @@ class ChatDetailsFragment : Fragment(), ChatDetailsView {
chatRoomId = getString(BUNDLE_CHAT_ROOM_ID)
chatRoomType = getString(BUNDLE_CHAT_ROOM_TYPE)
isSubscribed = getBoolean(BUNDLE_IS_SUBSCRIBED)
isFavorite = getBoolean(BUNDLE_IS_FAVORITE)
disableMenu = getBoolean(BUNDLE_DISABLE_MENU)
} ?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" }
setHasOptionsMenu(true)
}
override fun onCreateView(
......@@ -87,11 +106,27 @@ class ChatDetailsFragment : Fragment(), ChatDetailsView {
getDetails()
}
override fun onPrepareOptionsMenu(menu: Menu) {
menu.clear()
setupMenu(menu)
super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
setOnMenuItemClickListener(item)
return true
}
override fun showFavoriteIcon(isFavorite: Boolean) {
this.isFavorite = isFavorite
activity?.invalidateOptionsMenu()
}
override fun displayDetails(room: ChatDetails) {
ui {
val text = room.name
name.text = text
bindImage(chatRoomType!!)
bindImage(chatRoomType)
content_topic.text =
if (room.topic.isNullOrEmpty()) getString(R.string.msg_no_topic) else room.topic
content_announcement.text =
......@@ -203,8 +238,8 @@ class ChatDetailsFragment : Fragment(), ChatDetailsView {
private fun setupToolbar() {
with((activity as ChatRoomActivity)) {
hideToolbarChatRoomIcon()
showToolbarTitle(getString(R.string.title_channel_details))
hideExpandMoreForToolbar()
setupToolbarTitle(getString(R.string.title_channel_details))
}
}
}
\ No newline at end of file
package chat.rocket.android.chatdetails.ui
import android.view.Menu
import android.view.MenuItem
import chat.rocket.android.R
import chat.rocket.android.server.domain.isJitsiEnabled
import chat.rocket.android.server.domain.isJitsiEnabledForChannels
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
internal fun ChatDetailsFragment.setupMenu(menu: Menu) {
serverUrl.get()?.let {
with(settings.get(it)) {
if (isJitsiEnabled()) {
if (roomTypeOf(chatRoomType) !is RoomType.DirectMessage && !isJitsiEnabledForChannels()) {
return
}
menu.add(
Menu.NONE,
MENU_ACTION_VIDEO_CALL,
Menu.NONE,
R.string.msg_video_call
).setIcon(R.drawable.ic_video_24dp).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
}
}
if (isFavorite) {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_REMOVE_FAVORITE,
Menu.NONE,
R.string.action_remove_favorite
).setIcon(R.drawable.ic_star_yellow_24dp).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
} else {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_REMOVE_FAVORITE,
Menu.NONE,
R.string.action_favorite
).setIcon(R.drawable.ic_star_border_white_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
}
internal fun ChatDetailsFragment.setOnMenuItemClickListener(item: MenuItem) {
if (item.itemId == MENU_ACTION_FAVORITE_REMOVE_FAVORITE) {
presenter.toggleFavoriteChatRoom(chatRoomId, isFavorite)
} else if (item.itemId == MENU_ACTION_VIDEO_CALL) {
presenter.toVideoConference(chatRoomId, chatRoomType)
}
}
......@@ -7,7 +7,7 @@ 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
import kotlinx.coroutines.Job
@Module
class MessageInfoFragmentModule {
......
......@@ -24,7 +24,6 @@ fun Context.messageInformationIntent(messageId: String): Intent {
private const val INTENT_MESSAGE_ID = "message_id"
class MessageInfoActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
......@@ -45,7 +44,7 @@ class MessageInfoActivity : AppCompatActivity(), HasSupportFragmentInjector {
}
private fun setupToolbar() {
text_room_name.textContent = getString(R.string.message_information_title)
text_toolbar_title.textContent = getString(R.string.message_information_title)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
......
......@@ -44,8 +44,14 @@ class ChatRoomAdapter(
MessageViewHolder(
view,
actionsListener,
reactionListener
) { userId -> navigator?.toUserDetails(userId) }
reactionListener,
{ userId -> navigator?.toUserDetails(userId) },
{
if (roomId != null && roomType != null) {
navigator?.toVideoConference(roomId, roomType)
}
}
)
}
BaseUiModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview)
......@@ -101,8 +107,9 @@ class ChatRoomAdapter(
when (holder) {
is MessageViewHolder ->
holder.bind(dataSet[position] as MessageUiModel)
is UrlPreviewViewHolder ->
is UrlPreviewViewHolder -> {
holder.bind(dataSet[position] as UrlPreviewUiModel)
}
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyUiModel)
is AttachmentViewHolder ->
......
......@@ -13,8 +13,8 @@ import chat.rocket.android.emoji.Emoji
import chat.rocket.android.emoji.EmojiKeyboardListener
import chat.rocket.android.emoji.EmojiPickerPopup
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.emoji.internal.GlideApp
import chat.rocket.android.infrastructure.LocalRepository
import com.bumptech.glide.Glide
import kotlinx.android.synthetic.main.item_reaction.view.*
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
......@@ -103,9 +103,9 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
// The view at index 1 corresponds to the one to display custom emojis which are images.
view_flipper_reaction.displayedChild = 1
val glideRequest = if (reaction.url!!.endsWith("gif", true)) {
GlideApp.with(context).asGif()
Glide.with(context).asGif()
} else {
GlideApp.with(context).asBitmap()
Glide.with(context).asBitmap()
}
glideRequest.load(reaction.url).into(image_emoji)
......
......@@ -10,6 +10,7 @@ import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.MessageUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.core.model.MessageType
import chat.rocket.core.model.isSystemMessage
import com.bumptech.glide.load.resource.gif.GifDrawable
import kotlinx.android.synthetic.main.avatar.view.*
......@@ -19,7 +20,8 @@ class MessageViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
private val avatarListener: (String) -> Unit
private val avatarListener: (String) -> Unit,
private val joinVideoCallListener: (View) -> Unit
) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener), Drawable.Callback {
init {
......@@ -51,6 +53,9 @@ class MessageViewHolder(
text_content.text_content.text = data.content
button_join_video_call.isVisible = data.message.type is MessageType.JitsiCallStarted
button_join_video_call.setOnClickListener { joinVideoCallListener(it) }
image_avatar.setImageURI(data.avatar)
text_content.setTextColor(if (data.isTemporary) Color.GRAY else Color.BLACK)
......
......@@ -7,7 +7,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.Job
@Module
class ChatRoomModule {
......
package chat.rocket.android.chatroom.presentation
import android.os.Build
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.chatdetails.ui.TAG_CHAT_DETAILS_FRAGMENT
import chat.rocket.android.chatinformation.ui.messageInformationIntent
......@@ -13,6 +15,7 @@ import chat.rocket.android.pinnedmessages.ui.TAG_PINNED_MESSAGES_FRAGMENT
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.userdetails.ui.TAG_USER_DETAILS_FRAGMENT
import chat.rocket.android.util.extensions.addFragmentBackStack
import chat.rocket.android.videoconference.ui.videoConferenceIntent
class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
......@@ -22,6 +25,19 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
}
}
fun toVideoConference(chatRoomId: String, chatRoomType: String) {
// TODO: Jitsi isn't working with Android M- version. We need to remove the condition bellow after it's solved. (https://github.com/jitsi/jitsi-meet/pull/3967)/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.startActivity(activity.videoConferenceIntent(chatRoomId, chatRoomType))
} else {
Toast.makeText(
activity,
"Sorry, unable to open the video conference due to device configuration",
Toast.LENGTH_SHORT
).show()
}
}
fun toChatRoom(
chatRoomId: String,
chatRoomName: String,
......@@ -51,6 +67,7 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
chatRoomId: String,
chatRoomType: String,
isChatRoomSubscribed: Boolean,
isChatRoomFavorite: Boolean,
isMenuDisabled: Boolean
) {
activity.addFragmentBackStack(TAG_CHAT_DETAILS_FRAGMENT, R.id.fragment_container) {
......@@ -58,6 +75,7 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
chatRoomId,
chatRoomType,
isChatRoomSubscribed,
isChatRoomFavorite,
isMenuDisabled
)
}
......
......@@ -53,7 +53,6 @@ import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.rest.chatRoomRoles
import chat.rocket.core.internal.rest.commands
import chat.rocket.core.internal.rest.deleteMessage
import chat.rocket.core.internal.rest.favorite
import chat.rocket.core.internal.rest.getMembers
import chat.rocket.core.internal.rest.history
import chat.rocket.core.internal.rest.joinChat
......@@ -76,12 +75,11 @@ import chat.rocket.core.model.ChatRoomRole
import chat.rocket.core.model.Command
import chat.rocket.core.model.Message
import chat.rocket.core.model.Room
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.threeten.bp.Instant
import timber.log.Timber
import java.util.*
......@@ -134,7 +132,7 @@ class ChatRoomPresenter @Inject constructor(
draftKey = "${currentServer}_${LocalRepository.DRAFT_KEY}$roomId"
chatRoomId = roomId
chatRoomType = roomType
launch(CommonPool + strategy.jobs) {
GlobalScope.launch(Dispatchers.IO + strategy.jobs) {
try {
chatRoles = if (roomTypeOf(roomType) !is RoomType.DirectMessage) {
client.chatRoomRoles(roomType = roomTypeOf(roomType), roomName = roomName)
......@@ -178,7 +176,7 @@ class ChatRoomPresenter @Inject constructor(
}
private suspend fun subscribeRoomChanges() {
withContext(CommonPool + strategy.jobs) {
withContext(Dispatchers.IO + strategy.jobs) {
chatRoomId?.let {
manager.addRoomChannel(it, roomChangesChannel)
for (room in roomChangesChannel) {
......@@ -405,7 +403,7 @@ class ChatRoomPresenter @Inject constructor(
launchUI(strategy) {
view.showLoading()
try {
withContext(DefaultDispatcher) {
withContext(Dispatchers.Default) {
val fileName = uriInteractor.getFileName(uri) ?: uri.toString()
if (fileName.isEmpty()) {
view.showInvalidFileMessage()
......@@ -443,7 +441,7 @@ class ChatRoomPresenter @Inject constructor(
launchUI(strategy) {
view.showLoading()
try {
withContext(DefaultDispatcher) {
withContext(Dispatchers.Default) {
val fileName = uriInteractor.getFileName(uri) ?: uri.toString()
val fileSize = uriInteractor.getFileSize(uri)
val maxFileSizeAllowed = settings.uploadMaxFileSize()
......@@ -484,7 +482,7 @@ class ChatRoomPresenter @Inject constructor(
launchUI(strategy) {
view.showLoading()
try {
withContext(DefaultDispatcher) {
withContext(Dispatchers.Default) {
val fileName = UUID.randomUUID().toString() + ".png"
val fileSize = byteArray.size
val mimeType = "image/png"
......@@ -522,7 +520,7 @@ class ChatRoomPresenter @Inject constructor(
}
fun sendTyping() {
launch(CommonPool + strategy.jobs) {
GlobalScope.launch(Dispatchers.IO + strategy.jobs) {
if (chatRoomId != null && currentLoggedUsername != null) {
client.setTypingStatus(chatRoomId.toString(), currentLoggedUsername, true)
}
......@@ -530,7 +528,7 @@ class ChatRoomPresenter @Inject constructor(
}
fun sendNotTyping() {
launch(CommonPool + strategy.jobs) {
GlobalScope.launch(Dispatchers.IO + strategy.jobs) {
if (chatRoomId != null && currentLoggedUsername != null) {
client.setTypingStatus(chatRoomId.toString(), currentLoggedUsername, false)
}
......@@ -552,11 +550,11 @@ class ChatRoomPresenter @Inject constructor(
Timber.d("Subscribing to Status changes")
lastState = manager.state
manager.addStatusChannel(stateChannel)
launch(CommonPool + strategy.jobs) {
GlobalScope.launch(Dispatchers.IO + strategy.jobs) {
for (state in stateChannel) {
Timber.d("Got new state: $state - last: $lastState")
if (state != lastState) {
launch(UI) {
launch(Dispatchers.Main) {
view.showConnectionState(state)
}
......@@ -573,7 +571,7 @@ class ChatRoomPresenter @Inject constructor(
private fun subscribeMessages(roomId: String) {
manager.subscribeRoomMessages(roomId, messagesChannel)
launch(CommonPool + strategy.jobs) {
GlobalScope.launch(Dispatchers.IO + strategy.jobs) {
for (message in messagesChannel) {
Timber.d("New message for room ${message.roomId}")
updateMessage(message)
......@@ -582,7 +580,7 @@ class ChatRoomPresenter @Inject constructor(
}
private fun loadMissingMessages() {
launch(parent = strategy.jobs) {
GlobalScope.launch(strategy.jobs) {
chatRoomId?.let { chatRoomId ->
val roomType = roomTypeOf(chatRoomType)
val lastSyncDate = messagesRepository.getLastSyncDate(chatRoomId)
......@@ -891,32 +889,14 @@ class ChatRoomPresenter @Inject constructor(
}
}
fun toggleFavoriteChatRoom(roomId: String, isFavorite: Boolean) {
launchUI(strategy) {
try {
// Note that if it is favorite then the user wants to unfavorite - and vice versa.
retryIO("favorite($roomId, $isFavorite)") {
client.favorite(roomId, !isFavorite)
}
view.showFavoriteIcon(!isFavorite)
} catch (e: RocketChatException) {
Timber.e(e, "Error while trying to favorite/unfavorite chat room.")
e.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
}
fun toChatDetails(
chatRoomId: String,
chatRoomType: String,
isSubscribed: Boolean,
isFavorite: Boolean,
isMenuDisabled: Boolean
) {
navigator.toChatDetails(chatRoomId, chatRoomType, isSubscribed, isMenuDisabled)
navigator.toChatDetails(chatRoomId, chatRoomType, isSubscribed, isFavorite, isMenuDisabled)
}
fun loadChatRoomsSuggestions() {
......@@ -944,7 +924,7 @@ class ChatRoomPresenter @Inject constructor(
}
// TODO: move this to new interactor or FetchChatRoomsInteractor?
private suspend fun getChatRoomAsync(roomId: String): ChatRoom? = withContext(CommonPool) {
private suspend fun getChatRoomAsync(roomId: String): ChatRoom? = withContext(Dispatchers.IO) {
retryDB("getRoom($roomId)") {
dbManager.chatRoomDao().getSync(roomId)?.let {
with(it.chatRoom) {
......@@ -982,7 +962,7 @@ class ChatRoomPresenter @Inject constructor(
}
// TODO: move this to new interactor or FetchChatRoomsInteractor?
private suspend fun getChatRoomsAsync(name: String? = null): List<ChatRoom> = withContext(CommonPool) {
private suspend fun getChatRoomsAsync(name: String? = null): List<ChatRoom> = withContext(Dispatchers.IO) {
retryDB("getAllSync()") {
dbManager.chatRoomDao().getAllSync().filter {
if (name == null) {
......@@ -1212,7 +1192,7 @@ class ChatRoomPresenter @Inject constructor(
}
private fun subscribeTypingStatus() {
launch(CommonPool + strategy.jobs) {
GlobalScope.launch(Dispatchers.IO + strategy.jobs) {
client.subscribeTypingStatus(chatRoomId.toString()) { _, id ->
typingStatusSubscriptionId = id
}
......
......@@ -13,13 +13,6 @@ import chat.rocket.core.model.ChatRoom
interface ChatRoomView : LoadingView, MessageView {
/**
* Shows the Favorite/Unfavorite chat room icon.
*
* @param isFavorite Shows the favorite icon if true, otherwise shows the unfavorite icon.
*/
fun showFavoriteIcon(isFavorite: Boolean)
/**
* Shows the chat room messages.
*
......
......@@ -10,8 +10,9 @@ import chat.rocket.android.server.infraestructure.DatabaseMessagesRepository
import chat.rocket.core.internal.rest.sendMessage
import chat.rocket.core.model.Message
import dagger.android.AndroidInjection
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
......@@ -33,7 +34,7 @@ class MessageService : JobService() {
}
override fun onStartJob(params: JobParameters?): Boolean {
launch(CommonPool) {
GlobalScope.launch(Dispatchers.IO) {
getAccountsInteractor.get().forEach { account ->
retrySendingMessages(params, account.serverUrl)
}
......@@ -44,7 +45,8 @@ class MessageService : JobService() {
private suspend fun retrySendingMessages(params: JobParameters?, serverUrl: String) {
val dbManager = dbFactory.create(serverUrl)
val messageRepository = DatabaseMessagesRepository(dbManager, DatabaseMessageMapper(dbManager))
val messageRepository =
DatabaseMessagesRepository(dbManager, DatabaseMessageMapper(dbManager))
val temporaryMessages = messageRepository.getAllUnsent()
.sortedWith(compareBy(Message::timestamp))
if (temporaryMessages.isNotEmpty()) {
......
......@@ -4,16 +4,15 @@ import DrawableHelper
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
......@@ -56,7 +55,6 @@ private const val INTENT_CHAT_ROOM_MESSAGE = "chat_room_message"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
// TODO - workaround for now... We will move to a single activity
@Inject
lateinit var serverInteractor: GetCurrentServerInteractor
......@@ -136,31 +134,21 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
toolbar.setNavigationOnClickListener { finishActivity() }
}
fun showToolbarTitle(title: String) {
text_room_name.textContent = title
fun setupToolbarTitle(title: String) {
text_toolbar_title.textContent = title
}
fun showToolbarChatRoomIcon(chatRoomType: String) {
val drawable = when (roomTypeOf(chatRoomType)) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_hashtag_black_12dp, this)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_12_dp, this)
}
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.colorWhite)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
}
fun setupExpandMoreForToolbar(listener: (View) -> Unit) {
DrawableHelper.compoundRightDrawable(
text_toolbar_title,
DrawableHelper.getDrawableFromId(R.drawable.ic_chatroom_toolbar_expand_more_20dp, this)
)
text_toolbar_title.setOnClickListener { listener(it) }
}
fun hideToolbarChatRoomIcon() {
text_room_name.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
fun hideExpandMoreForToolbar() {
text_toolbar_title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
text_toolbar_title.setOnClickListener(null)
}
private fun finishActivity() {
......
......@@ -4,10 +4,11 @@ import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import androidx.core.view.isVisible
import chat.rocket.android.emoji.internal.GlideApp
import chat.rocket.android.util.extensions.getFileName
import chat.rocket.android.util.extensions.getMimeType
import chat.rocket.common.util.ifNull
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
......@@ -25,18 +26,18 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
when {
mimeType.startsWith("image") -> {
if (mimeType.contains("gif")) {
GlideApp
Glide
.with(context)
.asGif()
.load(uri)
.fitCenter()
.apply(RequestOptions().fitCenter())
.into(imagePreview)
} else {
GlideApp
Glide
.with(context)
.asBitmap()
.load(uri)
.fitCenter()
.apply(RequestOptions().fitCenter())
.into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(
resource: Bitmap,
......
......@@ -11,23 +11,6 @@ import chat.rocket.android.util.extension.onQueryTextListener
internal fun ChatRoomFragment.setupMenu(menu: Menu) {
setupSearchMessageMenuItem(menu, requireContext())
setupFavoriteMenuItem(menu)
setupDetailsMenuItem(menu)
}
internal fun ChatRoomFragment.setOnMenuItemClickListener(item: MenuItem) {
when (item.itemId) {
MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT -> presenter.toggleFavoriteChatRoom(
chatRoomId,
isFavorite
)
MENU_ACTION_SHOW_DETAILS -> presenter.toChatDetails(
chatRoomId,
chatRoomType,
isSubscribed,
disableMenu
)
}
}
private fun ChatRoomFragment.setupSearchMessageMenuItem(menu: Menu, context: Context) {
......@@ -37,27 +20,23 @@ private fun ChatRoomFragment.setupSearchMessageMenuItem(menu: Menu, context: Con
Menu.NONE,
R.string.title_search_message
).setActionView(SearchView(context))
.setIcon(R.drawable.ic_search_white_24dp)
.setIcon(R.drawable.ic_chatroom_toolbar_magnifier_20dp)
.setShowAsActionFlags(
MenuItem.SHOW_AS_ACTION_IF_ROOM or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
)
.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
dismissEmojiKeyboard()
removeFavoriteMenuItem(menu)
removeDetailMenuItem(menu)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
dismissEmojiKeyboard()
setupFavoriteMenuItem(menu)
setupDetailsMenuItem(menu)
return true
}
})
(searchItem?.actionView as? SearchView)?.let {
(searchItem.actionView as? SearchView)?.let {
// TODO: Check why we need to stylize the search text programmatically instead of by defining it in the styles.xml (ChatRoom.SearchView)
stylizeSearchView(it, context)
setupSearchViewTextListener(it)
......@@ -77,6 +56,7 @@ private fun stylizeSearchView(searchView: SearchView, context: Context) {
private fun ChatRoomFragment.setupSearchViewTextListener(searchView: SearchView) {
searchView.onQueryTextListener {
// TODO: We use isSearchTermQueried to avoid querying when the search view is expanded but the user doesn't start typing. Check for a native solution.
if (it.isEmpty() && isSearchTermQueried) {
presenter.loadMessages(chatRoomId, chatRoomType, clearDataSet = true)
} else if (it.isNotEmpty()) {
......@@ -84,42 +64,4 @@ private fun ChatRoomFragment.setupSearchViewTextListener(searchView: SearchView)
isSearchTermQueried = true
}
}
}
private fun ChatRoomFragment.setupFavoriteMenuItem(menu: Menu) {
if (isFavorite) {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT,
Menu.NONE,
R.string.title_unfavorite_chat
).setIcon(R.drawable.ic_star_yellow_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
} else {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT,
Menu.NONE,
R.string.title_favorite_chat
).setIcon(R.drawable.ic_star_border_white_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
}
private fun ChatRoomFragment.setupDetailsMenuItem(menu: Menu) {
menu.add(
Menu.NONE,
MENU_ACTION_SHOW_DETAILS,
Menu.NONE,
R.string.title_channel_details
).setIcon(R.drawable.ic_info_outline_white_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
private fun removeFavoriteMenuItem(menu: Menu) {
menu.removeItem(MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT)
}
private fun removeDetailMenuItem(menu: Menu) {
menu.removeItem(MENU_ACTION_SHOW_DETAILS)
}
\ No newline at end of file
......@@ -30,5 +30,5 @@ interface BaseUiModel<out T> {
internal fun Int.toViewType(): BaseUiModel.ViewType {
return BaseUiModel.ViewType.values().firstOrNull { it.viewType == this }
?: throw InvalidParameterException("Invalid viewType: $this for BaseUiModel.ViewType")
?: throw InvalidParameterException("Invalid viewType: $this for BaseUiModel.ViewType")
}
\ No newline at end of file
......@@ -26,7 +26,6 @@ data class MessageUiModel(
) : BaseMessageUiModel<Message> {
override val viewType: Int
get() = BaseUiModel.ViewType.MESSAGE.viewType
override val layoutId: Int
get() = R.layout.item_message
}
\ No newline at end of file
......@@ -44,8 +44,8 @@ import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.Field
import chat.rocket.core.model.isSystemMessage
import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import java.security.InvalidParameterException
import java.util.*
......@@ -79,7 +79,7 @@ class UiModelMapper @Inject constructor(
message: Message,
roomUiModel: RoomUiModel = RoomUiModel(roles = emptyList(), isBroadcast = true)
): List<BaseUiModel<*>> =
withContext(CommonPool) {
withContext(Dispatchers.IO) {
return@withContext translate(message, roomUiModel)
}
......@@ -88,7 +88,7 @@ class UiModelMapper @Inject constructor(
roomUiModel: RoomUiModel = RoomUiModel(roles = emptyList(), isBroadcast = true),
asNotReversed: Boolean = false
): List<BaseUiModel<*>> =
withContext(CommonPool) {
withContext(Dispatchers.IO) {
val list = ArrayList<BaseUiModel<*>>(messages.size)
messages.forEach {
......@@ -102,7 +102,7 @@ class UiModelMapper @Inject constructor(
suspend fun map(
readReceipts: List<ReadReceipt>
): List<ReadReceiptViewModel> = withContext(CommonPool) {
): List<ReadReceiptViewModel> = withContext(Dispatchers.IO) {
val list = arrayListOf<ReadReceiptViewModel>()
readReceipts.forEach {
......@@ -121,7 +121,7 @@ class UiModelMapper @Inject constructor(
message: Message,
roomUiModel: RoomUiModel
): List<BaseUiModel<*>> =
withContext(CommonPool) {
withContext(Dispatchers.IO) {
val list = ArrayList<BaseUiModel<*>>()
getChatRoomAsync(message.roomId)?.let { chatRoom ->
......@@ -167,7 +167,7 @@ class UiModelMapper @Inject constructor(
}
// TODO: move this to new interactor or FetchChatRoomsInteractor?
private suspend fun getChatRoomAsync(roomId: String): ChatRoom? = withContext(CommonPool) {
private suspend fun getChatRoomAsync(roomId: String): ChatRoom? = withContext(Dispatchers.IO) {
return@withContext dbManager.getRoom(id = roomId)?.let {
with(it.chatRoom) {
ChatRoom(
......@@ -212,7 +212,7 @@ class UiModelMapper @Inject constructor(
message: Message,
roomUiModel: RoomUiModel
): List<BaseUiModel<*>> =
withContext(CommonPool) {
withContext(Dispatchers.IO) {
val list = ArrayList<BaseUiModel<*>>()
getChatRoomAsync(message.roomId)?.let { chatRoom ->
......@@ -437,7 +437,7 @@ class UiModelMapper @Inject constructor(
private suspend fun mapMessage(
message: Message,
chatRoom: ChatRoom
): MessageUiModel = withContext(CommonPool) {
): MessageUiModel = withContext(Dispatchers.IO) {
val sender = getSenderName(message)
val time = getTime(message.timestamp)
val avatar = getUserAvatar(message)
......@@ -552,13 +552,15 @@ class UiModelMapper @Inject constructor(
is MessageType.SubscriptionRoleAdded -> getString(R.string.message_role_add, message.message, message.role, message.sender?.username)
is MessageType.SubscriptionRoleRemoved -> getString(R.string.message_role_removed, message.message, message.role, message.sender?.username)
is MessageType.RoomChangedPrivacy -> getString(R.string.message_room_changed_privacy, message.message, message.sender?.username)
is MessageType.JitsiCallStarted -> context.getString(
R.string.message_video_call_started, message.sender?.username
)
else -> throw InvalidParameterException("Invalid message type: ${message.type}")
}
}
val spannableMsg = SpannableStringBuilder(content)
spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length, 0)
spannableMsg.setSpan(ForegroundColorSpan(Color.GRAY), 0, spannableMsg.length, 0)
return spannableMsg
}
}
\ No newline at end of file
......@@ -23,11 +23,12 @@ import chat.rocket.common.model.roomTypeOf
import chat.rocket.core.internal.realtime.createDirectMessage
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.show
import kotlinx.coroutines.experimental.withTimeout
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Named
import kotlin.coroutines.experimental.suspendCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ChatRoomsPresenter @Inject constructor(
private val view: ChatRoomsView,
......
......@@ -20,16 +20,16 @@ import chat.rocket.core.model.SpotlightResult
import com.shopify.livedataktx.distinct
import com.shopify.livedataktx.map
import com.shopify.livedataktx.nonNull
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.isActive
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.lang.IllegalArgumentException
import kotlin.coroutines.experimental.coroutineContext
import kotlin.coroutines.coroutineContext
class ChatRoomsViewModel(
private val connectionManager: ConnectionManager,
......@@ -107,7 +107,7 @@ class ChatRoomsViewModel(
}
private fun fetchRooms() {
launch {
GlobalScope.launch {
setLoadingState(LoadingState.Loading(repository.count()))
try {
interactor.refreshChatRooms()
......@@ -125,7 +125,7 @@ class ChatRoomsViewModel(
}
private suspend fun setLoadingState(state: LoadingState) {
withContext(UI) {
withContext(Dispatchers.Main) {
loadingState.value = state
}
}
......
......@@ -37,6 +37,8 @@ import chat.rocket.android.settings.di.SettingsFragmentProvider
import chat.rocket.android.settings.password.di.PasswordFragmentProvider
import chat.rocket.android.settings.password.ui.PasswordActivity
import chat.rocket.android.userdetails.di.UserDetailsFragmentProvider
import chat.rocket.android.videoconference.di.VideoConferenceModule
import chat.rocket.android.videoconference.ui.VideoConferenceActivity
import chat.rocket.android.webview.adminpanel.di.AdminPanelWebViewFragmentProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -103,4 +105,8 @@ abstract class ActivityBuilder {
@PerActivity
@ContributesAndroidInjector(modules = [DrawModule::class])
abstract fun bindDrawingActivity(): DrawingActivity
@PerActivity
@ContributesAndroidInjector(modules = [VideoConferenceModule::class])
abstract fun bindVideoConferenceActivity(): VideoConferenceActivity
}
......@@ -63,7 +63,6 @@ import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.internal.ISO8601Date
import chat.rocket.common.model.TimestampAdapter
import chat.rocket.common.util.CalendarISO8601Converter
import chat.rocket.common.util.Logger
import chat.rocket.common.util.NoOpLogger
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.internal.AttachmentAdapterFactory
......
......@@ -13,7 +13,6 @@ import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.internal.ISO8601Date
import chat.rocket.common.model.TimestampAdapter
import chat.rocket.common.util.CalendarISO8601Converter
import chat.rocket.common.util.Logger
import chat.rocket.common.util.NoOpLogger
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.internal.AttachmentAdapterFactory
......
......@@ -110,6 +110,6 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
}
private fun setupToolbar() {
(activity as ChatRoomActivity).showToolbarTitle(getString(R.string.title_favorite_messages))
(activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_favorite_messages))
}
}
\ No newline at end of file
......@@ -147,7 +147,7 @@ class FilesFragment : Fragment(), FilesView {
}
private fun setupToolbar(totalFiles: Long) {
(activity as ChatRoomActivity).showToolbarTitle(
(activity as ChatRoomActivity).setupToolbarTitle(
(getString(
R.string.title_files_total,
totalFiles
......
......@@ -11,7 +11,10 @@ object AndroidPermissionsHelper {
const val WRITE_EXTERNAL_STORAGE_CODE = 1
fun checkPermission(context: Context, permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
return ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}
fun requestPermission(context: Activity, permission: String, requestCode: Int) {
......
package chat.rocket.android.helper
object JitsiHelper {
/**
* Returns the for the Jitsi video conferencing URL.
*
* @param isSecureProtocol True if using SSL, false otherwise - from the public settings.
* @param domain The Jitsi domain - from public settings.
* @param prefix The Jitsi prefix - from public settings.
* @param uniqueIdentifier The server unique identifier - from public settings.
* @param chatRoomId The ChatRoom ID where the video conferencing was started.
*/
fun getJitsiUrl(
isSecureProtocol: Boolean,
domain: String?,
prefix: String?,
uniqueIdentifier: String?,
chatRoomId: String?
): String =
getJitsiProtocol(isSecureProtocol) +
domain +
"/" +
prefix +
uniqueIdentifier +
chatRoomId
private fun getJitsiProtocol(isSecureProtocol: Boolean) =
if (isSecureProtocol) "https://" else "http://"
}
\ No newline at end of file
......@@ -8,7 +8,7 @@ import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.ui.MainActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.Job
@Module
class MainModule {
......
......@@ -37,7 +37,7 @@ import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.getCustomEmojis
import chat.rocket.core.internal.rest.me
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.channels.Channel
import timber.log.Timber
import javax.inject.Inject
......
......@@ -3,6 +3,7 @@ package chat.rocket.android.members.presentation
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.members.uimodel.MemberUiModel
import chat.rocket.android.members.uimodel.MemberUiModelMapper
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
......@@ -23,7 +24,8 @@ class MembersPresenter @Inject constructor(
@Named("currentServer") private val currentServer: String,
private val strategy: CancelStrategy,
private val mapper: MemberUiModelMapper,
val factory: RocketChatClientFactory
val factory: RocketChatClientFactory,
private val userHelper: UserHelper
) {
private val client: RocketChatClient = factory.create(currentServer)
private var offset: Long = 0
......@@ -39,7 +41,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
......@@ -59,6 +61,10 @@ class MembersPresenter @Inject constructor(
}
fun toMemberDetails(memberUiModel: MemberUiModel) {
navigator.toMemberDetails(memberUiModel.userId)
with(memberUiModel) {
if (userId != userHelper.user()?.id) {
navigator.toMemberDetails(userId)
}
}
}
}
......@@ -131,9 +131,9 @@ class MembersFragment : Fragment(), MembersView {
private fun setupToolbar(totalMembers: Long? = null) {
with((activity as ChatRoomActivity)) {
if (totalMembers != null) {
showToolbarTitle((getString(R.string.title_counted_members, totalMembers)))
setupToolbarTitle((getString(R.string.title_counted_members, totalMembers)))
} else {
showToolbarTitle((getString(R.string.title_members)))
setupToolbarTitle((getString(R.string.title_members)))
}
this.clearLightStatusBar()
toolbar.isVisible = true
......
......@@ -116,6 +116,6 @@ class MentionsFragment : Fragment(), MentionsView {
}
private fun setupToolbar() {
(activity as ChatRoomActivity).showToolbarTitle((getString(R.string.msg_mentions)))
(activity as ChatRoomActivity).setupToolbarTitle((getString(R.string.msg_mentions)))
}
}
\ No newline at end of file
......@@ -116,6 +116,6 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
}
private fun setupToolbar() {
(activity as ChatRoomActivity).showToolbarTitle((getString(R.string.title_pinned_messages)))
(activity as ChatRoomActivity).setupToolbarTitle((getString(R.string.title_pinned_messages)))
}
}
\ No newline at end of file
......@@ -27,8 +27,8 @@ import chat.rocket.core.internal.rest.deleteOwnAccount
import chat.rocket.core.internal.rest.resetAvatar
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
import javax.inject.Inject
......@@ -180,7 +180,7 @@ class ProfilePresenter @Inject constructor(
launchUI(strategy) {
view.showLoading()
try {
withContext(DefaultDispatcher) {
withContext(Dispatchers.Default) {
// REMARK: Backend API is only working with a lowercase hash.
// https://github.com/RocketChat/Rocket.Chat/issues/12573
retryIO { client.deleteOwnAccount(password.gethash().toHex().toLowerCase()) }
......
......@@ -11,8 +11,8 @@ import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.sendMessage
import dagger.android.AndroidInjection
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.*
import javax.inject.Inject
......@@ -36,7 +36,7 @@ class DirectReplyReceiver : BroadcastReceiver() {
if (ACTION_REPLY == intent.action) {
val message = intent.getParcelableExtra<PushMessage>(EXTRA_PUSH_MESSAGE)
message?.let {
launch(UI) {
MainScope().launch {
val notificationId = it.notificationId.toInt()
val hostname = it.info.host
try {
......
......@@ -30,7 +30,7 @@ import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import com.squareup.moshi.Json
import com.squareup.moshi.Moshi
import kotlinx.coroutines.experimental.runBlocking
import kotlinx.coroutines.runBlocking
import se.ansman.kotshi.JsonSerializable
import se.ansman.kotshi.KotshiConstructor
import timber.log.Timber
......
......@@ -3,13 +3,12 @@ package chat.rocket.android.server.di
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.server.presentation.ChangeServerNavigator
import chat.rocket.android.server.presentation.ChangeServerView
import chat.rocket.android.server.ui.ChangeServerActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.Job
@Module
class ChangeServerModule {
......
package chat.rocket.android.server.domain
import chat.rocket.core.model.ChatRoom
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class ChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
......@@ -23,15 +23,16 @@ class ChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsR
* @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 getAllByName(url: String, name: String): List<ChatRoom> = withContext(CommonPool) {
val allChatRooms = repository.get(url)
if (name.isEmpty()) {
return@withContext allChatRooms
suspend fun getAllByName(url: String, name: String): List<ChatRoom> =
withContext(Dispatchers.IO) {
val allChatRooms = repository.get(url)
if (name.isEmpty()) {
return@withContext allChatRooms
}
return@withContext allChatRooms.filter {
it.name.contains(name, true)
}
}
return@withContext allChatRooms.filter {
it.name.contains(name, true)
}
}
/**
* Get a specific [ChatRoom] by its id.
......@@ -40,11 +41,12 @@ class ChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsR
* @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) {
return@withContext repository.get(serverUrl).find {
it.id == roomId
suspend fun getById(serverUrl: String, roomId: String): ChatRoom? =
withContext(Dispatchers.IO) {
return@withContext repository.get(serverUrl).find {
it.id == roomId
}
}
}
/**
* Get a specific [ChatRoom] by its name.
......
......@@ -3,8 +3,9 @@ package chat.rocket.android.server.domain
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.core.internal.rest.permissions
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
......@@ -17,7 +18,7 @@ class RefreshPermissionsInteractor @Inject constructor(
) {
fun refreshAsync(server: String) {
launch(CommonPool) {
GlobalScope.launch(Dispatchers.IO) {
try {
factory.create(server).let { client ->
val permissions = retryIO(
......
......@@ -3,9 +3,10 @@ package chat.rocket.android.server.domain
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.core.internal.rest.settings
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
......@@ -18,10 +19,11 @@ class RefreshSettingsInteractor @Inject constructor(
) {
private var settingsFilter = arrayOf(
UNIQUE_IDENTIFIER,
LDAP_ENABLE,
CAS_ENABLE,
CAS_LOGIN_URL,
ACCOUNT_REGISTRATION,
ACCOUNT_LOGIN_FORM,
ACCOUNT_PASSWORD_RESET,
......@@ -37,6 +39,12 @@ class RefreshSettingsInteractor @Inject constructor(
ACCOUNT_WORDPRESS,
ACCOUNT_WORDPRESS_URL,
JITSI_ENABLED,
JISTI_ENABLE_CHANNELS,
JITSI_SSL,
JITSI_DOMAIN,
JITSI_URL_ROOM_PREFIX,
SITE_URL,
SITE_NAME,
FAVICON_512,
......@@ -65,7 +73,7 @@ class RefreshSettingsInteractor @Inject constructor(
)
suspend fun refresh(server: String) {
withContext(CommonPool) {
withContext(Dispatchers.IO) {
factory.create(server).let { client ->
val settings = retryIO(
description = "settings",
......@@ -81,7 +89,7 @@ class RefreshSettingsInteractor @Inject constructor(
}
fun refreshAsync(server: String) {
launch(CommonPool) {
GlobalScope.launch(Dispatchers.IO) {
try {
refresh(server)
} catch (ex: Exception) {
......
......@@ -5,7 +5,9 @@ import chat.rocket.core.model.Value
typealias PublicSettings = Map<String, Value<Any>>
// Authentication methods.
const val UNIQUE_IDENTIFIER = "uniqueID"
// Authentication methods
const val LDAP_ENABLE = "LDAP_Enable"
const val CAS_ENABLE = "CAS_enabled"
const val CAS_LOGIN_URL = "CAS_login_url"
......@@ -24,6 +26,13 @@ const val ACCOUNT_GITLAB_URL = "API_Gitlab_URL"
const val ACCOUNT_WORDPRESS = "Accounts_OAuth_Wordpress"
const val ACCOUNT_WORDPRESS_URL = "API_Wordpress_URL"
// Video call
const val JITSI_ENABLED = "Jitsi_Enabled"
const val JISTI_ENABLE_CHANNELS = "Jisti_Enable_Channels"
const val JITSI_SSL = "Jitsi_SSL"
const val JITSI_DOMAIN = "Jitsi_Domain"
const val JITSI_URL_ROOM_PREFIX = "Jitsi_URL_Room_Prefix"
const val SITE_URL = "Site_Url"
const val SITE_NAME = "Site_Name"
const val FAVICON_196 = "Assets_favicon_192"
......@@ -54,10 +63,13 @@ const val MESSAGE_READ_RECEIPT_STORE_USERS = "Message_Read_Receipt_Store_Users"
* Extension functions for Public Settings.
*
* If you need to access a Setting, add a const val key above, add it to the filter on
* ServerPresenter.kt and a extension function to access it
* RefreshSettingsInteractor.kt and a extension function to access it.
*/
fun PublicSettings.isLdapAuthenticationEnabled(): Boolean = this[LDAP_ENABLE]?.value == true
fun PublicSettings.uniqueIdentifier(): String? = this[UNIQUE_IDENTIFIER]?.value as String?
// Authentication
fun PublicSettings.isLdapAuthenticationEnabled(): Boolean = this[LDAP_ENABLE]?.value == true
fun PublicSettings.isCasAuthenticationEnabled(): Boolean = this[CAS_ENABLE]?.value == true
fun PublicSettings.casLoginUrl(): String = this[CAS_LOGIN_URL]?.value.toString()
fun PublicSettings.isRegistrationEnabledForNewUsers(): Boolean = this[ACCOUNT_REGISTRATION]?.value == "Public"
......@@ -74,6 +86,13 @@ fun PublicSettings.gitlabUrl(): String? = this[ACCOUNT_GITLAB_URL]?.value as Str
fun PublicSettings.isWordpressAuthenticationEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun PublicSettings.wordpressUrl(): String? = this[ACCOUNT_WORDPRESS_URL]?.value as String?
// Video call
fun PublicSettings.isJitsiEnabled(): Boolean = this[JITSI_ENABLED]?.value == true
fun PublicSettings.isJitsiEnabledForChannels(): Boolean = this[JISTI_ENABLE_CHANNELS]?.value == true
fun PublicSettings.isJitsiSSL(): Boolean = this[JITSI_SSL]?.value == true
fun PublicSettings.jitsiDomain(): String? = this[JITSI_DOMAIN]?.value as String?
fun PublicSettings.jitsiPrefix(): String? = this[JITSI_URL_ROOM_PREFIX]?.value as String?
fun PublicSettings.useRealName(): Boolean = this[USE_REALNAME]?.value == true
fun PublicSettings.useSpecialCharsOnRoom(): Boolean = this[ALLOW_ROOM_NAME_SPECIAL_CHARS]?.value == true
fun PublicSettings.faviconLarge(): String? = this[FAVICON_512]?.value as String?
......
......@@ -23,26 +23,31 @@ 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
import kotlinx.coroutines.experimental.channels.SendChannel
import kotlinx.coroutines.experimental.channels.actor
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.selects.select
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.selects.select
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.coroutines.experimental.CoroutineContext
import kotlin.coroutines.CoroutineContext
class ConnectionManager(
internal val client: RocketChatClient,
private val dbManager: DatabaseManager
) {
) : CoroutineScope {
private var connectJob : Job? = null
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO
val statusLiveData = MutableLiveData<State>()
private val statusChannelList = CopyOnWriteArrayList<Channel<State>>()
private val statusChannel = Channel<State>(Channel.CONFLATED)
private var connectJob: Job? = null
private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>()
private val userDataChannels = ArrayList<Channel<Myself>>()
......@@ -60,7 +65,7 @@ class ConnectionManager(
private val messagesContext = newSingleThreadContext("messagesContext")
fun connect() {
if (connectJob?.isActive == true && (state !is State.Disconnected)) {
if (connectJob?.isActive == true && state !is State.Disconnected) {
Timber.d("Already connected, just returning...")
return
}
......@@ -78,32 +83,32 @@ class ConnectionManager(
when (status) {
is State.Connected -> {
dbManager.clearUsersStatus()
client.subscribeSubscriptions { _, id ->
Timber.d("Subscribed to subscriptions: $id")
subscriptionId = id
}
client.subscribeRooms { _, id ->
Timber.d("Subscribed to rooms: $id")
roomsId = id
}
client.subscribeUserData { _, id ->
Timber.d("Subscribed to the userData id: $id")
userDataId = id
}
client.subscribeActiveUsers { _, id ->
Timber.d("Subscribed to the activeUser id: $id")
activeUserId = id
}
resubscribeRooms()
temporaryStatus?.let { client.setTemporaryStatus(it) }
temporaryStatus?.let { tempStatus ->
client.setTemporaryStatus(tempStatus)
}
}
is State.Waiting -> {
Timber.d("Connection in: ${status.seconds}")
}
is State.Waiting -> Timber.d("Connection in: ${status.seconds}")
}
statusLiveData.postValue(status)
......@@ -116,8 +121,9 @@ class ConnectionManager(
}
var totalBatchedUsers = 0
val userActor = createBatchActor<User>(activeUsersContext, parent = connectJob,
maxSize = 500, maxTime = 1000) { users ->
val userActor = createBatchActor<User>(
activeUsersContext, parent = connectJob, maxSize = 500, maxTime = 1000
) { users ->
totalBatchedUsers += users.size
Timber.d("Processing Users batch: ${users.size} - $totalBatchedUsers")
......@@ -125,8 +131,9 @@ class ConnectionManager(
dbManager.processUsersBatch(users)
}
val roomsActor = createBatchActor<StreamMessage<BaseRoom>>(roomsContext, parent = connectJob,
maxSize = 10) { batch ->
val roomsActor = createBatchActor<StreamMessage<BaseRoom>>(
roomsContext, parent = connectJob, maxSize = 10
) { batch ->
Timber.d("processing Stream batch: ${batch.size} - $batch")
dbManager.processChatRoomsBatch(batch)
......@@ -141,8 +148,9 @@ class ConnectionManager(
}
}
val messagesActor = createBatchActor<Message>(messagesContext, parent = connectJob,
maxSize = 100, maxTime = 500) { messages ->
val messagesActor = createBatchActor<Message>(
messagesContext, parent = connectJob, maxSize = 100, maxTime = 500
) { messages ->
Timber.d("Processing Messages batch: ${messages.size}")
dbManager.processMessagesBatch(messages.distinctBy { it.id })
......@@ -155,7 +163,7 @@ class ConnectionManager(
}
// stream-notify-user - ${userId}/rooms-changed
launch(parent = connectJob) {
launch {
for (room in client.roomsChannel) {
Timber.d("GOT Room streamed")
roomsActor.send(room)
......@@ -168,7 +176,7 @@ class ConnectionManager(
}
// stream-notify-user - ${userId}/subscriptions-changed
launch(parent = connectJob) {
launch {
for (subscription in client.subscriptionsChannel) {
Timber.d("GOT Subscription streamed")
roomsActor.send(subscription)
......@@ -176,7 +184,7 @@ class ConnectionManager(
}
// stream-room-messages - $roomId
launch(parent = connectJob) {
launch {
for (message in client.messagesChannel) {
Timber.d("Received new Message for room ${message.roomId}")
messagesActor.send(message)
......@@ -184,7 +192,7 @@ class ConnectionManager(
}
// userData
launch(parent = connectJob) {
launch {
for (myself in client.userDataChannel) {
Timber.d("Got userData")
dbManager.updateSelfUser(myself)
......@@ -195,7 +203,7 @@ class ConnectionManager(
}
// activeUsers
launch(parent = connectJob) {
launch {
for (user in client.activeUsersChannel) {
userActor.send(user)
}
......@@ -284,16 +292,18 @@ class ConnectionManager(
}
}
private inline fun <T> createBatchActor(context: CoroutineContext = CommonPool,
parent: Job? = null,
maxSize: Int = 100,
maxTime: Int = 500,
crossinline block: (List<T>) -> Unit): SendChannel<T> {
return actor(context, parent = parent) {
private inline fun <T> createBatchActor(
context: CoroutineContext = Dispatchers.IO,
parent: Job? = null,
maxSize: Int = 100,
maxTime: Int = 500,
crossinline block: (List<T>) -> Unit
): SendChannel<T> {
return actor(context) {
val batch = ArrayList<T>(maxSize)
var deadline = 0L // deadline for sending this batch to callback block
while(true) {
while (true) {
// when deadline is reached or size is exceeded, pass the batch to the callback block
val remainingTime = deadline - System.currentTimeMillis()
if (batch.isNotEmpty() && remainingTime <= 0 || batch.size >= maxSize) {
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.model.AttachmentActionEntity
import chat.rocket.android.db.model.AttachmentEntity
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.db.model.*
import chat.rocket.android.util.retryDB
import chat.rocket.common.model.SimpleRoom
import chat.rocket.common.model.SimpleUser
......@@ -21,8 +16,8 @@ import chat.rocket.core.model.messageTypeOf
import chat.rocket.core.model.url.Meta
import chat.rocket.core.model.url.ParsedUrl
import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
suspend fun map(message: FullMessage): Message? = map(listOf(message)).firstOrNull()
......@@ -57,7 +52,8 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
val attachments = this.attachments?.let { mapAttachments(it).asReversed() }
val messageType = messageTypeOf(this.message.type)
list.add(Message(
list.add(
Message(
id = this.message.id,
roomId = this.message.roomId,
message = this.message.message,
......@@ -81,7 +77,8 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
role = this.message.role,
synced = this.message.synced,
unread = this.message.unread
))
)
)
}
}
......@@ -105,13 +102,19 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
val parsedUrl = url.hostname?.let {
ParsedUrl(host = it)
}
val meta = if (!url.description.isNullOrEmpty() || !url.imageUrl.isNullOrEmpty() || !url.title.isNullOrEmpty()) {
val raw = HashMap<String, String>()
if (url.description != null) raw["ogDescription"] = url.description
if (url.title != null) raw["ogTitle"] = url.title
if (url.imageUrl != null) raw["ogImage"] = url.imageUrl
Meta(title = url.title,description = url.description, imageUrl = url.imageUrl, raw = raw)
} else null
val meta =
if (!url.description.isNullOrEmpty() || !url.imageUrl.isNullOrEmpty() || !url.title.isNullOrEmpty()) {
val raw = HashMap<String, String>()
if (url.description != null) raw["ogDescription"] = url.description
if (url.title != null) raw["ogTitle"] = url.title
if (url.imageUrl != null) raw["ogImage"] = url.imageUrl
Meta(
title = url.title,
description = url.description,
imageUrl = url.imageUrl,
raw = raw
)
} else null
list.add(Url(url = url.url, meta = meta, parsedUrl = parsedUrl))
}
......@@ -134,7 +137,7 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
attachments.forEach { attachment ->
with(attachment) {
val fields = if (hasFields) {
withContext(CommonPool) {
withContext(Dispatchers.IO) {
retryDB("getAttachmentFields(${attachment._id})") {
dbManager.messageDao().getAttachmentFields(attachment._id)
}
......@@ -143,7 +146,7 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
null
}
val actions = if (hasActions) {
withContext(CommonPool) {
withContext(Dispatchers.IO) {
retryDB("getAttachmentActions(${attachment._id})") {
dbManager.messageDao().getAttachmentActions(attachment._id)
}
......@@ -151,8 +154,8 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
} else {
null
}
list.add(Attachment(
list.add(
Attachment(
title = title,
type = type,
description = description,
......@@ -179,9 +182,10 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
fields = fields,
fallback = fallback,
buttonAlignment = if (actions != null && actions.isNotEmpty()) buttonAlignment
?: "vertical" else null,
?: "vertical" else null,
actions = actions
))
)
)
}
}
return list
......@@ -189,9 +193,11 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
private fun mapAction(action: AttachmentActionEntity): Action? {
return when (action.type) {
"button" -> ButtonAction(action.type, action.text, action.url, action.isWebView,
action.webViewHeightRatio, action.imageUrl, action.message,
action.isMessageInChatWindow)
"button" -> ButtonAction(
action.type, action.text, action.url, action.isWebView,
action.webViewHeightRatio, action.imageUrl, action.message,
action.isMessageInChatWindow
)
else -> null
}
}
......
......@@ -6,41 +6,42 @@ 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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DatabaseMessagesRepository(
private val dbManager: DatabaseManager,
private val mapper: DatabaseMessageMapper
) : MessagesRepository {
override suspend fun getById(id: String): Message? = withContext(CommonPool) {
override suspend fun getById(id: String): Message? = withContext(Dispatchers.IO) {
retryDB("getMessageById($id)") {
dbManager.messageDao().getMessageById(id)?.let { message -> mapper.map(message) }
}
}
override suspend fun getByRoomId(roomId: String): List<Message> = withContext(CommonPool) {
override suspend fun getByRoomId(roomId: String): List<Message> = withContext(Dispatchers.IO) {
// 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)
retryDB("getMessagesByRoomId($roomId)") {
dbManager.messageDao().getMessagesByRoomId(roomId)
.distinctBy { it.message.message.id }
.let { messages ->
mapper.map(messages)
}
.distinctBy { it.message.message.id }
.let { messages ->
mapper.map(messages)
}
}
}
override suspend fun getRecentMessages(roomId: String, count: Long): List<Message> = withContext(CommonPool) {
retryDB("getRecentMessagesByRoomId($roomId, $count)") {
dbManager.messageDao().getRecentMessagesByRoomId(roomId, count)
override suspend fun getRecentMessages(roomId: String, count: Long): List<Message> =
withContext(Dispatchers.IO) {
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) {
dbManager.processMessagesBatch(listOf(message)).join()
......@@ -51,24 +52,24 @@ class DatabaseMessagesRepository(
}
override suspend fun removeById(id: String) {
withContext(CommonPool) {
withContext(Dispatchers.IO) {
retryDB("delete($id)") { dbManager.messageDao().delete(id) }
}
}
override suspend fun removeByRoomId(roomId: String) {
withContext(CommonPool) {
withContext(Dispatchers.IO) {
retryDB("deleteByRoomId($roomId)") {
dbManager.messageDao().deleteByRoomId(roomId)
}
}
}
override suspend fun getAllUnsent(): List<Message> = withContext(CommonPool) {
override suspend fun getAllUnsent(): List<Message> = withContext(Dispatchers.IO) {
retryDB("getUnsentMessages") {
dbManager.messageDao().getUnsentMessages()
.distinctBy { it.message.message.id }
.let { mapper.map(it) }
.distinctBy { it.message.message.id }
.let { mapper.map(it) }
}
}
......@@ -76,7 +77,7 @@ class DatabaseMessagesRepository(
dbManager.sendOperation(Operation.SaveLastSync(MessagesSync(roomId, timeMillis)))
}
override suspend fun getLastSyncDate(roomId: String): Long? = withContext(CommonPool) {
override suspend fun getLastSyncDate(roomId: String): Long? = withContext(Dispatchers.IO) {
retryDB("getLastSync($roomId)") {
dbManager.messageDao().getLastSync(roomId)?.timestamp
}
......
......@@ -45,10 +45,10 @@ import chat.rocket.core.internal.rest.serverInfo
import chat.rocket.core.internal.rest.settingsOauth
import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.withContext
import timber.log.Timber
private const val SERVICE_NAME_FACEBOOK = "facebook"
......@@ -220,7 +220,7 @@ abstract class CheckServerPresenter constructor(
}
removeAccountInteractor?.remove(currentServer)
tokenRepository?.remove(currentServer)
withContext(CommonPool) { dbManager.logout() }
withContext(Dispatchers.IO) { dbManager.logout() }
navigator?.switchOrAddNewServer()
} catch (ex: Exception) {
Timber.e(ex, "Error cleaning up the session...")
......
......@@ -26,7 +26,6 @@ fun Context.changeServerIntent(serverUrl: String? = null, chatRoomId: String? =
class ChangeServerActivity : AppCompatActivity(), ChangeServerView {
@Inject lateinit var presenter: ChangeServerPresenter
var progress: ProgressDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
......
......@@ -7,7 +7,7 @@ import chat.rocket.android.settings.presentation.SettingsView
import chat.rocket.android.settings.ui.SettingsFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.Job
@Module
class SettingsFragmentModule {
......
......@@ -7,7 +7,7 @@ import chat.rocket.android.settings.password.presentation.PasswordView
import chat.rocket.android.settings.password.ui.PasswordFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.Job
@Module
class PasswordFragmentModule {
......
......@@ -6,7 +6,9 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.model.ChatRoomEntity
import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.server.domain.GetConnectingServerInteractor
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.isJitsiEnabled
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.avatarUrl
......@@ -14,8 +16,8 @@ import chat.rocket.android.util.retryIO
import chat.rocket.common.model.RoomType
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.createDirectMessage
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
......@@ -24,13 +26,15 @@ class UserDetailsPresenter @Inject constructor(
private val dbManager: DatabaseManager,
private val strategy: CancelStrategy,
private val navigator: ChatRoomNavigator,
serverInteractor: GetConnectingServerInteractor,
settingsInteractor: GetSettingsInteractor,
serverInteractor: CurrentServerRepository,
factory: ConnectionManagerFactory
) {
private var currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer)
private val client = manager.client
private val interactor = FetchChatRoomsInteractor(client, dbManager)
private val settings = settingsInteractor.get(currentServer)
private lateinit var userEntity: UserEntity
fun loadUserDetails(userId: String) {
......@@ -47,12 +51,13 @@ class UserDetailsPresenter @Inject constructor(
userEntity.utcOffset // TODO Convert UTC and display like the mockup
if (avatarUrl != null && username != null && name != null && utcOffset != null) {
view.showUserDetails(
view.showUserDetailsAndActions(
avatarUrl = avatarUrl,
name = name,
username = username,
status = userEntity.status,
utcOffset = utcOffset.toString()
utcOffset = utcOffset.toString(),
isVideoCallAllowed = settings.isJitsiEnabled()
)
} else {
throw Exception()
......@@ -76,7 +81,7 @@ class UserDetailsPresenter @Inject constructor(
try {
view.showLoading()
withContext(DefaultDispatcher) {
withContext(Dispatchers.Default) {
val directMessage = retryIO("createDirectMessage($username") {
client.createDirectMessage(username)
}
......@@ -118,4 +123,25 @@ class UserDetailsPresenter @Inject constructor(
}
}
}
fun toVideoConference(username: String) {
launchUI(strategy) {
try {
withContext(Dispatchers.Default) {
val directMessage = retryIO("createDirectMessage($username") {
client.createDirectMessage(username)
}
navigator.toVideoConference(directMessage.id, RoomType.DIRECT_MESSAGE)
}
} catch (ex: Exception) {
Timber.e(ex)
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
}
}
......@@ -13,12 +13,14 @@ interface UserDetailsView : LoadingView, MessageView {
* @param username The user's username.
* @param status The user's status.
* @param utcOffset The user's UTC offset.
* @param isVideoCallAllowed True if the video call is allowed, false otherwise.
*/
fun showUserDetails(
fun showUserDetailsAndActions(
avatarUrl: String,
name: String,
username: String,
status: String,
utcOffset: String
utcOffset: String,
isVideoCallAllowed: Boolean
)
}
......@@ -52,7 +52,8 @@ class UserDetailsFragment : Fragment(), UserDetailsView {
arguments?.run {
userId = getString(BUNDLE_USER_ID, "")
} ?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" }
}
?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" }
}
override fun onCreateView(
......@@ -76,12 +77,13 @@ class UserDetailsFragment : Fragment(), UserDetailsView {
super.onDestroyView()
}
override fun showUserDetails(
override fun showUserDetailsAndActions(
avatarUrl: String,
name: String,
username: String,
status: String,
utcOffset: String
utcOffset: String,
isVideoCallAllowed: Boolean
) {
val requestBuilder = Glide.with(this).load(avatarUrl)
......@@ -99,6 +101,13 @@ class UserDetailsFragment : Fragment(), UserDetailsView {
// We should also setup the user details listeners.
text_message.setOnClickListener { presenter.createDirectMessage(username) }
if (isVideoCallAllowed) {
text_video_call.isVisible = true
text_video_call.setOnClickListener { presenter.toVideoConference(username) }
} else {
text_video_call.isVisible = false
}
}
override fun showLoading() {
......
......@@ -2,26 +2,25 @@ 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
import kotlinx.coroutines.experimental.isActive
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import timber.log.Timber
import kotlin.coroutines.experimental.coroutineContext
import kotlin.coroutines.coroutineContext
const val DEFAULT_RETRY = 3
private const val DEFAULT_DB_RETRY = 15
suspend fun <T> retryIO(
description: String = "<missing description>",
times: Int = DEFAULT_RETRY,
initialDelay: Long = 100, // 0.1 second
maxDelay: Long = 1000, // 1 second
factor: Double = 2.0,
block: suspend () -> T): T
{
description: String = "<missing description>",
times: Int = DEFAULT_RETRY,
initialDelay: Long = 100, // 0.1 second
maxDelay: Long = 1000, // 1 second
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) { currentTry ->
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
if (!coroutineContext.isActive) throw Exception("Job canceled when trying to execute retryIO")
try {
return block()
} catch (e: RocketChatNetworkErrorException) {
......@@ -29,26 +28,26 @@ suspend fun <T> retryIO(
e.printStackTrace()
}
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
if (!coroutineContext.isActive) throw Exception("Job canceled when trying to execute retryIO")
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
if (!coroutineContext.isActive) throw Exception("Job canceled when trying to execute retryIO")
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 = 1500, // 1.5 second
factor: Double = 1.2,
block: suspend () -> T): T
{
description: String = "<missing description>",
times: Int = DEFAULT_DB_RETRY,
initialDelay: Long = 100, // 0.1 second
maxDelay: Long = 1500, // 1.5 second
factor: Double = 1.2,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) { currentTry ->
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
if (!coroutineContext.isActive) throw Exception("Job canceled when trying to execute retryDB")
try {
return block()
} catch (e: SQLiteDatabaseLockedException) {
......@@ -56,11 +55,11 @@ suspend fun <T> retryDB(
e.printStackTrace()
}
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
if (!coroutineContext.isActive) throw Exception("Job canceled when trying to execute retryDB")
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
if (!coroutineContext.isActive) throw Exception("Job canceled when trying to execute retryDB")
return block() // last attempt
}
\ No newline at end of file
......@@ -3,9 +3,10 @@ package chat.rocket.android.util.extensions
import android.os.Looper
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
inline fun Fragment.ui(crossinline block: (activity: FragmentActivity) -> Unit): Job? {
// Checking first for activity and view saves us from some synchronyzed and thread local checks
......@@ -16,7 +17,7 @@ inline fun Fragment.ui(crossinline block: (activity: FragmentActivity) -> Unit):
null
} else {
// Launch a Job on the UI context and check again if the activity and view are still valid
launch(UI) {
GlobalScope.launch(Dispatchers.Main) {
if (activity != null && view != null && context != null) {
block(activity!!)
}
......
......@@ -7,15 +7,15 @@ import chat.rocket.android.util.retryIO
import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.model.Message
import chat.rocket.core.model.asString
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
suspend fun RocketChatClientFactory.registerPushToken(
token: String,
accounts: List<Account>
) {
withContext(CommonPool) {
withContext(Dispatchers.IO) {
accounts.forEach { account ->
try {
retryIO(description = "register push token: ${account.serverUrl}") {
......
......@@ -2,28 +2,28 @@ package chat.rocket.android.util.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import kotlin.coroutines.experimental.CoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
class TransformedLiveData<Source, Output>(
private val runContext: CoroutineContext = CommonPool,
private val runContext: CoroutineContext = Dispatchers.IO,
private val source: LiveData<Source>,
private val transformation: (Source?) -> Output?)
: LiveData<Output>() {
private val transformation: (Source?) -> Output?
) : LiveData<Output>() {
private var job: Job? = null
private val observer = Observer<Source> { source ->
job?.cancel()
job = launch(runContext) {
job = GlobalScope.launch(runContext) {
transformation(source)?.let { transformed ->
// Could have used postValue instead, but using the UI context I can guarantee that
// a canceled job will never emit values.
withContext(UI) {
withContext(Dispatchers.Main) {
value = transformed
}
}
......@@ -41,5 +41,6 @@ class TransformedLiveData<Source, Output>(
}
fun <Source, Output> LiveData<Source>.transform(
runContext: CoroutineContext = CommonPool,
transformation: (Source?) -> Output?) = TransformedLiveData(runContext, this, transformation)
\ No newline at end of file
runContext: CoroutineContext = Dispatchers.IO,
transformation: (Source?) -> Output?
) = TransformedLiveData(runContext, this, transformation)
\ No newline at end of file
......@@ -3,21 +3,22 @@ package chat.rocket.android.util.livedata
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.launch
import kotlin.coroutines.experimental.CoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
class WrappedLiveData<Source, Output>(
private val runContext: CoroutineContext = CommonPool,
private val runContext: CoroutineContext = Dispatchers.IO,
private val source: LiveData<Source>,
private val transformation: suspend (Source?, MutableLiveData<Output>) -> Unit)
: MutableLiveData<Output>() {
private val transformation: suspend (Source?, MutableLiveData<Output>) -> Unit
) : MutableLiveData<Output>() {
private var job: Job? = null
private val observer = Observer<Source> { source ->
job?.cancel()
job = launch(runContext) {
job = GlobalScope.launch(runContext) {
transformation(source, this@WrappedLiveData)
}
}
......@@ -33,6 +34,7 @@ class WrappedLiveData<Source, Output>(
}
fun <Source, Output> LiveData<Source>.wrap(
runContext: CoroutineContext = CommonPool,
transformation: suspend (Source?, MutableLiveData<Output>) -> Unit) =
WrappedLiveData(runContext, this, transformation)
\ No newline at end of file
runContext: CoroutineContext = Dispatchers.IO,
transformation: suspend (Source?, MutableLiveData<Output>) -> Unit
) =
WrappedLiveData(runContext, this, transformation)
\ No newline at end of file
package chat.rocket.android.videoconference.di
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.videoconference.presenter.JitsiVideoConferenceView
import chat.rocket.android.videoconference.ui.VideoConferenceActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.Job
@Module
class VideoConferenceModule {
@Provides
@PerActivity
fun provideVideoConferenceView(activity: VideoConferenceActivity): JitsiVideoConferenceView {
return activity
}
@Provides
@PerActivity
fun provideJob() = Job()
@Provides
@PerActivity
fun provideLifecycleOwner(activity: VideoConferenceActivity): LifecycleOwner = activity
@Provides
@PerActivity
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy =
CancelStrategy(owner, jobs)
}
\ No newline at end of file
package chat.rocket.android.videoconference.presenter
interface JitsiVideoConferenceView {
/**
* Starts the Jitsi video conference.
*
* @param url The video conference URL to be loaded.
* @param name The user name to be show on the video conference.
*/
fun startJitsiVideoConference(url: String, name: String?)
/**
* Finishes the Jitsi video conference.
*/
fun finishJitsiVideoConference()
/**
* Logs the state of the Jitsi Meet conference displayed in a JitsiMeetView.
*
* @param message The message to log.
* @param map the map information by Jitsi
*/
fun logJitsiMeetViewState(message: String, map: MutableMap<String, Any>?)
}
\ No newline at end of file
package chat.rocket.android.videoconference.presenter
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.SubscriptionTypeEvent
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.JitsiHelper
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.updateJitsiTimeout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import kotlin.concurrent.timer
class VideoConferencePresenter @Inject constructor(
private val view: JitsiVideoConferenceView,
private val strategy: CancelStrategy,
private val currentServerRepository: CurrentServerRepository,
private val connectionManagerFactory: ConnectionManagerFactory,
private val settings: GetSettingsInteractor,
private val userHelp: UserHelper,
private val analyticsManager: AnalyticsManager
) {
private lateinit var client: RocketChatClient
private lateinit var publicSettings: PublicSettings
private lateinit var chatRoomId: String
private lateinit var chatRoomType: String
private lateinit var timer: Timer
fun setup(chatRoomId: String, chatRoomType: String) {
currentServerRepository.get()?.let {
client = connectionManagerFactory.create(it).client
publicSettings = settings.get(it)
}
this.chatRoomId = chatRoomId
this.chatRoomType = chatRoomType
}
fun initVideoConference() {
launchUI(strategy) {
try {
with(publicSettings) {
view.startJitsiVideoConference(
JitsiHelper.getJitsiUrl(
isJitsiSSL(),
jitsiDomain(),
jitsiPrefix(),
uniqueIdentifier(),
chatRoomId
),
userHelp.user()?.username
)
updateJitsiTimeout()
logVideoConferenceEvent()
}
} catch (ex: Exception) {
Timber.e(ex)
view.finishJitsiVideoConference()
}
}
}
fun invalidateTimer() = timer.cancel()
// Jitsi update call needs to be called every 10 seconds to make sure call is not ended and is available to web users.
private fun updateJitsiTimeout() {
timer = timer(daemon = false, initialDelay = 0L, period = 10000) {
GlobalScope.launch(Dispatchers.IO + strategy.jobs) {
client.updateJitsiTimeout(chatRoomId)
}
}
}
private fun logVideoConferenceEvent() = when {
roomTypeOf(chatRoomType) is RoomType.DirectMessage ->
analyticsManager.logVideoConference(SubscriptionTypeEvent.DirectMessage)
roomTypeOf(chatRoomType) is RoomType.Channel ->
analyticsManager.logVideoConference(SubscriptionTypeEvent.Channel)
else -> analyticsManager.logVideoConference(SubscriptionTypeEvent.Group)
}
}
package chat.rocket.android.videoconference.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.os.bundleOf
import chat.rocket.android.videoconference.presenter.JitsiVideoConferenceView
import chat.rocket.android.videoconference.presenter.VideoConferencePresenter
import dagger.android.AndroidInjection
import org.jitsi.meet.sdk.JitsiMeetActivity
import org.jitsi.meet.sdk.JitsiMeetView
import org.jitsi.meet.sdk.JitsiMeetViewListener
import timber.log.Timber
import javax.inject.Inject
fun Context.videoConferenceIntent(chatRoomId: String, chatRoomType: String): Intent =
Intent(this, VideoConferenceActivity::class.java)
.putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
.putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType)
private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
class VideoConferenceActivity : JitsiMeetActivity(), JitsiVideoConferenceView,
JitsiMeetViewListener {
@Inject
lateinit var presenter: VideoConferencePresenter
private lateinit var chatRoomId: String
private lateinit var chatRoomType: String
private var view: JitsiMeetView? = null
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE)
requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" }
view = JitsiMeetView(this)
view?.listener = this
setContentView(view)
presenter.setup(chatRoomId, chatRoomType)
presenter.initVideoConference()
}
override fun onConferenceWillJoin(map: MutableMap<String, Any>?) =
logJitsiMeetViewState("Joining video conferencing", map)
override fun onConferenceJoined(map: MutableMap<String, Any>?) =
logJitsiMeetViewState("Joined video conferencing", map)
override fun onConferenceWillLeave(map: MutableMap<String, Any>?) =
logJitsiMeetViewState("Leaving video conferencing", map)
override fun onConferenceLeft(map: MutableMap<String, Any>?) {
logJitsiMeetViewState("Left video conferencing", map)
finishJitsiVideoConference()
}
override fun onLoadConfigError(map: MutableMap<String, Any>?) =
logJitsiMeetViewState("Error loading video conference config", map)
override fun onConferenceFailed(map: MutableMap<String, Any>?) =
logJitsiMeetViewState("Video conference failed", map)
override fun startJitsiVideoConference(url: String, name: String?) {
view?.loadURLObject(
bundleOf(
"config" to bundleOf(
"startWithAudioMuted" to true,
"startWithVideoMuted" to true
),
"context" to bundleOf(
"user" to bundleOf("name" to name),
"iss" to "rocketchat-android"
),
"url" to url
)
)
}
override fun finishJitsiVideoConference() {
presenter.invalidateTimer()
view?.dispose()
view = null
finish()
}
override fun logJitsiMeetViewState(message: String, map: MutableMap<String, Any>?) =
Timber.i("$message: $map")
}
......@@ -31,8 +31,8 @@ class WebViewActivity : AppCompatActivity() {
setContentView(R.layout.activity_web_view)
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
toolbarTitle = intent.getStringExtra(TOOLBAR_TITLE)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
toolbarTitle = intent.getStringExtra(TOOLBAR_TITLE)
setupToolbar()
}
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="#FFFFFFFF"
android:fillType="nonZero"
android:pathData="M14.748,13.572l3.752,3.752 -1.176,1.176 -3.752,-3.752a7.435,7.435 0,1 1,1.176 -1.176zM8.935,14.952a6.017,6.017 0,1 0,0 -12.034,6.017 6.017,0 0,0 0,12.035z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
......
<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:fillColor="#FFFFFFFF"
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="#1d74f5"
android:fillType="evenOdd"
android:pathData="M16.538,13.564l-3.388,-1.09v-2.07l3.394,-1.085 -0.006,4.245zM11.696,14.56L3.454,14.56L3.454,8.32h8.242v6.24zM17.428,8.107c0.362,0.261 0.57,0.69 0.57,1.176v4.312c0,0.487 -0.209,0.914 -0.57,1.175a1.37,1.37 0,0 1,-0.808 0.254c-0.164,0 -0.331,-0.026 -0.498,-0.08l-2.972,-0.956L13.15,16L2,16L2,6.88h11.15v2.01l2.973,-0.956c0.468,-0.15 0.943,-0.087 1.305,0.173zM4.424,5.44L4.424,4h6.302v1.44L4.424,5.44z" />
</vector>
......@@ -17,16 +17,11 @@
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<TextView
android:id="@+id/text_room_name"
android:id="@+id/text_toolbar_title"
style="@style/ChatRoom.ChatName.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="@dimen/text_view_drawable_padding"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/colorWhite"
android:textSize="18sp"
android:textStyle="bold"
tools:text="general" />
tools:text="Product Team" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/whitesmoke"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="@color/whitesmoke">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:background="@color/colorWhite"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:padding="15dp">
android:background="@color/colorWhite"
android:padding="16dp">
<TextView
android:id="@+id/name"
style="@style/ChatDetails.Title.TextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="#important" />
<TextView
android:id="@+id/title_description"
style="@style/ChatDetails.Title.TextView"
android:drawablePadding="@dimen/text_view_drawable_padding"
tools:text="#important"/>
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/name"
android:layout_marginTop="20dp"
android:text="@string/title_description" />
<TextView
android:id="@+id/content_description"
style="@style/ChatDetails.Content.TextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/title_description"
android:text="@string/msg_no_description" />
<TextView
android:id="@+id/title_topic"
style="@style/ChatDetails.Title.TextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_below="@+id/content_description"
android:layout_marginTop="20dp"
style="@style/ChatDetails.Title.TextView"
android:text="@string/title_topic" />
<TextView
android:id="@+id/content_topic"
style="@style/ChatDetails.Content.TextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title_topic"
style="@style/ChatDetails.Content.TextView"
android:layout_below="@+id/title_topic"
android:text="@string/msg_no_topic" />
<TextView
android:id="@+id/title_announcement"
style="@style/ChatDetails.Title.TextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/content_topic"
android:layout_marginTop="15dp"
style="@style/ChatDetails.Title.TextView"
android:layout_below="@+id/content_topic"
android:layout_marginTop="20dp"
android:text="@string/title_announcement" />
<TextView
android:id="@+id/content_announcement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title_announcement"
style="@style/ChatDetails.Content.TextView"
android:text="@string/msg_no_announcement" />
<TextView
android:id="@+id/title_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/content_announcement"
android:layout_marginTop="15dp"
style="@style/ChatDetails.Title.TextView"
android:text="@string/title_description" />
<TextView
android:id="@+id/content_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title_description"
android:layout_marginBottom="10dp"
style="@style/ChatDetails.Content.TextView"
android:text="@string/msg_no_description" />
android:layout_below="@+id/title_announcement"
android:text="@string/msg_no_announcement" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/options"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="match_parent" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
\ No newline at end of file
......@@ -8,12 +8,16 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white">
android:background="@android:color/white"
android:paddingBottom="@dimen/screen_edge_left_and_right_margins">
<ImageView
android:id="@+id/image_blur"
android:layout_width="match_parent"
android:layout_height="120dp" />
android:layout_height="120dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/image_arrow_back"
......@@ -66,10 +70,24 @@
android:layout_marginTop="24dp"
android:drawableTop="@drawable/ic_message_24dp"
android:text="@string/msg_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/text_video_call"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_username" />
<TextView
android:id="@+id/text_video_call"
style="@style/UserDetails.TextView.Actions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:drawableTop="@drawable/ic_video_24dp"
android:text="@string/msg_video_call"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/text_message"
app:layout_constraintTop_toBottomOf="@+id/text_username"
tools:visibility="visible" />
<TextView
android:id="@+id/text_title_status"
style="@style/UserDetails.TextView.Title"
......
......@@ -158,6 +158,16 @@
app:layout_constraintTop_toBottomOf="@+id/message_header"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<Button
android:id="@+id/button_join_video_call"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:text="@string/msg_join_video_call"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@+id/text_content"
app:layout_constraintTop_toBottomOf="@+id/text_content" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
......
......@@ -29,8 +29,9 @@
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="Filename.png"
tools:visibility="visible"/>
tools:visibility="visible" />
<TextView
android:id="@+id/file_description"
......@@ -40,31 +41,31 @@
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/file_name"
tools:text="Some description"
tools:visibility="visible"/>
tools:visibility="visible" />
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_attachment"
android:layout_width="0dp"
android:layout_height="150dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/file_description"
fresco:actualImageScaleType="centerCrop"
fresco:placeholderImage="@drawable/image_dummy"
tools:background="@drawable/image_dummy"
tools:visibility="visible"/>
tools:visibility="visible" />
<FrameLayout
android:id="@+id/audio_video_attachment"
android:layout_width="0dp"
android:layout_height="150dp"
android:background="@color/colorBlack"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
app:layout_constraintStart_toStartOf="@id/guideline"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/image_attachment"
tools:visibility="gone">
......@@ -85,21 +86,21 @@
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/audio_video_attachment"
tools:text="Some text"
tools:visibility="visible"/>
tools:visibility="visible" />
<TextView
android:id="@+id/text_file_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:drawableStart="@drawable/ic_files_24dp"
android:drawablePadding="6dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/colorAccent"
android:textDirection="locale"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/file_text"
android:textDirection="locale"
tools:text="This is a very, very, very long filename, to test how the layout will work on very very very long filenames.pdf"
tools:visibility="visible" />
<!-- END File attachments -->
......@@ -108,13 +109,13 @@
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:src="@drawable/quote_vertical_gray_bar"
android:scaleType="fitXY"
android:src="@drawable/quote_vertical_gray_bar"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/actions_list"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/text_file_name"
tools:visibility="visible"/>
tools:visibility="visible" />
<!-- Message attachment -->
<TextView
......@@ -173,27 +174,27 @@
android:id="@+id/author_icon"
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_marginTop="6dp"
android:layout_marginStart="8dp"
android:layout_marginTop="6dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/text_view_more"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/text_view_more"
tools:src="@tools:sample/avatars"
tools:visibility="visible"/>
tools:visibility="visible" />
<TextView
android:id="@+id/text_author_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:textColor="@color/colorAccent"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/colorAccent"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/text_view_more"
app:layout_constraintStart_toEndOf="@id/author_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/author_icon"
app:layout_constraintTop_toBottomOf="@id/text_view_more"
tools:text="#5571 - User profile from SSO must not have password change option"
tools:visibility="visible"/>
tools:visibility="visible" />
<!-- END author -->
<!-- TEXT -->
......@@ -202,15 +203,16 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/colorAccent"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/colorAccent"
android:textDirection="locale"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/text_author_name"
android:textDirection="locale"
tools:text="This is a very, very, very long filename, to test how the layout will work on very very very long filenames.pdf"
tools:visibility="visible" />
<TextView
android:id="@+id/attachment_text"
android:layout_width="0dp"
......@@ -218,11 +220,11 @@
android:layout_marginStart="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/file_name_not_file_type"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/file_name_not_file_type"
tools:text="#5571 - User profile from SSO must not have password change option"
tools:visibility="visible"/>
tools:visibility="visible" />
<!-- END TEXT -->
<!-- Fields -->
......@@ -230,14 +232,14 @@
android:id="@+id/text_fields"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
app:layout_constraintTop_toBottomOf="@id/attachment_text"
app:layout_constraintStart_toEndOf="@id/guideline"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/guideline"
app:layout_constraintTop_toBottomOf="@id/attachment_text"
tools:text="line1\nline2\n\nline3\nline4"
tools:visibility="visible"/>
tools:visibility="visible" />
<!-- END Fields -->
<!-- Actions -->
......
......@@ -12,6 +12,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:foregroundGravity="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<include layout="@layout/emoji_row_item" />
......
......@@ -62,6 +62,8 @@
<string name="action_register">Registrieren</string>
<string name="action_confirm">Bestätigen</string>
<string name="action_delete_account">Konto löschen</string>
<string name="action_favorite">Favorite</string> <!-- TODO Add translation -->
<string name="action_remove_favorite">Remove favorite</string> <!-- TODO Add translation -->
<!-- Settings List -->
<string-array name="settings_actions">
......@@ -97,6 +99,8 @@
<string name="msg_yesterday">Gestern</string>
<string name="msg_today">Heute</string>
<string name="msg_message">Nachricht</string>
<string name="msg_video_call">Video call</string> <!-- TODO Add translation -->
<string name="msg_join_video_call">Join video call</string> <!-- TODO Add translation -->
<string name="msg_this_room_is_read_only">Dieser Raum ist nur lesen</string>
<string name="msg_invalid_2fa_code">Falscher 2FA Code</string>
<string name="msg_invalid_file">Falsche Datei</string>
......@@ -164,15 +168,16 @@
<string name="msg_two_factor_authentication">Zwei-Faktor-Authentifizierung</string>
<string name="msg__your_2fa_code">Wie lautet Ihr F2A Code?</string>
<string name="msg_muted_on_this_channel">Sie sind auf diesem Kanal stummgeschaltet</string>
<string name="msg_no_topic">Kein Thema hinzugefügt</string>
<string name="msg_no_announcement">Keine Ankündigung hinzugefügt</string>
<string name="msg_no_description">Keine Beschreibung hinzugefügt</string>
<string name="msg_no_topic">No topic</string> <!-- TODO Add translation -->
<string name="msg_no_announcement">No announcement</string> <!-- TODO Add translation -->
<string name="msg_no_description">No description</string> <!-- TODO Add translation -->
<string name="msg_unable_to_update_password">Änderung des Passworts nicht möglich. Fehlermeldung: %1$s</string>
<string name="msg_password_updated_successfully">Passwort erfolgreich geändert</string>
<plurals name="msg_reacted_with_">
<item quantity="one">%1$ s reagierte mit %2$s</item>
<item quantity="other">%1$s reagierte mit %2$s</item>
</plurals>
<string name="msg_credentials_saved_successfully">Login-Daten erfolgreich gespeichert</string>
<!-- Create channel messages -->
......@@ -216,7 +221,7 @@
<string name="message_unmuted">Benutzer %1$s nicht mehr stumm geschaltet von %2$s</string>
<string name="message_role_add">%1$s wurde gesetzt %2$s von %3$s</string>
<string name="message_role_removed">%1$s ist nicht länger %2$s von %3$s</string>
<string name="message_credentials_saved_successfully">Login-Daten erfolgreich gespeichert</string>
<string name="message_video_call_started">Video call started by %1$s</string> <!-- TODO Add translation -->
<!-- Message actions -->
<string name="action_msg_reply">Antworten</string>
......@@ -244,10 +249,6 @@
<!-- Search message -->
<string name="title_search_message">Suche Nachricht</string>
<!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">Chat favorisieren</string>
<string name="title_unfavorite_chat">Chat nicht favorisieren</string>
<!-- Members List -->
<string name="title_members_list">Benutzer</string>
......
......@@ -59,6 +59,8 @@
<string name="action_register">Register</string> <!-- TODO Add translation -->
<string name="action_confirm">Confirm</string> <!-- TODO Add translation -->
<string name="action_delete_account">Delete account</string> <!-- TODO Add translation -->
<string name="action_favorite">Favorite</string> <!-- TODO Add translation -->
<string name="action_remove_favorite">Remove favorite</string> <!-- TODO Add translation -->
<!-- Settings List -->
<string-array name="settings_actions">
......@@ -94,6 +96,8 @@
<string name="msg_yesterday">Ayer</string>
<string name="msg_today">Hoy</string>
<string name="msg_message">Mensaje</string>
<string name="msg_video_call">Video call</string> <!-- TODO Add translation -->
<string name="msg_join_video_call">Join video call</string> <!-- TODO Add translation -->
<string name="msg_this_room_is_read_only">Esta sala es de solo lectura</string>
<string name="msg_invalid_2fa_code">Código 2FA no válido</string>
<string name="msg_invalid_file">Archivo inválido</string>
......@@ -176,15 +180,16 @@
<string name="msg_send_email">Send email</string> <!-- TODO - Add proper translation -->
<string name="msg_android_app_support">Android app support</string> <!-- TODO - Add proper translation -->
<string name="msg_muted_on_this_channel">You are muted on this channel</string> <!-- TODO - Add proper translation -->
<string name="msg_no_topic">No topic added</string> <!-- TODO Add translation -->
<string name="msg_no_announcement">No announcement added</string> <!-- TODO Add translation -->
<string name="msg_no_description">No description added</string> <!-- TODO Add translation -->
<string name="msg_no_topic">No topic</string> <!-- TODO Add translation -->
<string name="msg_no_announcement">No announcement</string> <!-- TODO Add translation -->
<string name="msg_no_description">No description</string> <!-- TODO Add translation -->
<string name="msg_unable_to_update_password">Unable to update password. Error message: %1$s</string> <!-- TODO - Add proper translation -->
<string name="msg_password_updated_successfully">Password updated successfully</string> <!-- TODO - Add proper translation -->
<plurals name="msg_reacted_with_">
<item quantity="one">%1$s reacted with %2$s</item> <!-- TODO - Add proper translation -->
<item quantity="other">%1$s reacted with %2$s</item> <!-- TODO - Add proper translation -->
</plurals>
<string name="msg_credentials_saved_successfully">Credenciales guardadas con éxito</string>
<!-- Preferences messages -->
<string name="msg_analytics_tracking">Analytics tracking</string> <!-- TODO Add translation -->
......@@ -205,7 +210,7 @@
<string name="message_unmuted">Usuario %1$s no silenciado por %2$s</string>
<string name="message_role_add">%1$s fue establecido %2$s por %3$s</string>
<string name="message_role_removed">%1$s ya no es %2$s por %3$s</string>
<string name="message_credentials_saved_successfully">Credenciales guardadas con éxito</string>
<string name="message_video_call_started">Video call started by %1$s</string> <!-- TODO Add translation -->
<!-- Message actions -->
<string name="action_msg_reply">Respuesta</string>
......@@ -235,10 +240,6 @@
<!-- Search message -->
<string name="title_search_message">Búsqueda de mensajes</string>
<!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">Chat favorito</string>
<string name="title_unfavorite_chat">Deshacer chat favorito</string>
<!-- Members List -->
<string name="title_members_list">Miembros</string>
......
......@@ -59,6 +59,8 @@
<string name="action_register">ثبت‌نام</string>
<string name="action_confirm">تایید</string>
<string name="action_delete_account">حذف حساب کاربری</string>
<string name="action_favorite">Favorite</string> <!-- TODO Add translation -->
<string name="action_remove_favorite">Remove favorite</string> <!-- TODO Add translation -->
<!-- Settings List -->
<string-array name="settings_actions">
......@@ -94,6 +96,8 @@
<string name="msg_yesterday">دیروز</string>
<string name="msg_today">امروز</string>
<string name="msg_message">پیام</string>
<string name="msg_video_call">Video call</string> <!-- TODO Add translation -->
<string name="msg_join_video_call">Join video call</string> <!-- TODO Add translation -->
<string name="msg_this_room_is_read_only">این اتاق فقط خواندنی است</string>
<string name="msg_invalid_2fa_code">Invalid 2FA Code</string> <!-- TODO Add translation -->
<string name="msg_invalid_file">پرونده‌ی نامعتبر</string>
......@@ -161,9 +165,9 @@
<string name="msg_two_factor_authentication">تایید هویت دو فاکتوره</string>
<string name="msg__your_2fa_code">کد 2FA تان چیست؟</string>
<string name="msg_permalink_copied">پیوند کپی شد</string>
<string name="msg_no_topic">هیچ موضوعی اضافه نشد</string>
<string name="msg_no_announcement">هیچ اعلانی اضافه نشد</string>
<string name="msg_no_description">هیچ توضیحی اضافه نشد</string>
<string name="msg_no_topic">No topic</string> <!-- TODO Add translation -->
<string name="msg_no_announcement">No announcement</string> <!-- TODO Add translation -->
<string name="msg_no_description">No description</string> <!-- TODO Add translation -->
<string name="msg_send_email">ارسال ایمیل</string>
<string name="msg_android_app_support">حمایت از اپ اندروید</string>
<string name="msg_unable_to_update_password">Unable to update password. Error message: %1$s</string> <!-- TODO Add translation -->
......@@ -172,6 +176,7 @@
<item quantity="one">%1$s reacted with %2$s</item>
<item quantity="other">%1$s reacted with %2$s</item>
</plurals> <!-- TODO Add translation -->
<string name="msg_credentials_saved_successfully">اختیارها با موفقیت ذخیره شد</string>
<!-- Create channel messages -->
<string name="msg_private_channel">خصوصی</string>
......@@ -210,7 +215,7 @@
<string name="message_unmuted">User %1$s unmuted by %2$s</string> <!-- TODO Add translation -->
<string name="message_role_add">%1$s was set %2$s by %3$s</string> <!-- TODO Add translation -->
<string name="message_role_removed">%1$s is no longer %2$s by %3$s</string> <!-- TODO Add translation -->
<string name="message_credentials_saved_successfully">اختیارها با موفقیت ذخیره شد</string>
<string name="message_video_call_started">Video call started by %1$s</string> <!-- TODO Add translation -->
<!-- Message actions -->
<string name="action_msg_reply">جواب</string>
......@@ -238,10 +243,6 @@
<!-- Search message -->
<string name="title_search_message">جست‌وجوی پیام</string>
<!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">مورد علاقه کردن گفت‌وگو</string>
<string name="title_unfavorite_chat">از مورد علاقه دراوردن گفت‌وگو</string>
<!-- Members List -->
<string name="title_members_list">اعضاء</string>
......
......@@ -59,6 +59,8 @@
<string name="action_register">registre</string>
<string name="action_confirm">Confirmer</string>
<string name="action_delete_account">Effacer le compte</string>
<string name="action_favorite">Favorite</string> <!-- TODO Add translation -->
<string name="action_remove_favorite">Remove favorite</string> <!-- TODO Add translation -->
<!-- Settings List -->
<string-array name="settings_actions">
......@@ -94,6 +96,8 @@
<string name="msg_yesterday">Hier</string>
<string name="msg_today">Aujourd\'hui</string>
<string name="msg_message">Message</string>
<string name="msg_video_call">Video call</string> <!-- TODO Add translation -->
<string name="msg_join_video_call">Join video call</string> <!-- TODO Add translation -->
<string name="msg_this_room_is_read_only">Ce salon est en lecture seule</string>
<string name="msg_invalid_2fa_code">Code 2FA non valide</string>
<string name="msg_invalid_file">Fichier non valide</string>
......@@ -176,6 +180,7 @@
<item quantity="one">%1$s a réagi avec %2$s</item>
<item quantity="other">%1$s a réagi avec %2$s</item>
</plurals>
<string name="msg_credentials_saved_successfully">Certificats sauvegardés</string>
<!-- Create channel messages -->
<string name="msg_private_channel">Privé</string>
......@@ -208,8 +213,7 @@
<string name="message_unmuted">Utilisateur %1$s a retrouvé la parole grâce à %2$s</string>
<string name="message_role_add">%1$s a été défini %2$s par %3$s</string>
<string name="message_role_removed">%1$s n\'est plus %2$s par %3$s</string>
<string name="message_credentials_saved_successfully">Certificats sauvegardés</string>
<string name="message_video_call_started">Video call started by %1$s</string> <!-- TODO Add translation -->
<!-- Message actions -->
<string name="action_msg_reply">Répondre</string>
......@@ -237,10 +241,6 @@
<!-- Search message -->
<string name="title_search_message">Rechercher message</string>
<!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">Mettre chat en favori</string>
<string name="title_unfavorite_chat">Retirer chat des favoris</string>
<!-- Members List -->
<string name="title_members_list">Membres</string>
......
......@@ -59,6 +59,8 @@
<string name="action_register">रजिस्टर</string>
<string name="action_confirm">पुष्टि करें</string>
<string name="action_delete_account">खाता हटा दो</string>
<string name="action_favorite">Favorite</string> <!-- TODO Add translation -->
<string name="action_remove_favorite">Remove favorite</string> <!-- TODO Add translation -->
<!-- Settings List -->
<string-array name="settings_actions">
......@@ -94,6 +96,8 @@
<string name="msg_yesterday">कल</string>
<string name="msg_today">आज</string>
<string name="msg_message">संदेश</string>
<string name="msg_video_call">Video call</string> <!-- TODO Add translation -->
<string name="msg_join_video_call">Join video call</string> <!-- TODO Add translation -->
<string name="msg_this_room_is_read_only">यह रूम केवल पढ़ने के लिए है</string>
<string name="msg_invalid_2fa_code">अमान्य 2FA कोड</string>
<string name="msg_invalid_file">अवैध फाइल</string>
......@@ -181,9 +185,9 @@
<string name="msg_send_email">ईमेल भेजें</string>
<string name="msg_android_app_support">एंड्रॉइड ऐप समर्थन</string>
<string name="msg_muted_on_this_channel">आप इस चैनल पर म्यूट कर रहे हैं</string>
<string name="msg_no_topic">कोई विषय नहीं जोड़ा गया</string>
<string name="msg_no_announcement">कोई घोषणा नहीं जोड़ा गया</string>
<string name="msg_no_description">कोई विवरण नहीं जोड़ा गया</string>
<string name="msg_no_topic">No topic</string> <!-- TODO Add translation -->
<string name="msg_no_announcement">No announcement</string> <!-- TODO Add translation -->
<string name="msg_no_description">No description</string> <!-- TODO Add translation -->
<string name="msg_unable_to_update_password">पासवर्ड अपडेट करने में असमर्थ। त्रुटि संदेश: %1$s</string>
<string name="msg_password_updated_successfully">पासवर्ड सफलतापूर्वक अपडेट किया गया</string>
<plurals name="msg_reacted_with_">
......@@ -191,6 +195,7 @@
<item quantity="few">%1$s ने %2$s के साथ प्रतिक्रिया व्यक्त की</item>
<item quantity="many">%1$s ने %2$s के साथ प्रतिक्रिया व्यक्त की</item>
</plurals>
<string name="msg_credentials_saved_successfully">प्रमाण पत्र सफलतापूर्वक सहेजे गए</string>
<!-- Preferences messages -->
<string name="msg_analytics_tracking">एनालिटिक्स ट्रैकिंग</string>
......@@ -211,7 +216,7 @@
<string name="message_unmuted">उपयोगकर्ता %1$s %2$s द्वारा अनम्यूट किया गया</string>
<string name="message_role_add">%1$s %3$s द्वारा %2$s सेट किया गया था</string>
<string name="message_role_removed">%1$s अब %3$s द्वारा %2$s नहीं है</string>
<string name="message_credentials_saved_successfully">प्रमाण पत्र सफलतापूर्वक सहेजे गए</string>
<string name="message_video_call_started">Video call started by %1$s</string> <!-- TODO Add translation -->
<!-- Message actions -->
<string name="action_msg_reply">जवाब दें</string>
......@@ -239,10 +244,6 @@
<!-- Search message -->
<string name="title_search_message">संदेश खोजें</string>
<!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">पसंदीदा चैट</string>
<string name="title_unfavorite_chat">नापसंद चैट</string>
<!-- Members List -->
<string name="title_members_list">सदस्य</string>
......
......@@ -59,6 +59,8 @@
<string name="action_register">Registra</string>
<string name="action_confirm">Conferma</string>
<string name="action_delete_account">Elimina utente</string>
<string name="action_favorite">Favorite</string> <!-- TODO Add translation -->
<string name="action_remove_favorite">Remove favorite</string> <!-- TODO Add translation -->
<!-- Settings List -->
<string-array name="settings_actions">
......@@ -94,6 +96,8 @@
<string name="msg_yesterday">Ieri</string>
<string name="msg_today">Oggi</string>
<string name="msg_message">Messaggio</string>
<string name="msg_video_call">Video call</string> <!-- TODO Add translation -->
<string name="msg_join_video_call">Join video call</string> <!-- TODO Add translation -->
<string name="msg_this_room_is_read_only">Questa stanza è di sola lettura</string>
<string name="msg_invalid_2fa_code">Invalido Codice 2FA non valido</string>
<string name="msg_invalid_file">Documento non valido</string>
......@@ -158,9 +162,9 @@
<string name="msg_two_factor_authentication">Autenticazione a 2 fattori (2FA)</string>
<string name="msg__your_2fa_code">Qual è il tuo codice 2FA ?</string>
<string name="msg_permalink_copied">Permalink copiato</string>
<string name="msg_no_topic">Nessun argomento aggiunto</string>
<string name="msg_no_announcement">Nessun annuncio aggiunto</string>
<string name="msg_no_description">Nessuna descrizione aggiunta</string>
<string name="msg_no_topic">No topic</string> <!-- TODO Add translation -->
<string name="msg_no_announcement">No announcement</string> <!-- TODO Add translation -->
<string name="msg_no_description">No description</string> <!-- TODO Add translation -->
<string name="msg_send_email">Invia una email</string>
<string name="msg_android_app_support">Supporto per le app Android</string>
<string name="msg_unable_to_update_password">Impossibile aggiornare la password. Messaggio di errore: %1$s</string>
......@@ -169,6 +173,7 @@
<item quantity="one">%1$s ha reagito con %2$s</item>
<item quantity="other">%1$s ha reagito con %2$s</item>
</plurals>
<string name="msg_credentials_saved_successfully">Credenziali salvate con successo</string>
<!-- Create channel messages -->
<string name="msg_private_channel">Privato</string>
......@@ -207,7 +212,7 @@
<string name="message_unmuted">Utente %1$s riattivato da %2$s</string>
<string name="message_role_add">%1$s ha il ruolo %2$s aggiunto da %3$s</string>
<string name="message_role_removed">%1$s ha il ruolo %2$s rimosso da %3$s</string>
<string name="message_credentials_saved_successfully">Credenziali salvate con successo</string>
<string name="message_video_call_started">Video call started by %1$s</string> <!-- TODO Add translation -->
<!-- Message actions -->
<string name="action_msg_reply">Rispondi</string>
......@@ -235,10 +240,6 @@
<!-- Search message -->
<string name="title_search_message">Cerca messaggio</string>
<!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">Stanza preferita</string>
<string name="title_unfavorite_chat">Stanza normale</string>
<!-- Members List -->
<string name="title_members_list">Partecipanti</string>
......
......@@ -59,6 +59,8 @@
<string name="action_register">登録</string>
<string name="action_confirm">確認</string>
<string name="action_delete_account">アカウントを削除する</string>
<string name="action_favorite">Favorite</string> <!-- TODO Add translation -->
<string name="action_remove_favorite">Remove favorite</string> <!-- TODO Add translation -->
<!-- Settings List -->
<string-array name="settings_actions">
......@@ -94,6 +96,8 @@
<string name="msg_today">今日</string>
<string name="msg_yesterday">昨日</string>
<string name="msg_message">メッセージ</string>
<string name="msg_video_call">Video call</string> <!-- TODO Add translation -->
<string name="msg_join_video_call">Join video call</string> <!-- TODO Add translation -->
<string name="msg_this_room_is_read_only">この部屋は読み取り専用です</string>
<string name="msg_invalid_2fa_code">無効な 2FA コード</string>
<string name="msg_invalid_file">無効なファイル</string>
......@@ -163,15 +167,16 @@
<string name="msg_view_more">更に表示</string>
<string name="msg_view_less">隠す</string>
<string name="msg_muted_on_this_channel">あなたはこのチャンネルでミュートされています</string>
<string name="msg_no_topic">No topic added</string> <!-- TODO Add translation -->
<string name="msg_no_announcement">No announcement added</string> <!-- TODO Add translation -->
<string name="msg_no_description">No description added</string> <!-- TODO Add translation -->
<string name="msg_no_topic">No topic</string> <!-- TODO Add translation -->
<string name="msg_no_announcement">No announcement</string> <!-- TODO Add translation -->
<string name="msg_no_description">No description</string> <!-- TODO Add translation -->
<string name="msg_unable_to_update_password">Unable to update password. Error message: %1$s</string> <!-- TODO - Add proper translation -->
<string name="msg_password_updated_successfully">Password updated successfully</string> <!-- TODO - Add proper translation -->
<plurals name="msg_reacted_with_">
<item quantity="one">%1$s reacted with %2$s</item> <!-- TODO - Add proper translation -->
<item quantity="other">%1$s reacted with %2$s</item> <!-- TODO - Add proper translation -->
<item quantity="one">%1$s reacted with %2$s</item> <!-- TODO - Add proper translation -->
<item quantity="other">%1$s reacted with %2$s</item> <!-- TODO - Add proper translation -->
</plurals>
<string name="msg_credentials_saved_successfully">資格情報を正常に保存しました</string>
<!-- Create channel messages -->
<string name="msg_private_channel">プライベート</string>
......@@ -210,7 +215,7 @@
<string name="message_unmuted">ユーザー %1$s は %2$s によってミュートされていません</string>
<string name="message_role_add">%1$s は %3$s によって %2$s に設定されました</string>
<string name="message_role_removed">%1$s は %3$s で、もう %2$s ではありません</string>
<string name="message_credentials_saved_successfully">資格情報を正常に保存しました</string>
<string name="message_video_call_started">Video call started by %1$s</string> <!-- TODO Add translation -->
<!-- Message actions -->
<string name="action_msg_reply">返信</string>
......@@ -238,10 +243,6 @@
<!-- Search message -->
<string name="title_search_message">メッセージを検索</string>
<!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">お気に入り</string>
<string name="title_unfavorite_chat">お気に入り解除</string>
<!-- Members List -->
<string name="title_members_list">メンバー</string>
......
......@@ -59,6 +59,8 @@
<string name="action_register">Registrar</string>
<string name="action_confirm">Confirmar</string>
<string name="action_delete_account">Deletar conta</string>
<string name="action_favorite">Favoritar</string>
<string name="action_remove_favorite">Remover favorito</string>
<!-- Settings List -->
<string-array name="settings_actions">
......@@ -94,6 +96,8 @@
<string name="msg_yesterday">Ontem</string>
<string name="msg_today">Hoje</string>
<string name="msg_message">Mensagem</string>
<string name="msg_video_call">Videochamada</string>
<string name="msg_join_video_call">Entrar na videochamada</string>
<string name="msg_this_room_is_read_only">Este chat é apenas de leitura</string>
<string name="msg_invalid_2fa_code">Código 2FA inválido</string>
<string name="msg_invalid_file">Arquivo inválido</string>
......@@ -168,9 +172,9 @@
<string name="msg_send_email">Enviar e-mail</string>
<string name="msg_android_app_support">Suporte ao aplicativo Android</string>
<string name="msg_muted_on_this_channel">Você está silenciado neste canal</string>
<string name="msg_no_topic">Nenhum tópico adicionado</string>
<string name="msg_no_announcement">Nenhum anúncio adicionado</string>
<string name="msg_no_description">Nenhuma descrição adicionada</string>
<string name="msg_no_topic">Nenhum tópico</string>
<string name="msg_no_announcement">Nenhum anúncio</string>
<string name="msg_no_description">Nenhuma descrição</string>
<string name="msg_unable_to_update_password">Não foi possível atualizar a senha. Mensagem de erro: %1$s</string>
<string name="msg_password_updated_successfully">Senha alterada com sucesso</string>
<string name="msg_sort">Ordenar</string>
......@@ -178,6 +182,7 @@
<item quantity="one">%1$s reagiu com %2$s</item>
<item quantity="other">%1$s reagiram com %2$s</item>
</plurals>
<string name="msg_credentials_saved_successfully">Credenciais salvas com sucesso</string>
<!-- Create channel messages -->
<string name="msg_private_channel">Privado</string>
......@@ -210,7 +215,7 @@
<string name="message_unmuted">Usuário %1$s saiu do modo silenciado por %2$s</string>
<string name="message_role_add">%1$s foi definido %2$s por %3$s</string>
<string name="message_role_removed">%1$s não é mais %2$s por %3$s</string>
<string name="message_credentials_saved_successfully">Credenciais salvas com sucesso</string>
<string name="message_video_call_started">Videochamada iniciada por %1$s</string>
<!-- Message actions -->
<string name="action_msg_reply">Responder</string>
......@@ -238,10 +243,6 @@
<!-- Search message -->
<string name="title_search_message">Procurar mensagem</string>
<!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">Marcar canal como favorito</string>
<string name="title_unfavorite_chat">Desmarcar canal como favorito</string>
<!-- Members List -->
<string name="title_members_list">Membros</string>
......
......@@ -211,7 +211,8 @@
<string name="message_unmuted">Utilizador %2$s removeu o silêncio de %1$s</string>
<string name="message_role_add">%3$s deu estatuto de %2$s a %1$s</string>
<string name="message_role_removed">%3$s retirou o estatuto de %2$s a %1$s</string>
<string name="message_credentials_saved_successfully">Credenciais guardadas com sucesso</string>
<string name="action_favorite">Marcar chat como favorito</string>
<string name="action_remove_favorite">Desmarcar chat como favorito</string>
<!-- Message actions -->
<string name="action_msg_reply">Responder</string>
......@@ -239,10 +240,6 @@
<!-- Search message -->
<string name="title_search_message">Pesquisar mensagem</string>
<!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">Marcar chat como favorito</string>
<string name="title_unfavorite_chat">Desmarcar chat como favorito</string>
<!-- Members List -->
<string name="title_members_list">Membros</string>
......
......@@ -59,6 +59,8 @@
<string name="action_register">Зарегистрировать</string>
<string name="action_confirm">Подтвердить</string>
<string name="action_delete_account">Удалить аккаунт</string>
<string name="action_favorite">Favorite</string> <!-- TODO Add translation -->
<string name="action_remove_favorite">Remove favorite</string> <!-- TODO Add translation -->
<!-- Settings List -->
<string-array name="settings_actions">
......@@ -94,6 +96,8 @@
<string name="msg_yesterday">Вчера</string>
<string name="msg_today">Сегодня</string>
<string name="msg_message">Сообщение</string>
<string name="msg_video_call">Video call</string> <!-- TODO Add translation -->
<string name="msg_join_video_call">Join video call</string> <!-- TODO Add translation -->
<string name="msg_this_room_is_read_only">Канал только для чтения</string>
<string name="msg_invalid_2fa_code">Неверный код 2FA</string>
<string name="msg_invalid_file">Неверный файл</string>
......@@ -162,9 +166,9 @@
<string name="msg_view_more">больше</string>
<string name="msg_view_less">меньше</string>
<string name="msg_permalink_copied">Ссылка скопирована</string>
<string name="msg_no_topic">Нет темы</string>
<string name="msg_no_announcement">Нет объявления</string>
<string name="msg_no_description">Нет описания</string>
<string name="msg_no_topic">No topic</string> <!-- TODO Add translation -->
<string name="msg_no_announcement">No announcement</string> <!-- TODO Add translation -->
<string name="msg_no_description">No description</string> <!-- TODO Add translation -->
<string name="msg_send_email">Отправить e-mail</string>
<string name="msg_android_app_support">Поддержка Android-приложения</string>
<string name="msg_muted_on_this_channel">Вы лишены дара речи на этом канале</string>
......@@ -175,6 +179,7 @@
<item quantity="few">%1$s реагируют с %2$s</item>
<item quantity="many">%1$s реагируют с %2$s</item>
</plurals>
<string name="msg_credentials_saved_successfully">Учетные данные успешно сохранены</string>
<!-- Create channel messages -->
<string name="msg_private_channel">Приватный</string>
......@@ -207,7 +212,7 @@
<string name="message_unmuted">Пользователю %1$s вернули дар речи по решению %2$s</string>
<string name="message_role_add">%1$s был назначен %2$s пользователем %3$s</string>
<string name="message_role_removed">%1$s больше не %2$s по решению %3$s</string>
<string name="message_credentials_saved_successfully">Учетные данные успешно сохранены</string>
<string name="message_video_call_started">Video call started by %1$s</string> <!-- TODO Add translation -->
<!-- Message actions -->
<string name="action_msg_reply">Ответить</string>
......@@ -235,10 +240,6 @@
<!-- Search message -->
<string name="title_search_message">Поиск сообщения</string>
<!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">Добавить в избранное</string>
<string name="title_unfavorite_chat">Удалить из избранного</string>
<!-- Members List -->
<string name="title_members_list">Пользователи</string>
......
......@@ -59,6 +59,8 @@
<string name="action_register">Register</string> <!-- TODO Add translation -->
<string name="action_confirm">Confirm</string> <!-- TODO Add translation -->
<string name="action_delete_account">Delete account</string> <!-- TODO Add translation -->
<string name="action_favorite">Favorite</string> <!-- TODO Add translation -->
<string name="action_remove_favorite">Remove favorite</string> <!-- TODO Add translation -->
<!-- Settings List -->
<string-array name="settings_actions">
......@@ -94,6 +96,8 @@
<string name="msg_yesterday">Dün</string>
<string name="msg_today">Bugün</string>
<string name="msg_message">Mesaj</string>
<string name="msg_video_call">Video call</string> <!-- TODO Add translation -->
<string name="msg_join_video_call">Join video call</string> <!-- TODO Add translation -->
<string name="msg_this_room_is_read_only">Bu oda sadece okunabilir modundadır</string>
<string name="msg_invalid_2fa_code">Geçersiz 2FA Kodu</string>
<string name="msg_invalid_file">Geçersiz dosya</string>
......@@ -181,15 +185,16 @@
<string name="msg_send_email">Send email</string> <!-- TODO - Add proper translation -->
<string name="msg_android_app_support">Android app support</string> <!-- TODO - Add proper translation -->
<string name="msg_muted_on_this_channel">You are muted on this channel</string> <!-- TODO - Add proper translation -->
<string name="msg_no_topic">No topic added</string> <!-- TODO Add translation -->
<string name="msg_no_announcement">No announcement added</string> <!-- TODO Add translation -->
<string name="msg_no_description">No description added</string> <!-- TODO Add translation -->
<string name="msg_no_topic">No topic</string> <!-- TODO Add translation -->
<string name="msg_no_announcement">No announcement</string> <!-- TODO Add translation -->
<string name="msg_no_description">No description</string> <!-- TODO Add translation -->
<string name="msg_unable_to_update_password">Unable to update password. Error message: %1$s</string> <!-- TODO - Add proper translation -->
<string name="msg_password_updated_successfully">Password updated successfully</string> <!-- TODO - Add proper translation -->
<plurals name="msg_reacted_with_">
<item quantity="one">%1$s reacted with %2$s</item> <!-- TODO - Add proper translation -->
<item quantity="other">%1$s reacted with %2$s</item> <!-- TODO - Add proper translation -->
</plurals>
<string name="msg_credentials_saved_successfully">Kimlik bilgileri başarıyla kaydedildi</string>
<!-- Preferences messages -->
<string name="msg_analytics_tracking">İstatistik takibi</string>
......@@ -210,7 +215,7 @@
<string name="message_unmuted">%1$s kullanıcısı %2$s tarafından sessizden çıkarıldı</string>
<string name="message_role_add">%1$s, %3$s tarafından %2$s olacak şekilde değiştirildi</string>
<string name="message_role_removed">%1$s, artık %2$s olmayacak şekilde %3$s tarafından değiştirildi</string>
<string name="message_credentials_saved_successfully">Kimlik bilgileri başarıyla kaydedildi</string>
<string name="message_video_call_started">Video call started by %1$s</string> <!-- TODO Add translation -->
<!-- Message actions -->
<string name="action_msg_reply">Kaydet</string>
......@@ -240,10 +245,6 @@
<!-- Search message -->
<string name="title_search_message">Mesajlarda arayın</string>
<!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">Sohbeti favorilerime ekle</string>
<string name="title_unfavorite_chat">Sohbeti favorilerimden kaldır</string>
<!-- Members List -->
<string name="title_members_list">Üyeler</string>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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