Merge branch 'develop' into feature/edit_message

This commit is contained in:
Valere
2019-05-27 17:08:39 +02:00
committed by GitHub
63 changed files with 2609 additions and 44 deletions

View File

@ -156,7 +156,7 @@ dependencies {
implementation("com.airbnb.android:epoxy:$epoxy_version")
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
implementation 'com.airbnb.android:mvrx:0.7.0'
implementation 'com.airbnb.android:mvrx:1.0.1'
// Work
implementation "android.arch.work:work-runtime-ktx:1.0.0"

View File

@ -43,6 +43,8 @@
android:name="im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity"
android:label="@string/title_activity_emoji_reaction_picker" />
<activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
<service
android:name=".core.services.CallService"
android:exported="false" />

View File

@ -30,6 +30,7 @@ import im.vector.riotredesign.core.di.AppModule
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.rageshake.VectorFileLogger
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
import org.koin.log.EmptyLogger
import org.koin.standalone.StandAloneContext.startKoin
import timber.log.Timber
@ -56,7 +57,8 @@ class VectorApplication : Application() {
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
val appModule = AppModule(applicationContext).definition
val homeModule = HomeModule().definition
startKoin(listOf(appModule, homeModule), logger = EmptyLogger())
val roomDirectoryModule = RoomDirectoryModule().definition
startKoin(listOf(appModule, homeModule, roomDirectoryModule), logger = EmptyLogger())
Matrix.getInstance().setApplicationFlavor(BuildConfig.FLAVOR_DESCRIPTION)
}

View File

@ -19,7 +19,9 @@ package im.vector.riotredesign.core.di
import android.content.Context
import android.content.Context.MODE_PRIVATE
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringArrayProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
@ -40,6 +42,10 @@ class AppModule(private val context: Context) {
StringProvider(context.resources)
}
single {
StringArrayProvider(context.resources)
}
single {
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
}
@ -60,6 +66,10 @@ class AppModule(private val context: Context) {
RoomSummaryComparator()
}
single {
ErrorFormatter(get())
}
single {
NotificationDrawerManager(context)
}

View File

@ -0,0 +1,46 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.riotredesign.core.epoxy
import android.widget.Button
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
@EpoxyModelClass(layout = R.layout.item_error_retry)
abstract class ErrorWithRetryItem : VectorEpoxyModel<ErrorWithRetryItem.Holder>() {
@EpoxyAttribute
var text: String? = null
@EpoxyAttribute
var listener: (() -> Unit)? = null
override fun bind(holder: Holder) {
holder.textView.text = text
holder.buttonView.setOnClickListener { listener?.invoke() }
}
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.itemErrorRetryText)
val buttonView by bind<Button>(R.id.itemErrorRetryButton)
}
}

View File

@ -16,9 +16,11 @@
package im.vector.riotredesign.core.epoxy
import android.content.Context
import android.widget.ProgressBar
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT)
class LoadingItem(context: Context) : ProgressBar(context)
@EpoxyModelClass(layout = R.layout.item_loading)
abstract class LoadingItem : VectorEpoxyModel<LoadingItem.Holder>() {
class Holder : VectorEpoxyHolder()
}

View File

@ -0,0 +1,40 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.riotredesign.core.epoxy
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
@EpoxyModelClass(layout = R.layout.item_no_result)
abstract class NoResultItem : VectorEpoxyModel<NoResultItem.Holder>() {
@EpoxyAttribute
var text: String? = null
override fun bind(holder: Holder) {
holder.textView.text = text
}
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.itemNoResultText)
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.error
import im.vector.matrix.android.api.failure.Failure
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
class ErrorFormatter(val stringProvider: StringProvider) {
fun toHumanReadable(failure: Failure): String {
// Default
return failure.localizedMessage
}
fun toHumanReadable(throwable: Throwable): String {
return when (throwable) {
is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network)
else -> throwable.localizedMessage
}
}
}

View File

@ -18,26 +18,26 @@ package im.vector.riotredesign.core.extensions
import androidx.fragment.app.Fragment
fun androidx.fragment.app.Fragment.addFragment(fragment: Fragment, frameId: Int) {
fun Fragment.addFragment(fragment: Fragment, frameId: Int) {
fragmentManager?.inTransaction { add(frameId, fragment) }
}
fun androidx.fragment.app.Fragment.replaceFragment(fragment: Fragment, frameId: Int) {
fun Fragment.replaceFragment(fragment: Fragment, frameId: Int) {
fragmentManager?.inTransaction { replace(frameId, fragment) }
}
fun androidx.fragment.app.Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
fun Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
fragmentManager?.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}
fun androidx.fragment.app.Fragment.addChildFragment(fragment: Fragment, frameId: Int) {
fun Fragment.addChildFragment(fragment: Fragment, frameId: Int) {
childFragmentManager.inTransaction { add(frameId, fragment) }
}
fun androidx.fragment.app.Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) {
fun Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) {
childFragmentManager.inTransaction { replace(frameId, fragment) }
}
fun androidx.fragment.app.Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
fun Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
childFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.extensions
import android.widget.TextView
import androidx.core.view.isVisible
/**
* Set a text in the TextView, or set visibility to GONE it if the text is null
*/
fun TextView.setTextOrHide(newText: String?, hideWhenBlank: Boolean = true) {
if (newText == null
|| (newText.isBlank() && hideWhenBlank)) {
isVisible = false
} else {
this.text = newText
isVisible = true
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.platform
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import im.vector.riotredesign.R
import kotlinx.android.synthetic.main.view_button_state.view.*
class ButtonStateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) {
sealed class State {
object Button : State()
object Loading : State()
object Loaded : State()
object Error : State()
}
var callback: Callback? = null
interface Callback {
fun onButtonClicked()
fun onRetryClicked()
}
init {
View.inflate(context, R.layout.view_button_state, this)
layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
buttonStateButton.setOnClickListener {
callback?.onButtonClicked()
}
buttonStateRetry.setOnClickListener {
callback?.onRetryClicked()
}
// Read attributes
context.theme.obtainStyledAttributes(
attrs,
R.styleable.ButtonStateView,
0, 0)
.apply {
try {
buttonStateButton.text = getString(R.styleable.ButtonStateView_bsv_button_text)
buttonStateLoaded.setImageDrawable(getDrawable(R.styleable.ButtonStateView_bsv_loaded_image_src))
} finally {
recycle()
}
}
}
fun render(newState: State) {
if (newState == State.Button) {
buttonStateButton.isVisible = true
} else {
// We use isInvisible because we want to keep button space in the layout
buttonStateButton.isInvisible = true
}
buttonStateLoading.isVisible = newState == State.Loading
buttonStateLoaded.isVisible = newState == State.Loaded
buttonStateRetry.isVisible = newState == State.Error
}
}

View File

@ -93,6 +93,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
}
protected fun Disposable.disposeOnDestroy(): Disposable {
// TODO Ganfra: never disposed...
uiDisposables.add(this)
return this
}

View File

@ -27,6 +27,9 @@ import butterknife.Unbinder
import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util.assertMainThread
import com.google.android.material.snackbar.Snackbar
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import timber.log.Timber
abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
@ -78,6 +81,12 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
mUnBinder = null
}
override fun onDestroy() {
super.onDestroy()
uiDisposables.dispose()
}
/* ==========================================================================================
* Restorable
* ========================================================================================== */
@ -100,6 +109,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
override fun invalidate() {
//no-ops by default
Timber.w("invalidate() method has not been implemented")
}
protected fun setArguments(args: Parcelable? = null) {
@ -113,6 +123,16 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed {
return this
}
/* ==========================================================================================
* Disposable
* ========================================================================================== */
private val uiDisposables = CompositeDisposable()
protected fun Disposable.disposeOnDestroy(): Disposable {
uiDisposables.add(this)
return this
}
/* ==========================================================================================
* MENU MANAGEMENT

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.resources
import android.content.res.Resources
import androidx.annotation.ArrayRes
import androidx.annotation.NonNull
class StringArrayProvider(private val resources: Resources) {
/**
* Returns a localized string array from the application's package's
* default string array table.
*
* @param resId Resource id for the string array
* @return The string array associated with the resource, stripped of styled
* text information.
*/
@NonNull
fun getStringArray(@ArrayRes resId: Int): Array<String> {
return resources.getStringArray(resId)
}
}

View File

@ -40,6 +40,7 @@ import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
import im.vector.riotredesign.features.rageshake.BugReporter
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotredesign.features.settings.VectorSettingsActivity
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
import kotlinx.android.synthetic.main.activity_home.*
@ -124,21 +125,22 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
android.R.id.home -> {
drawerLayout.openDrawer(GravityCompat.START)
return true
}
R.id.sliding_menu_settings -> {
R.id.sliding_menu_settings -> {
startActivity(VectorSettingsActivity.getIntent(this, "TODO"))
return true
}
R.id.sliding_menu_sign_out -> {
R.id.sliding_menu_sign_out -> {
SignOutUiWorker(this).perform(Matrix.getInstance().currentSession!!)
return true
}
// TODO Temporary code here to create a room
R.id.tmp_menu_create_room -> {
homeActivityViewModel.createRoom()
R.id.tmp_menu_create_room -> {
// Start Activity for now
startActivity(Intent(this, RoomDirectoryActivity::class.java))
return true
}
}

View File

@ -30,7 +30,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.LoadingItemModel_
import im.vector.riotredesign.core.epoxy.LoadingItem_
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.helper.*
@ -131,14 +131,14 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
}
override fun buildModels() {
LoadingItemModel_()
LoadingItem_()
.id("forward_loading_item")
.addWhen(Timeline.Direction.FORWARDS)
val timelineModels = getModels()
add(timelineModels)
LoadingItemModel_()
LoadingItem_()
.id("backward_loading_item")
.addWhen(Timeline.Direction.BACKWARDS)
}
@ -261,7 +261,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
}
}
private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) {
private fun LoadingItem_.addWhen(direction: Timeline.Direction) {
val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false
addIf(shouldAdd, this@TimelineEventController)
}

View File

@ -19,21 +19,30 @@ import android.os.Bundle
import com.airbnb.mvrx.MvRxView
import com.airbnb.mvrx.MvRxViewModelStore
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import java.util.*
/**
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
*/
abstract class BaseMvRxBottomSheetDialog() : BottomSheetDialogFragment(), MvRxView {
abstract class BaseMvRxBottomSheetDialog : BottomSheetDialogFragment(), MvRxView {
override val mvrxViewModelStore by lazy { MvRxViewModelStore(viewModelStore) }
private lateinit var mvrxPersistedViewId: String
final override val mvrxViewId: String by lazy { mvrxPersistedViewId }
override fun onCreate(savedInstanceState: Bundle?) {
mvrxViewModelStore.restoreViewModels(this, savedInstanceState)
mvrxPersistedViewId = savedInstanceState?.getString(PERSISTED_VIEW_ID_KEY)
?: this::class.java.simpleName + "_" + UUID.randomUUID().toString()
super.onCreate(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mvrxViewModelStore.saveViewModels(outState)
outState.putString(PERSISTED_VIEW_ID_KEY, mvrxViewId)
}
override fun onStart() {
@ -42,4 +51,6 @@ abstract class BaseMvRxBottomSheetDialog() : BottomSheetDialogFragment(), MvRxVi
// subscribe to a ViewModel.
postInvalidate()
}
}
}
private const val PERSISTED_VIEW_ID_KEY = "mvrx:bottomsheet_persisted_view_id"

View File

@ -0,0 +1,100 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.platform.ButtonStateView
import im.vector.riotredesign.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_public_room)
abstract class PublicRoomItem : VectorEpoxyModel<PublicRoomItem.Holder>() {
enum class JoinState {
NOT_JOINED,
JOINING,
JOINING_ERROR,
JOINED
}
@EpoxyAttribute
var avatarUrl: String? = null
@EpoxyAttribute
var roomId: String? = null
@EpoxyAttribute
var roomName: String? = null
@EpoxyAttribute
var nbOfMembers: Int = 0
@EpoxyAttribute
var joinState: JoinState = JoinState.NOT_JOINED
@EpoxyAttribute
var globalListener: (() -> Unit)? = null
@EpoxyAttribute
var joinListener: (() -> Unit)? = null
override fun bind(holder: Holder) {
holder.rootView.setOnClickListener { globalListener?.invoke() }
AvatarRenderer.render(avatarUrl, roomId!!, roomName, holder.avatarView)
holder.nameView.text = roomName
// TODO Use formatter for big numbers?
holder.counterView.text = nbOfMembers.toString()
holder.buttonState.render(
when (joinState) {
JoinState.NOT_JOINED -> ButtonStateView.State.Button
JoinState.JOINING -> ButtonStateView.State.Loading
JoinState.JOINED -> ButtonStateView.State.Loaded
JoinState.JOINING_ERROR -> ButtonStateView.State.Error
}
)
holder.buttonState.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
joinListener?.invoke()
}
override fun onRetryClicked() {
// Same action
onButtonClicked()
}
}
}
class Holder : VectorEpoxyHolder() {
val rootView by bind<ViewGroup>(R.id.itemPublicRoomLayout)
val avatarView by bind<ImageView>(R.id.itemPublicRoomAvatar)
val nameView by bind<TextView>(R.id.itemPublicRoomName)
val counterView by bind<TextView>(R.id.itemPublicRoomMembersCount)
val buttonState by bind<ButtonStateView>(R.id.itemPublicRoomButtonState)
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.epoxy.VisibilityState
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.errorWithRetryItem
import im.vector.riotredesign.core.epoxy.loadingItem
import im.vector.riotredesign.core.epoxy.noResultItem
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.resources.StringProvider
class PublicRoomsController(private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter) : TypedEpoxyController<PublicRoomsViewState>() {
var callback: Callback? = null
override fun buildModels(viewState: PublicRoomsViewState) {
val publicRooms = viewState.publicRooms
if (publicRooms.isEmpty()
&& viewState.asyncPublicRoomsRequest is Success) {
// No result
noResultItem {
id("noResult")
text(stringProvider.getString(R.string.no_result_placeholder))
}
} else {
publicRooms.forEach {
buildPublicRoom(it, viewState)
}
if ((viewState.hasMore && viewState.asyncPublicRoomsRequest is Success)
|| viewState.asyncPublicRoomsRequest is Incomplete) {
loadingItem {
// Change id to avoid list to scroll automatically when first results are displayed
if (publicRooms.isEmpty()) {
id("loading")
} else {
id("loadMore")
}
onVisibilityStateChanged { _, _, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.loadMore()
}
}
}
}
}
if (viewState.asyncPublicRoomsRequest is Fail) {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(viewState.asyncPublicRoomsRequest.error))
listener { callback?.loadMore() }
}
}
}
private fun buildPublicRoom(publicRoom: PublicRoom, viewState: PublicRoomsViewState) {
publicRoomItem {
id(publicRoom.roomId)
roomId(publicRoom.roomId)
avatarUrl(publicRoom.avatarUrl)
roomName(publicRoom.name)
nbOfMembers(publicRoom.numJoinedMembers)
when {
viewState.joinedRoomsIds.contains(publicRoom.roomId) -> joinState(PublicRoomItem.JoinState.JOINED)
viewState.joiningRoomsIds.contains(publicRoom.roomId) -> joinState(PublicRoomItem.JoinState.JOINING)
viewState.joiningErrorRoomsIds.contains(publicRoom.roomId) -> joinState(PublicRoomItem.JoinState.JOINING_ERROR)
else -> joinState(PublicRoomItem.JoinState.NOT_JOINED)
}
joinListener {
callback?.onPublicRoomJoin(publicRoom)
}
globalListener {
callback?.onPublicRoomClicked(publicRoom)
}
}
}
interface Callback {
fun onPublicRoomClicked(publicRoom: PublicRoom)
fun onPublicRoomJoin(publicRoom: PublicRoom)
fun loadMore()
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory
import android.os.Bundle
import android.view.View
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding2.widget.RxTextView
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotredesign.R
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.roomdirectory.picker.RoomDirectoryPickerFragment
import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_public_rooms.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
import timber.log.Timber
import java.util.concurrent.TimeUnit
/**
* What can be improved:
* - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect
*
* TODO For Nad:
* Display number of rooms?
* Picto size are not correct
* Where I put the room directory picker?
* World Readable badge
* Guest can join badge
*
*/
class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback {
private val viewModel: RoomDirectoryViewModel by activityViewModel()
private val publicRoomsController: PublicRoomsController by inject()
private val errorFormatter: ErrorFormatter by inject()
override fun getLayoutResId() = R.layout.fragment_public_rooms
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
vectorBaseActivity.setSupportActionBar(toolbar)
vectorBaseActivity.supportActionBar?.let {
it.setDisplayShowHomeEnabled(true)
it.setDisplayHomeAsUpEnabled(true)
}
RxTextView.textChanges(publicRoomsFilter)
.debounce(500, TimeUnit.MILLISECONDS)
.subscribeBy {
viewModel.filterWith(it.toString())
}
.disposeOnDestroy()
publicRoomsCreateNewRoom.setOnClickListener {
// TODO homeActivityViewModel.createRoom()
vectorBaseActivity.notImplemented()
}
publicRoomsChangeDirectory.setOnClickListener {
vectorBaseActivity.addFragmentToBackstack(RoomDirectoryPickerFragment(), R.id.simpleFragmentContainer)
}
viewModel.joinRoomErrorLiveData.observe(this, Observer {
it.getContentIfNotHandled()?.let { throwable ->
Snackbar.make(publicRoomsCoordinator, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT)
.show()
}
})
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))
setupRecyclerView()
}
private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(publicRoomsList)
val layoutManager = LinearLayoutManager(context)
publicRoomsList.layoutManager = layoutManager
publicRoomsController.callback = this
publicRoomsList.setController(publicRoomsController)
}
override fun onPublicRoomClicked(publicRoom: PublicRoom) {
Timber.v("PublicRoomClicked: $publicRoom")
vectorBaseActivity.notImplemented()
}
override fun onPublicRoomJoin(publicRoom: PublicRoom) {
Timber.v("PublicRoomJoinClicked: $publicRoom")
viewModel.joinRoom(publicRoom)
}
override fun loadMore() {
viewModel.loadMore()
}
override fun invalidate() = withState(viewModel) { state ->
// Populate list with Epoxy
publicRoomsController.setData(state)
// Directory name
publicRoomsDirectoryName.text = state.roomDirectoryDisplayName
}
}

View File

@ -0,0 +1,223 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsFilter
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent
import org.koin.android.ext.android.get
import timber.log.Timber
private const val PUBLIC_ROOMS_LIMIT = 20
class RoomDirectoryViewModel(initialState: PublicRoomsViewState,
private val session: Session) : VectorViewModel<PublicRoomsViewState>(initialState) {
companion object : MvRxViewModelFactory<RoomDirectoryViewModel, PublicRoomsViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: PublicRoomsViewState): RoomDirectoryViewModel? {
val currentSession = viewModelContext.activity.get<Session>()
return RoomDirectoryViewModel(state, currentSession)
}
}
private val _joinRoomErrorLiveData = MutableLiveData<LiveEvent<Throwable>>()
val joinRoomErrorLiveData: LiveData<LiveEvent<Throwable>>
get() = _joinRoomErrorLiveData
// TODO Store in ViewState?
private var currentFilter: String = ""
private var since: String? = null
private var currentTask: Cancelable? = null
// Default RoomDirectoryData
private var roomDirectoryData = RoomDirectoryData()
init {
// Load with empty filter
load()
setState {
copy(
roomDirectoryDisplayName = roomDirectoryData.displayName
)
}
// Observe joined room (from the sync)
observeJoinedRooms()
}
private fun observeJoinedRooms() {
session
.rx()
.liveRoomSummaries()
.execute { async ->
val joinedRoomIds = async.invoke()
// Keep only joined room
?.filter { it.membership == Membership.JOIN }
?.map { it.roomId }
?.toList()
?: emptyList()
copy(
joinedRoomsIds = joinedRoomIds,
// Remove (newly) joined room id from the joining room list
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) },
// Remove (newly) joined room id from the joining room list in error
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) }
)
}
}
fun setRoomDirectoryData(roomDirectoryData: RoomDirectoryData) {
if (this.roomDirectoryData == roomDirectoryData) {
return
}
this.roomDirectoryData = roomDirectoryData
reset()
load()
}
fun filterWith(filter: String) {
if (currentFilter == filter) {
return
}
currentTask?.cancel()
currentFilter = filter
reset()
load()
}
private fun reset() {
// Reset since token
since = null
setState {
copy(
publicRooms = emptyList(),
asyncPublicRoomsRequest = Loading(),
hasMore = false,
roomDirectoryDisplayName = roomDirectoryData.displayName
)
}
}
fun loadMore() {
if (currentTask == null) {
setState {
copy(
asyncPublicRoomsRequest = Loading()
)
}
load()
}
}
private fun load() {
currentTask = session.getPublicRooms(roomDirectoryData.homeServer,
PublicRoomsParams(
limit = PUBLIC_ROOMS_LIMIT,
filter = PublicRoomsFilter(searchTerm = currentFilter),
includeAllNetworks = roomDirectoryData.includeAllNetworks,
since = since,
thirdPartyInstanceId = roomDirectoryData.thirdPartyInstanceId
),
object : MatrixCallback<PublicRoomsResponse> {
override fun onSuccess(data: PublicRoomsResponse) {
currentTask = null
since = data.nextBatch
setState {
copy(
asyncPublicRoomsRequest = Success(data.chunk!!),
// It's ok to append at the end of the list, so I use publicRooms.size()
publicRooms = publicRooms.appendAt(data.chunk!!, publicRooms.size),
hasMore = since != null
)
}
}
override fun onFailure(failure: Throwable) {
currentTask = null
setState {
copy(
asyncPublicRoomsRequest = Fail(failure)
)
}
}
})
}
fun joinRoom(publicRoom: PublicRoom) = withState { state ->
if (state.joiningRoomsIds.contains(publicRoom.roomId)) {
// Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
setState {
copy(
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { add(publicRoom.roomId) }
)
}
session.joinRoom(publicRoom.roomId, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined
}
override fun onFailure(failure: Throwable) {
// Notify the user
_joinRoomErrorLiveData.postValue(LiveEvent(failure))
setState {
copy(
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { remove(publicRoom.roomId) },
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableList().apply { add(publicRoom.roomId) }
)
}
}
})
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
data class PublicRoomsViewState(
// Store cumul of pagination result
val publicRooms: List<PublicRoom> = emptyList(),
// Current pagination request
val asyncPublicRoomsRequest: Async<List<PublicRoom>> = Uninitialized,
// True if more result are available server side
val hasMore: Boolean = false,
// List of roomIds that the user wants to join
val joiningRoomsIds: List<String> = emptyList(),
// List of roomIds that the user wants to join, but an error occurred
val joiningErrorRoomsIds: List<String> = emptyList(),
// List of joined roomId,
val joinedRoomsIds: List<String> = emptyList(),
val roomDirectoryDisplayName: String? = null
) : MvRxState

View File

@ -0,0 +1,43 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory
import android.os.Bundle
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.addFragment
import im.vector.riotredesign.core.platform.VectorBaseActivity
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
class RoomDirectoryActivity : VectorBaseActivity() {
override fun getLayoutRes() = R.layout.activity_simple
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))
}
override fun initUiAndData() {
if (isFirstCreation()) {
addFragment(PublicRoomsFragment(), R.id.simpleFragmentContainer)
}
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.features.roomdirectory.picker.RoomDirectoryListCreator
import im.vector.riotredesign.features.roomdirectory.picker.RoomDirectoryPickerController
import org.koin.dsl.module.module
// TODO Ganfra: When do we create a new module?
class RoomDirectoryModule {
companion object {
const val ROOM_DIRECTORY_SCOPE = "ROOM_DIRECTORY_SCOPE"
}
val definition = module(override = true) {
scope(ROOM_DIRECTORY_SCOPE) {
RoomDirectoryPickerController(get(), get(), get())
}
scope(ROOM_DIRECTORY_SCOPE) {
RoomDirectoryListCreator(get(), get<Session>().sessionParams.credentials)
}
scope(ROOM_DIRECTORY_SCOPE) {
PublicRoomsController(get(), get())
}
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory.picker
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.setTextOrHide
import im.vector.riotredesign.core.glide.GlideApp
@EpoxyModelClass(layout = R.layout.item_room_directory)
abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>() {
@EpoxyAttribute
var directoryAvatarUrl: String? = null
@EpoxyAttribute
var directoryName: String? = null
@EpoxyAttribute
var directoryDescription: String? = null
@EpoxyAttribute
var includeAllNetworks: Boolean = false
@EpoxyAttribute
var globalListener: (() -> Unit)? = null
override fun bind(holder: Holder) {
holder.rootView.setOnClickListener { globalListener?.invoke() }
// Avatar
GlideApp.with(holder.avatarView)
.load(directoryAvatarUrl)
.apply {
if (!includeAllNetworks) {
placeholder(R.drawable.network_matrix)
}
}
.into(holder.avatarView)
holder.nameView.text = directoryName
holder.descritionView.setTextOrHide(directoryDescription)
}
class Holder : VectorEpoxyHolder() {
val rootView by bind<ViewGroup>(R.id.itemRoomDirectoryLayout)
val avatarView by bind<ImageView>(R.id.itemRoomDirectoryAvatar)
val nameView by bind<TextView>(R.id.itemRoomDirectoryName)
val descritionView by bind<TextView>(R.id.itemRoomDirectoryDescription)
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory.picker
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringArrayProvider
class RoomDirectoryListCreator(private val stringArrayProvider: StringArrayProvider,
private val credentials: Credentials) {
fun computeDirectories(thirdPartyProtocolData: Map<String, ThirdPartyProtocol>): List<RoomDirectoryData> {
val result = ArrayList<RoomDirectoryData>()
// Add user homeserver name
val userHsName = credentials.userId.substring(credentials.userId.indexOf(":") + 1)
result.add(RoomDirectoryData(
displayName = userHsName,
includeAllNetworks = true
))
// Add user's HS but for Matrix public rooms only
result.add(RoomDirectoryData())
// Add custom directory servers
val hsNamesList = stringArrayProvider.getStringArray(R.array.room_directory_servers)
hsNamesList.forEach {
if (it != userHsName) {
// Use the server name as a default display name
result.add(RoomDirectoryData(
displayName = it,
includeAllNetworks = true
))
}
}
// Add result of the request
thirdPartyProtocolData.forEach {
it.value.instances?.forEach { thirdPartyProtocolInstance ->
result.add(RoomDirectoryData(
homeServer = null,
displayName = thirdPartyProtocolInstance.desc ?: "",
thirdPartyInstanceId = thirdPartyProtocolInstance.instanceId,
includeAllNetworks = false,
// Default to protocol icon
avatarUrl = thirdPartyProtocolInstance.icon ?: it.value.icon
))
}
}
return result
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory.picker
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.errorWithRetryItem
import im.vector.riotredesign.core.epoxy.loadingItem
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.resources.StringProvider
class RoomDirectoryPickerController(private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter,
private val roomDirectoryListCreator: RoomDirectoryListCreator
) : TypedEpoxyController<RoomDirectoryPickerViewState>() {
var callback: Callback? = null
var index = 0
override fun buildModels(viewState: RoomDirectoryPickerViewState) {
val asyncThirdPartyProtocol = viewState.asyncThirdPartyRequest
when (asyncThirdPartyProtocol) {
is Success -> {
val directories = roomDirectoryListCreator.computeDirectories(asyncThirdPartyProtocol.invoke())
directories.forEach {
buildDirectory(it)
}
}
is Incomplete -> {
loadingItem {
id("loading")
}
}
is Fail -> {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(asyncThirdPartyProtocol.error))
listener { callback?.retry() }
}
}
}
}
private fun buildDirectory(roomDirectoryData: RoomDirectoryData) {
roomDirectoryItem {
id(index++)
directoryName(roomDirectoryData.displayName)
val description = when {
roomDirectoryData.includeAllNetworks ->
stringProvider.getString(R.string.directory_server_all_rooms_on_server, roomDirectoryData.displayName)
"Matrix" == roomDirectoryData.displayName ->
stringProvider.getString(R.string.directory_server_native_rooms, roomDirectoryData.displayName)
else ->
null
}
directoryDescription(description)
directoryAvatarUrl(roomDirectoryData.avatarUrl)
includeAllNetworks(roomDirectoryData.includeAllNetworks)
globalListener {
callback?.onRoomDirectoryClicked(roomDirectoryData)
}
}
}
interface Callback {
fun onRoomDirectoryClicked(roomDirectory: RoomDirectoryData)
fun retry()
}
}

View File

@ -0,0 +1,105 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory.picker
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule
import im.vector.riotredesign.features.roomdirectory.RoomDirectoryViewModel
import kotlinx.android.synthetic.main.fragment_public_rooms.toolbar
import kotlinx.android.synthetic.main.fragment_room_directory_picker.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
import timber.log.Timber
// TODO Set title to R.string.select_room_directory
// TODO Menu to add custom room directory (not done in RiotWeb so far...)
class RoomDirectoryPickerFragment : VectorBaseFragment(), RoomDirectoryPickerController.Callback {
private val viewModel: RoomDirectoryViewModel by activityViewModel()
private val pickerViewModel: RoomDirectoryPickerViewModel by fragmentViewModel()
private val roomDirectoryPickerController: RoomDirectoryPickerController by inject()
override fun getLayoutResId() = R.layout.fragment_room_directory_picker
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
vectorBaseActivity.setSupportActionBar(toolbar)
vectorBaseActivity.supportActionBar?.let {
it.setDisplayShowHomeEnabled(true)
it.setDisplayHomeAsUpEnabled(true)
}
}
override fun getMenuRes() = R.menu.menu_directory_server_picker
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.action_add_custom_hs) {
// TODO
vectorBaseActivity.notImplemented()
return true
}
return super.onOptionsItemSelected(item)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(RoomDirectoryModule.ROOM_DIRECTORY_SCOPE))
setupRecyclerView()
}
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
roomDirectoryPickerList.layoutManager = layoutManager
roomDirectoryPickerController.callback = this
roomDirectoryPickerList.setController(roomDirectoryPickerController)
}
override fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData) {
Timber.v("onRoomDirectoryClicked: $roomDirectoryData")
viewModel.setRoomDirectoryData(roomDirectoryData)
// TODO Not the best way to manage Fragment Backstack...
vectorBaseActivity.onBackPressed()
}
override fun retry() {
Timber.v("Retry")
pickerViewModel.load()
}
override fun invalidate() = withState(pickerViewModel) { state ->
// Populate list with Epoxy
roomDirectoryPickerController.setData(state)
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory.picker
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
import im.vector.riotredesign.core.platform.VectorViewModel
import org.koin.android.ext.android.get
class RoomDirectoryPickerViewModel(initialState: RoomDirectoryPickerViewState,
private val session: Session) : VectorViewModel<RoomDirectoryPickerViewState>(initialState) {
companion object : MvRxViewModelFactory<RoomDirectoryPickerViewModel, RoomDirectoryPickerViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomDirectoryPickerViewState): RoomDirectoryPickerViewModel? {
val currentSession = viewModelContext.activity.get<Session>()
return RoomDirectoryPickerViewModel(state, currentSession)
}
}
init {
load()
}
fun load() {
session.getThirdPartyProtocol(object : MatrixCallback<Map<String, ThirdPartyProtocol>> {
override fun onSuccess(data: Map<String, ThirdPartyProtocol>) {
setState {
copy(asyncThirdPartyRequest = Success(data))
}
}
override fun onFailure(failure: Throwable) {
setState {
copy(asyncThirdPartyRequest = Fail(failure))
}
}
})
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.roomdirectory.picker
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
data class RoomDirectoryPickerViewState(
val asyncThirdPartyRequest: Async<Map<String, ThirdPartyProtocol>> = Uninitialized
) : MvRxState

View File

@ -0,0 +1,13 @@
<vector android:height="24dp" android:viewportHeight="22"
android:viewportWidth="22" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M11,11m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeColor="#03B381" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.4"/>
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M11,7L11,15" android:strokeColor="#03B381"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="1.4"/>
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M7,11L15,11" android:strokeColor="#03B381"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="1.4"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector android:height="24dp" android:viewportHeight="11"
android:viewportWidth="15" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M5.3033,10.0815L13.7886,1.5962"
android:strokeColor="#7E899C" android:strokeLineCap="round" android:strokeWidth="1.3"/>
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M5.3033,10.0815L1.0607,5.8388"
android:strokeColor="#7E899C" android:strokeLineCap="round" android:strokeWidth="1.3"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector android:height="24dp" android:viewportHeight="16"
android:viewportWidth="15" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M14,15L14,13.4444C14,11.7262 12.5449,10.3333 10.75,10.3333L4.25,10.3333C2.4551,10.3333 1,11.7262 1,13.4444L1,15"
android:strokeColor="#7E899C" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.16666667"/>
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M4.25,4.1111a3.25,3.1111 0,1 0,6.5 0a3.25,3.1111 0,1 0,-6.5 0z"
android:strokeColor="#7E899C" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.16666667"/>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/simpleFragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/publicRoomsCoordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/pale_grey">
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/publicRoomsList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_public_room" />
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:expandedTitleGravity="top"
app:layout_scrollFlags="scroll|enterAlways|exitUntilCollapsed|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/publicRoomsFilter"
android:layout_width="match_parent"
android:layout_height="32dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_search_edit_text"
android:drawableStart="@drawable/ic_search_white"
android:drawableLeft="@drawable/ic_search_white"
android:drawablePadding="8dp"
android:drawableTint="#9fa9ba"
android:hint="@string/home_filter_placeholder_rooms"
android:lines="1"
android:paddingLeft="8dp"
android:paddingRight="8dp" />
</androidx.appcompat.widget.Toolbar>
<Button
android:id="@+id/publicRoomsCreateNewRoom"
style="@style/VectorButtonStyleFlat"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:drawableStart="@drawable/ic_plus_circle"
android:drawableLeft="@drawable/ic_plus_circle"
android:drawablePadding="13dp"
android:text="@string/create_new_room" />
<!-- TODO Layout with Nad -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/publicRoomsDirectoryName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:textColor="#FFFFFF"
tools:text="RoomDirectoryName" />
<Button
android:id="@+id/publicRoomsChangeDirectory"
style="@style/VectorButtonStyleFlat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:text="@string/action_change" />
</RelativeLayout>
</LinearLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -102,6 +102,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
app:layout_constraintVertical_bias="1.0"
tools:visibility="gone" />
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/pale_grey">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/roomDirectoryPickerList"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
tools:listitem="@layout/item_room_directory" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/itemErrorRetryText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Error" />
<Button
android:id="@+id/itemErrorRetryButton"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/global_retry"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/itemErrorRetryText" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/itemNoResultText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="16dp"
android:text="@string/no_result_placeholder" />

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemPublicRoomLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_room_item"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<ImageView
android:id="@+id/itemPublicRoomAvatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toTopOf="@+id/itemPublicRoomBottomSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/itemPublicRoomName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="#2E2F3E"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/itemPublicRoomBottomSeparator"
app:layout_constraintEnd_toStartOf="@id/itemPublicRoomMembersCount"
app:layout_constraintStart_toEndOf="@id/itemPublicRoomAvatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/itemPublicRoomMembersCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:drawableStart="@drawable/ic_user"
android:drawableLeft="@drawable/ic_user"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:minWidth="40dp"
android:textColor="#7E899C"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/itemPublicRoomBottomSeparator"
app:layout_constraintEnd_toStartOf="@id/itemPublicRoomButtonState"
app:layout_constraintStart_toEndOf="@id/itemPublicRoomName"
app:layout_constraintTop_toTopOf="parent"
tools:text="148" />
<im.vector.riotredesign.core.platform.ButtonStateView
android:id="@+id/itemPublicRoomButtonState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
app:bsv_button_text="@string/join"
app:bsv_loaded_image_src="@drawable/ic_tick"
app:layout_constraintBottom_toTopOf="@+id/itemPublicRoomBottomSeparator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/itemPublicRoomBottomSeparator"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="#E9EDF1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoomDirectoryLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_room_item"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<ImageView
android:id="@+id/itemRoomDirectoryAvatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toTopOf="@+id/itemRoomDirectoryBottomSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/itemRoomDirectoryName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:lines="1"
android:maxLines="2"
android:textColor="#2E2F3E"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/itemRoomDirectoryDescription"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/itemRoomDirectoryAvatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/itemRoomDirectoryDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:lines="1"
android:maxLines="2"
android:textColor="#2E2F3E"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/itemRoomDirectoryBottomSeparator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/itemRoomDirectoryAvatar"
app:layout_constraintTop_toBottomOf="@id/itemRoomDirectoryName"
tools:text="@string/directory_server_all_rooms_on_server" />
<View
android:id="@+id/itemRoomDirectoryBottomSeparator"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="#E9EDF1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:parentTag="android.widget.FrameLayout">
<Button
android:id="@+id/buttonStateButton"
style="@style/VectorButtonStyleFlat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:layout_constraintBottom_toTopOf="@+id/itemPublicRoomBottomSeparator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/join" />
<ProgressBar
android:id="@+id/buttonStateLoading"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:scaleType="center" />
<ImageView
android:id="@+id/buttonStateLoaded"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:src="@drawable/ic_tick" />
<Button
android:id="@+id/buttonStateRetry"
style="@style/VectorButtonStyleFlat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/global_retry"
android:textColor="@color/vector_warning_color" />
</merge>

View File

@ -4,7 +4,7 @@
<item
android:id="@+id/action_add_custom_hs"
android:icon="@drawable/ic_add_white"
android:icon="@drawable/ic_add_black"
android:title="@string/action_open"
app:showAsAction="always" />

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ButtonStateView">
<attr name="bsv_loaded_image_src" format="reference" />
<attr name="bsv_button_text" format="reference|string" />
</declare-styleable>
</resources>

View File

@ -19,5 +19,8 @@
<string name="malformed_message">Malformed event, cannot display</string>
<string name="create_new_room">Create New Room</string>
<string name="error_no_network">No network. Please check your Internet connection.</string>
<string name="action_change">"Change"</string>
</resources>