mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 12:00:03 +03:00
Merge pull request #615 from vector-im/feature/report_content
Add ability to report content
This commit is contained in:
commit
9d7c4abb97
52 changed files with 1413 additions and 967 deletions
|
@ -9,6 +9,7 @@ Improvements:
|
|||
- Do not upload file too big for the homeserver (#587)
|
||||
- Handle read markers (#84)
|
||||
- Mark all messages as read (#396)
|
||||
- Add ability to report content (#515)
|
||||
|
||||
Other changes:
|
||||
- Accessibility improvements to read receipts in the room timeline and reactions emoji chooser
|
||||
|
|
|
@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService
|
|||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||
import im.vector.matrix.android.api.session.room.reporting.ReportingService
|
||||
import im.vector.matrix.android.api.session.room.read.ReadService
|
||||
import im.vector.matrix.android.api.session.room.send.DraftService
|
||||
import im.vector.matrix.android.api.session.room.send.SendService
|
||||
|
@ -38,6 +39,7 @@ interface Room :
|
|||
ReadService,
|
||||
MembershipService,
|
||||
StateService,
|
||||
ReportingService,
|
||||
RelationService,
|
||||
RoomCryptoService {
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.matrix.android.api.session.room.reporting
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to report content of an event.
|
||||
*/
|
||||
interface ReportingService {
|
||||
|
||||
/**
|
||||
* Report content
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid
|
||||
*/
|
||||
fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
}
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.Room
|
|||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||
import im.vector.matrix.android.api.session.room.reporting.ReportingService
|
||||
import im.vector.matrix.android.api.session.room.read.ReadService
|
||||
import im.vector.matrix.android.api.session.room.send.DraftService
|
||||
import im.vector.matrix.android.api.session.room.send.SendService
|
||||
|
@ -44,18 +45,20 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
|||
private val sendService: SendService,
|
||||
private val draftService: DraftService,
|
||||
private val stateService: StateService,
|
||||
private val reportingService: ReportingService,
|
||||
private val readService: ReadService,
|
||||
private val cryptoService: CryptoService,
|
||||
private val relationService: RelationService,
|
||||
private val roomMembersService: MembershipService
|
||||
) : Room,
|
||||
TimelineService by timelineService,
|
||||
SendService by sendService,
|
||||
DraftService by draftService,
|
||||
StateService by stateService,
|
||||
ReadService by readService,
|
||||
RelationService by relationService,
|
||||
MembershipService by roomMembersService {
|
||||
private val roomMembersService: MembershipService) :
|
||||
Room,
|
||||
TimelineService by timelineService,
|
||||
SendService by sendService,
|
||||
DraftService by draftService,
|
||||
StateService by stateService,
|
||||
ReportingService by reportingService,
|
||||
ReadService by readService,
|
||||
RelationService by relationService,
|
||||
MembershipService by roomMembersService {
|
||||
|
||||
override fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>> {
|
||||
val liveData = monarchy.findAllMappedWithChanges(
|
||||
|
|
|
@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.network.NetworkConstants
|
|||
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
|
||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
|
||||
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
|
||||
import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
|
||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
|
||||
|
@ -245,4 +246,16 @@ internal interface RoomAPI {
|
|||
@Path("eventId") parent_id: String,
|
||||
@Body reason: Map<String, String>
|
||||
): Call<SendResponse>
|
||||
|
||||
/**
|
||||
* Reports an event as inappropriate to the server, which may then notify the appropriate people.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @param eventId the event to report content
|
||||
* @param body body containing score and reason
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}")
|
||||
fun reportContent(@Path("roomId") roomId: String,
|
||||
@Path("eventId") eventId: String,
|
||||
@Body body: ReportContentBody): Call<Unit>
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
|
|||
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
|
||||
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
|
||||
import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService
|
||||
import im.vector.matrix.android.internal.session.room.reporting.DefaultReportingService
|
||||
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
|
||||
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
|
||||
|
@ -40,6 +41,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
|||
private val sendServiceFactory: DefaultSendService.Factory,
|
||||
private val draftServiceFactory: DefaultDraftService.Factory,
|
||||
private val stateServiceFactory: DefaultStateService.Factory,
|
||||
private val reportingServiceFactory: DefaultReportingService.Factory,
|
||||
private val readServiceFactory: DefaultReadService.Factory,
|
||||
private val relationServiceFactory: DefaultRelationService.Factory,
|
||||
private val membershipServiceFactory: DefaultMembershipService.Factory) :
|
||||
|
@ -54,6 +56,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
|||
sendServiceFactory.create(roomId),
|
||||
draftServiceFactory.create(roomId),
|
||||
stateServiceFactory.create(roomId),
|
||||
reportingServiceFactory.create(roomId),
|
||||
readServiceFactory.create(roomId),
|
||||
cryptoService,
|
||||
relationServiceFactory.create(roomId),
|
||||
|
|
|
@ -45,6 +45,8 @@ import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkers
|
|||
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
|
||||
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
||||
import im.vector.matrix.android.internal.session.room.relation.*
|
||||
import im.vector.matrix.android.internal.session.room.reporting.DefaultReportContentTask
|
||||
import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask
|
||||
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
|
||||
import im.vector.matrix.android.internal.session.room.state.SendStateTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.*
|
||||
|
@ -114,6 +116,9 @@ internal abstract class RoomModule {
|
|||
@Binds
|
||||
abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindReportContentTask(reportContentTask: DefaultReportContentTask): ReportContentTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask
|
||||
|
||||
|
|
|
@ -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.matrix.android.internal.session.room.reporting
|
||||
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.reporting.ReportingService
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
|
||||
internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val reportContentTask: ReportContentTask
|
||||
) : ReportingService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(roomId: String): ReportingService
|
||||
}
|
||||
|
||||
override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = ReportContentTask.Params(roomId, eventId, score, reason)
|
||||
|
||||
return reportContentTask
|
||||
.configureWith(params) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
|
@ -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.matrix.android.internal.session.room.reporting
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class ReportContentBody(
|
||||
/**
|
||||
* Required. The score to rate this content as where -100 is most offensive and 0 is inoffensive.
|
||||
*/
|
||||
@Json(name = "score") val score: Int,
|
||||
|
||||
/**
|
||||
* Required. The reason the content is being reported. May be blank.
|
||||
*/
|
||||
@Json(name = "reason") val reason: String
|
||||
)
|
|
@ -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.matrix.android.internal.session.room.reporting
|
||||
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface ReportContentTask : Task<ReportContentTask.Params, Unit> {
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val eventId: String,
|
||||
val score: Int,
|
||||
val reason: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultReportContentTask @Inject constructor(private val roomAPI: RoomAPI) : ReportContentTask {
|
||||
override suspend fun execute(params: ReportContentTask.Params) {
|
||||
return executeRequest {
|
||||
apiCall = roomAPI.reportContent(params.roomId, params.eventId, ReportContentBody(params.score, params.reason))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,6 +42,8 @@ import im.vector.riotx.features.home.group.GroupListFragment
|
|||
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
|
||||
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.*
|
||||
import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
|
||||
import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
|
||||
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
|
||||
import im.vector.riotx.features.home.room.list.RoomListFragment
|
||||
import im.vector.riotx.features.invite.VectorInviteView
|
||||
|
@ -103,12 +105,10 @@ interface ScreenComponent {
|
|||
|
||||
fun inject(messageActionsBottomSheet: MessageActionsBottomSheet)
|
||||
|
||||
fun inject(viewReactionBottomSheet: ViewReactionBottomSheet)
|
||||
fun inject(viewReactionsBottomSheet: ViewReactionsBottomSheet)
|
||||
|
||||
fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
|
||||
|
||||
fun inject(messageMenuFragment: MessageMenuFragment)
|
||||
|
||||
fun inject(vectorSettingsActivity: VectorSettingsActivity)
|
||||
|
||||
fun inject(createRoomFragment: CreateRoomFragment)
|
||||
|
@ -135,8 +135,6 @@ interface ScreenComponent {
|
|||
|
||||
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
|
||||
|
||||
fun inject(quickReactionFragment: QuickReactionFragment)
|
||||
|
||||
fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity)
|
||||
|
||||
fun inject(loginActivity: LoginActivity)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.riotx.core.dialogs
|
||||
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import im.vector.riotx.R
|
||||
|
||||
fun AlertDialog.withColoredButton(whichButton: Int, @ColorRes color: Int = R.color.vector_error_color): AlertDialog {
|
||||
getButton(whichButton)?.setTextColor(ContextCompat.getColor(context, color))
|
||||
return this
|
||||
}
|
|
@ -13,18 +13,23 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||
package im.vector.riotx.core.platform
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.CallSuper
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.MvRxView
|
||||
import com.airbnb.mvrx.MvRxViewModelStore
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import im.vector.riotx.core.di.DaggerScreenComponent
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
|
@ -37,10 +42,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
|
|||
private lateinit var screenComponent: ScreenComponent
|
||||
final override val mvrxViewId: String by lazy { mvrxPersistedViewId }
|
||||
|
||||
private var bottomSheetBehavior: BottomSheetBehavior<FrameLayout>? = null
|
||||
|
||||
val vectorBaseActivity: VectorBaseActivity by lazy {
|
||||
activity as VectorBaseActivity
|
||||
}
|
||||
|
||||
open val showExpanded = false
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
|
||||
super.onAttach(context)
|
||||
|
@ -57,6 +66,17 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
|
|||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return super.onCreateDialog(savedInstanceState).apply {
|
||||
val dialog = this as? BottomSheetDialog
|
||||
bottomSheetBehavior = dialog?.behavior
|
||||
bottomSheetBehavior?.setPeekHeight(DimensionConverter(resources).dpToPx(400), false)
|
||||
if (showExpanded) {
|
||||
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
mvrxViewModelStore.saveViewModels(outState)
|
||||
|
@ -70,6 +90,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
|
|||
postInvalidate()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun invalidate() {
|
||||
if (showExpanded) {
|
||||
// Force the bottom sheet to be expanded
|
||||
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
|
||||
protected fun setArguments(args: Parcelable? = null) {
|
||||
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
|
||||
}
|
|
@ -49,6 +49,9 @@ sealed class RoomDetailActions {
|
|||
|
||||
data class ResendMessage(val eventId: String) : RoomDetailActions()
|
||||
data class RemoveFailedEcho(val eventId: String) : RoomDetailActions()
|
||||
|
||||
data class ReportContent(val eventId: String, val reason: String, val spam: Boolean = false, val inappropriate: Boolean = false) : RoomDetailActions()
|
||||
|
||||
object ClearSendQueue : RoomDetailActions()
|
||||
object ResendAll : RoomDetailActions()
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
|
@ -50,6 +51,7 @@ import com.airbnb.mvrx.*
|
|||
import com.github.piasy.biv.BigImageViewer
|
||||
import com.github.piasy.biv.loader.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.jaiselrahman.filepicker.activity.FilePickerActivity
|
||||
import com.jaiselrahman.filepicker.config.Configurations
|
||||
import com.jaiselrahman.filepicker.model.MediaFile
|
||||
|
@ -68,6 +70,7 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
|||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.dialogs.withColoredButton
|
||||
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.extensions.hideKeyboard
|
||||
|
@ -93,8 +96,12 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
|
|||
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
|
||||
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.*
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction
|
||||
import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
||||
import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
|
||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||
import im.vector.riotx.features.html.PillImageSpan
|
||||
import im.vector.riotx.features.invite.VectorInviteView
|
||||
|
@ -264,6 +271,10 @@ class RoomDetailFragment :
|
|||
roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState ->
|
||||
syncStateView.render(syncState)
|
||||
}
|
||||
|
||||
roomDetailViewModel.requestLiveData.observeEvent(this) {
|
||||
displayRoomDetailActionResult(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -766,13 +777,77 @@ class RoomDetailFragment :
|
|||
}
|
||||
|
||||
private fun displayCommandError(message: String) {
|
||||
AlertDialog.Builder(activity!!)
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.command_error)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun promptReasonToReportContent(action: SimpleAction.ReportContentCustom) {
|
||||
val inflater = requireActivity().layoutInflater
|
||||
val layout = inflater.inflate(R.layout.dialog_report_content, null)
|
||||
|
||||
val input = layout.findViewById<TextInputEditText>(R.id.dialog_report_content_input)
|
||||
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.report_content_custom_title)
|
||||
.setView(layout)
|
||||
.setPositiveButton(R.string.report_content_custom_submit) { _, _ ->
|
||||
val reason = input.text.toString()
|
||||
roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, reason))
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun displayRoomDetailActionResult(result: Async<RoomDetailActions>) {
|
||||
when (result) {
|
||||
is Fail -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(errorFormatter.toHumanReadable(result.error))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
is Success -> {
|
||||
when (val data = result.invoke()) {
|
||||
is RoomDetailActions.ReportContent -> {
|
||||
when {
|
||||
data.spam -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.content_reported_as_spam_title)
|
||||
.setMessage(R.string.content_reported_as_spam_content)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
|
||||
.show()
|
||||
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
}
|
||||
data.inappropriate -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.content_reported_as_inappropriate_title)
|
||||
.setMessage(R.string.content_reported_as_inappropriate_content)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
|
||||
.show()
|
||||
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
}
|
||||
else -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.content_reported_title)
|
||||
.setMessage(R.string.content_reported_content)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
|
||||
.show()
|
||||
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
|
||||
override fun onUrlClicked(url: String): Boolean {
|
||||
|
@ -917,7 +992,7 @@ class RoomDetailFragment :
|
|||
}
|
||||
|
||||
override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) {
|
||||
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
|
||||
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
|
||||
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
||||
}
|
||||
|
||||
|
@ -966,23 +1041,23 @@ class RoomDetailFragment :
|
|||
|
||||
private fun handleActions(action: SimpleAction) {
|
||||
when (action) {
|
||||
is SimpleAction.AddReaction -> {
|
||||
is SimpleAction.AddReaction -> {
|
||||
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
|
||||
}
|
||||
is SimpleAction.ViewReactions -> {
|
||||
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
|
||||
is SimpleAction.ViewReactions -> {
|
||||
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
|
||||
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
||||
}
|
||||
is SimpleAction.Copy -> {
|
||||
is SimpleAction.Copy -> {
|
||||
// I need info about the current selected message :/
|
||||
copyToClipboard(requireContext(), action.content, false)
|
||||
val msg = requireContext().getString(R.string.copied_to_clipboard)
|
||||
showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
|
||||
}
|
||||
is SimpleAction.Delete -> {
|
||||
is SimpleAction.Delete -> {
|
||||
roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason)))
|
||||
}
|
||||
is SimpleAction.Share -> {
|
||||
is SimpleAction.Share -> {
|
||||
// TODO current data communication is too limited
|
||||
// Need to now the media type
|
||||
// TODO bad, just POC
|
||||
|
@ -1010,10 +1085,10 @@ class RoomDetailFragment :
|
|||
}
|
||||
)
|
||||
}
|
||||
is SimpleAction.ViewEditHistory -> {
|
||||
is SimpleAction.ViewEditHistory -> {
|
||||
onEditedDecorationClicked(action.messageInformationData)
|
||||
}
|
||||
is SimpleAction.ViewSource -> {
|
||||
is SimpleAction.ViewSource -> {
|
||||
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
|
||||
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
|
||||
it.text = action.content
|
||||
|
@ -1024,7 +1099,7 @@ class RoomDetailFragment :
|
|||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
is SimpleAction.ViewDecryptedSource -> {
|
||||
is SimpleAction.ViewDecryptedSource -> {
|
||||
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
|
||||
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
|
||||
it.text = action.content
|
||||
|
@ -1035,31 +1110,40 @@ class RoomDetailFragment :
|
|||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
is SimpleAction.QuickReact -> {
|
||||
is SimpleAction.QuickReact -> {
|
||||
// eventId,ClickedOn,Add
|
||||
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
|
||||
}
|
||||
is SimpleAction.Edit -> {
|
||||
is SimpleAction.Edit -> {
|
||||
roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString()))
|
||||
}
|
||||
is SimpleAction.Quote -> {
|
||||
is SimpleAction.Quote -> {
|
||||
roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString()))
|
||||
}
|
||||
is SimpleAction.Reply -> {
|
||||
is SimpleAction.Reply -> {
|
||||
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString()))
|
||||
}
|
||||
is SimpleAction.CopyPermalink -> {
|
||||
is SimpleAction.CopyPermalink -> {
|
||||
val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
|
||||
copyToClipboard(requireContext(), permalink, false)
|
||||
showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
|
||||
}
|
||||
is SimpleAction.Resend -> {
|
||||
is SimpleAction.Resend -> {
|
||||
roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId))
|
||||
}
|
||||
is SimpleAction.Remove -> {
|
||||
is SimpleAction.Remove -> {
|
||||
roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId))
|
||||
}
|
||||
else -> {
|
||||
is SimpleAction.ReportContentSpam -> {
|
||||
roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is spam", spam = true))
|
||||
}
|
||||
is SimpleAction.ReportContentInappropriate -> {
|
||||
roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is inappropriate", inappropriate = true))
|
||||
}
|
||||
is SimpleAction.ReportContentCustom -> {
|
||||
promptReasonToReportContent(action)
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,10 +21,7 @@ import android.text.TextUtils
|
|||
import androidx.annotation.IdRes
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.airbnb.mvrx.*
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
|
@ -97,6 +94,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
|
||||
private var timeline = room.createTimeline(eventId, timelineSettings)
|
||||
|
||||
// Can be used for several actions, for a one shot result
|
||||
private val _requestLiveData = MutableLiveData<LiveEvent<Async<RoomDetailActions>>>()
|
||||
val requestLiveData: LiveData<LiveEvent<Async<RoomDetailActions>>>
|
||||
get() = _requestLiveData
|
||||
|
||||
// Slot to keep a pending action during permission request
|
||||
var pendingAction: RoomDetailActions? = null
|
||||
|
||||
|
@ -155,6 +157,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
is RoomDetailActions.ResendAll -> handleResendAll()
|
||||
is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action)
|
||||
is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead()
|
||||
is RoomDetailActions.ReportContent -> handleReportContent(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -708,6 +711,18 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
room.markAllAsRead(object : MatrixCallback<Any> {})
|
||||
}
|
||||
|
||||
private fun handleReportContent(action: RoomDetailActions.ReportContent) {
|
||||
room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
_requestLiveData.postValue(LiveEvent(Success(action)))
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
_requestLiveData.postValue(LiveEvent(Fail(failure)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun observeSyncState() {
|
||||
session.rx()
|
||||
.liveSyncState()
|
||||
|
|
|
@ -21,19 +21,18 @@ import android.os.Parcelable
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import com.airbnb.epoxy.EpoxyRecyclerView
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.args
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
|
@ -48,8 +47,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
|
||||
@Inject lateinit var epoxyController: DisplayReadReceiptsController
|
||||
|
||||
@BindView(R.id.bottom_sheet_display_reactions_list)
|
||||
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
||||
@BindView(R.id.bottomSheetRecyclerView)
|
||||
lateinit var recyclerView: RecyclerView
|
||||
|
||||
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
|
||||
|
||||
|
@ -58,24 +57,20 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
|
||||
val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
|
||||
ButterKnife.bind(this, view)
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
epoxyRecyclerView.setController(epoxyController)
|
||||
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
|
||||
LinearLayout.VERTICAL)
|
||||
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
recyclerView.adapter = epoxyController.adapter
|
||||
bottomSheetTitle.text = getString(R.string.read_at)
|
||||
epoxyController.setData(displayReadReceiptArgs.readReceipts)
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
// we are not using state for this one as it's static
|
||||
}
|
||||
// we are not using state for this one as it's static, so no need to override invalidate()
|
||||
|
||||
companion object {
|
||||
fun newInstance(readReceipts: List<ReadReceiptData>): DisplayReadReceiptsBottomSheet {
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
|
||||
/**
|
||||
* A action for bottom sheet.
|
||||
*/
|
||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_action)
|
||||
abstract class BottomSheetItemAction : VectorEpoxyModel<BottomSheetItemAction.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
@DrawableRes
|
||||
var iconRes: Int = 0
|
||||
@EpoxyAttribute
|
||||
var textRes: Int = 0
|
||||
@EpoxyAttribute
|
||||
var showExpand = false
|
||||
@EpoxyAttribute
|
||||
var expanded = false
|
||||
@EpoxyAttribute
|
||||
var subMenuItem = false
|
||||
@EpoxyAttribute
|
||||
lateinit var listener: View.OnClickListener
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.view.setOnClickListener {
|
||||
listener.onClick(it)
|
||||
}
|
||||
|
||||
holder.startSpace.isVisible = subMenuItem
|
||||
holder.icon.setImageResource(iconRes)
|
||||
holder.text.setText(textRes)
|
||||
holder.expand.isVisible = showExpand
|
||||
if (showExpand) {
|
||||
holder.expand.setImageResource(if (expanded) R.drawable.ic_material_expand_less_black else R.drawable.ic_material_expand_more_black)
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val startSpace by bind<View>(R.id.action_start_space)
|
||||
val icon by bind<ImageView>(R.id.action_icon)
|
||||
val text by bind<TextView>(R.id.action_title)
|
||||
val expand by bind<ImageView>(R.id.action_expand)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.core.extensions.setTextOrHide
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
|
||||
/**
|
||||
* A message preview for bottom sheet.
|
||||
*/
|
||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview)
|
||||
abstract class BottomSheetItemMessagePreview : VectorEpoxyModel<BottomSheetItemMessagePreview.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
||||
@EpoxyAttribute
|
||||
lateinit var informationData: MessageInformationData
|
||||
@EpoxyAttribute
|
||||
var senderName: String? = null
|
||||
@EpoxyAttribute
|
||||
lateinit var body: CharSequence
|
||||
@EpoxyAttribute
|
||||
var time: CharSequence? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
avatarRenderer.render(informationData.avatarUrl, informationData.senderId, senderName, holder.avatar)
|
||||
holder.sender.setTextOrHide(senderName)
|
||||
holder.body.text = body
|
||||
holder.timestamp.setTextOrHide(time)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val avatar by bind<ImageView>(R.id.bottom_sheet_message_preview_avatar)
|
||||
val sender by bind<TextView>(R.id.bottom_sheet_message_preview_sender)
|
||||
val body by bind<TextView>(R.id.bottom_sheet_message_preview_body)
|
||||
val timestamp by bind<TextView>(R.id.bottom_sheet_message_preview_timestamp)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.EmojiCompatFontProvider
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
|
||||
/**
|
||||
* A quick reaction list for bottom sheet.
|
||||
*/
|
||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_quick_reaction)
|
||||
abstract class BottomSheetItemQuickReactions : VectorEpoxyModel<BottomSheetItemQuickReactions.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var fontProvider: EmojiCompatFontProvider
|
||||
@EpoxyAttribute
|
||||
lateinit var texts: List<String>
|
||||
@EpoxyAttribute
|
||||
lateinit var selecteds: List<Boolean>
|
||||
@EpoxyAttribute
|
||||
var listener: Listener? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.textViews.forEachIndexed { index, textView ->
|
||||
textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT
|
||||
textView.text = texts[index]
|
||||
textView.alpha = if (selecteds[index]) 0.2f else 1f
|
||||
|
||||
textView.setOnClickListener {
|
||||
listener?.didSelect(texts[index], !selecteds[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
private val quickReaction0 by bind<TextView>(R.id.quickReaction0)
|
||||
private val quickReaction1 by bind<TextView>(R.id.quickReaction1)
|
||||
private val quickReaction2 by bind<TextView>(R.id.quickReaction2)
|
||||
private val quickReaction3 by bind<TextView>(R.id.quickReaction3)
|
||||
private val quickReaction4 by bind<TextView>(R.id.quickReaction4)
|
||||
private val quickReaction5 by bind<TextView>(R.id.quickReaction5)
|
||||
private val quickReaction6 by bind<TextView>(R.id.quickReaction6)
|
||||
private val quickReaction7 by bind<TextView>(R.id.quickReaction7)
|
||||
|
||||
val textViews
|
||||
get() = listOf(
|
||||
quickReaction0,
|
||||
quickReaction1,
|
||||
quickReaction2,
|
||||
quickReaction3,
|
||||
quickReaction4,
|
||||
quickReaction5,
|
||||
quickReaction6,
|
||||
quickReaction7
|
||||
)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun didSelect(emoji: String, selected: Boolean)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
|
||||
/**
|
||||
* A send state for bottom sheet.
|
||||
*/
|
||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status)
|
||||
abstract class BottomSheetItemSendState : VectorEpoxyModel<BottomSheetItemSendState.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var showProgress: Boolean = false
|
||||
@EpoxyAttribute
|
||||
lateinit var text: CharSequence
|
||||
@EpoxyAttribute
|
||||
@DrawableRes
|
||||
var drawableStart: Int = 0
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.progress.isVisible = showProgress
|
||||
holder.text.setCompoundDrawablesWithIntrinsicBounds(drawableStart, 0, 0, 0)
|
||||
holder.text.text = text
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val progress by bind<View>(R.id.messageStatusProgress)
|
||||
val text by bind<TextView>(R.id.messageStatusText)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider)
|
||||
abstract class BottomSheetItemSeparator : VectorEpoxyModel<BottomSheetItemSeparator.Holder>() {
|
||||
|
||||
class Holder : VectorEpoxyHolder()
|
||||
}
|
|
@ -15,62 +15,46 @@
|
|||
*/
|
||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_message_actions.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Bottom sheet fragment that shows a message preview with list of contextual actions
|
||||
* (Includes fragments for quick reactions and list of actions)
|
||||
*/
|
||||
class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), MessageActionsEpoxyController.MessageActionsEpoxyControllerListener {
|
||||
|
||||
@Inject lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory
|
||||
@Inject lateinit var messageActionsEpoxyController: MessageActionsEpoxyController
|
||||
|
||||
@BindView(R.id.bottomSheetRecyclerView)
|
||||
lateinit var recyclerView: RecyclerView
|
||||
|
||||
@Inject
|
||||
lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory
|
||||
@Inject
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
||||
private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)
|
||||
|
||||
override val showExpanded = true
|
||||
|
||||
private lateinit var actionHandlerModel: ActionsHandler
|
||||
|
||||
@BindView(R.id.bottom_sheet_message_preview_avatar)
|
||||
lateinit var senderAvatarImageView: ImageView
|
||||
|
||||
@BindView(R.id.bottom_sheet_message_preview_sender)
|
||||
lateinit var senderNameTextView: TextView
|
||||
|
||||
@BindView(R.id.bottom_sheet_message_preview_timestamp)
|
||||
lateinit var messageTimestampText: TextView
|
||||
|
||||
@BindView(R.id.bottom_sheet_message_preview_body)
|
||||
lateinit var messageBodyTextView: TextView
|
||||
|
||||
override fun injectWith(screenComponent: ScreenComponent) {
|
||||
screenComponent.inject(this)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.bottom_sheet_message_actions, container, false)
|
||||
val view = inflater.inflate(R.layout.bottom_sheet_generic_list, container, false)
|
||||
ButterKnife.bind(this, view)
|
||||
return view
|
||||
}
|
||||
|
@ -78,78 +62,26 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
||||
|
||||
val cfm = childFragmentManager
|
||||
var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment
|
||||
if (menuActionFragment == null) {
|
||||
menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
|
||||
cfm.beginTransaction()
|
||||
.replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment")
|
||||
.commit()
|
||||
}
|
||||
menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener {
|
||||
override fun didSelectMenuAction(simpleAction: SimpleAction) {
|
||||
actionHandlerModel.fireAction(simpleAction)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
|
||||
if (quickReactionFragment == null) {
|
||||
quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
|
||||
cfm.beginTransaction()
|
||||
.replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
|
||||
.commit()
|
||||
}
|
||||
quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {
|
||||
override fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String) {
|
||||
actionHandlerModel.fireAction(SimpleAction.QuickReact(eventId, clickedOn, add))
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
|
||||
recyclerView.adapter = messageActionsEpoxyController.adapter
|
||||
// Disable item animation
|
||||
recyclerView.itemAnimator = null
|
||||
messageActionsEpoxyController.listener = this
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
// We want to force the bottom sheet initial state to expanded
|
||||
(dialog as? BottomSheetDialog)?.let { bottomSheetDialog ->
|
||||
bottomSheetDialog.setOnShowListener { dialog ->
|
||||
val d = dialog as BottomSheetDialog
|
||||
(d.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) as? FrameLayout)?.let {
|
||||
BottomSheetBehavior.from(it).state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
}
|
||||
override fun didSelectMenuAction(simpleAction: SimpleAction) {
|
||||
if (simpleAction is SimpleAction.ReportContent) {
|
||||
// Toggle report menu
|
||||
viewModel.toggleReportMenu()
|
||||
} else {
|
||||
actionHandlerModel.fireAction(simpleAction)
|
||||
dismiss()
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
val body = viewModel.resolveBody(it)
|
||||
if (body != null) {
|
||||
bottom_sheet_message_preview.isVisible = true
|
||||
senderNameTextView.text = it.senderName()
|
||||
messageBodyTextView.text = body
|
||||
messageTimestampText.text = it.time()
|
||||
avatarRenderer.render(it.informationData.avatarUrl, it.informationData.senderId, it.senderName(), senderAvatarImageView)
|
||||
} else {
|
||||
bottom_sheet_message_preview.isVisible = false
|
||||
}
|
||||
quickReactBottomDivider.isVisible = it.canReact()
|
||||
bottom_sheet_quick_reaction_container.isVisible = it.canReact()
|
||||
if (it.informationData.sendState.isSending()) {
|
||||
messageStatusInfo.isVisible = true
|
||||
messageStatusProgress.isVisible = true
|
||||
messageStatusText.text = getString(R.string.event_status_sending_message)
|
||||
messageStatusText.setCompoundDrawables(null, null, null, null)
|
||||
} else if (it.informationData.sendState.hasFailed()) {
|
||||
messageStatusInfo.isVisible = true
|
||||
messageStatusProgress.isVisible = false
|
||||
messageStatusText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_warning_small, 0, 0, 0)
|
||||
messageStatusText.text = getString(R.string.unable_to_send_message)
|
||||
} else {
|
||||
messageStatusInfo.isVisible = false
|
||||
}
|
||||
return@withState
|
||||
messageActionsEpoxyController.setData(it)
|
||||
super.invalidate()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import android.view.View
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import com.airbnb.mvrx.Success
|
||||
import im.vector.riotx.EmojiCompatFontProvider
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Epoxy controller for message action list
|
||||
*/
|
||||
class MessageActionsEpoxyController @Inject constructor(private val stringProvider: StringProvider,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val fontProvider: EmojiCompatFontProvider) : TypedEpoxyController<MessageActionState>() {
|
||||
|
||||
var listener: MessageActionsEpoxyControllerListener? = null
|
||||
|
||||
override fun buildModels(state: MessageActionState) {
|
||||
// Message preview
|
||||
val body = state.messageBody
|
||||
if (body != null) {
|
||||
bottomSheetItemMessagePreview {
|
||||
id("preview")
|
||||
avatarRenderer(avatarRenderer)
|
||||
informationData(state.informationData)
|
||||
senderName(state.senderName())
|
||||
body(body)
|
||||
time(state.time())
|
||||
}
|
||||
}
|
||||
|
||||
// Send state
|
||||
if (state.informationData.sendState.isSending()) {
|
||||
bottomSheetItemSendState {
|
||||
id("send_state")
|
||||
showProgress(true)
|
||||
text(stringProvider.getString(R.string.event_status_sending_message))
|
||||
}
|
||||
} else if (state.informationData.sendState.hasFailed()) {
|
||||
bottomSheetItemSendState {
|
||||
id("send_state")
|
||||
showProgress(false)
|
||||
text(stringProvider.getString(R.string.unable_to_send_message))
|
||||
drawableStart(R.drawable.ic_warning_small)
|
||||
}
|
||||
}
|
||||
|
||||
// Quick reactions
|
||||
if (state.canReact() && state.quickStates is Success) {
|
||||
// Separator
|
||||
bottomSheetItemSeparator {
|
||||
id("reaction_separator")
|
||||
}
|
||||
|
||||
bottomSheetItemQuickReactions {
|
||||
id("quick_reaction")
|
||||
fontProvider(fontProvider)
|
||||
texts(state.quickStates()?.map { it.reaction }.orEmpty())
|
||||
selecteds(state.quickStates.invoke().map { it.isSelected })
|
||||
listener(object : BottomSheetItemQuickReactions.Listener {
|
||||
override fun didSelect(emoji: String, selected: Boolean) {
|
||||
listener?.didSelectMenuAction(SimpleAction.QuickReact(state.eventId, emoji, selected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
bottomSheetItemSeparator {
|
||||
id("actions_separator")
|
||||
}
|
||||
|
||||
// Action
|
||||
state.actions()?.forEachIndexed { index, action ->
|
||||
bottomSheetItemAction {
|
||||
id("action_$index")
|
||||
iconRes(action.iconResId)
|
||||
textRes(action.titleRes)
|
||||
showExpand(action is SimpleAction.ReportContent)
|
||||
expanded(state.expendedReportContentMenu)
|
||||
listener(View.OnClickListener { listener?.didSelectMenuAction(action) })
|
||||
}
|
||||
|
||||
if (action is SimpleAction.ReportContent && state.expendedReportContentMenu) {
|
||||
// Special case for report content menu: add the submenu
|
||||
listOf(
|
||||
SimpleAction.ReportContentSpam(action.eventId),
|
||||
SimpleAction.ReportContentInappropriate(action.eventId),
|
||||
SimpleAction.ReportContentCustom(action.eventId)
|
||||
).forEachIndexed { indexReport, actionReport ->
|
||||
bottomSheetItemAction {
|
||||
id("actionReport_$indexReport")
|
||||
subMenuItem(true)
|
||||
iconRes(actionReport.iconResId)
|
||||
textRes(actionReport.titleRes)
|
||||
listener(View.OnClickListener { listener?.didSelectMenuAction(actionReport) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface MessageActionsEpoxyControllerListener {
|
||||
fun didSelectMenuAction(simpleAction: SimpleAction)
|
||||
}
|
||||
}
|
|
@ -21,26 +21,48 @@ import com.squareup.inject.assisted.AssistedInject
|
|||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.rx.RxRoom
|
||||
import im.vector.matrix.rx.unwrap
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.canReact
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Quick reactions state
|
||||
*/
|
||||
data class ToggleState(
|
||||
val reaction: String,
|
||||
val isSelected: Boolean
|
||||
)
|
||||
|
||||
data class MessageActionState(
|
||||
val roomId: String,
|
||||
val eventId: String,
|
||||
val informationData: MessageInformationData,
|
||||
val timelineEvent: Async<TimelineEvent> = Uninitialized
|
||||
val timelineEvent: Async<TimelineEvent> = Uninitialized,
|
||||
val messageBody: CharSequence? = null,
|
||||
// For quick reactions
|
||||
val quickStates: Async<List<ToggleState>> = Uninitialized,
|
||||
// For actions
|
||||
val actions: Async<List<SimpleAction>> = Uninitialized,
|
||||
val expendedReportContentMenu: Boolean = false
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
|
||||
|
@ -49,18 +71,101 @@ data class MessageActionState(
|
|||
|
||||
fun senderName(): String = informationData.memberName?.toString() ?: ""
|
||||
|
||||
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) }
|
||||
?: ""
|
||||
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: ""
|
||||
|
||||
fun canReact() = timelineEvent()?.canReact() == true
|
||||
}
|
||||
|
||||
fun messageBody(eventHtmlRenderer: EventHtmlRenderer?, noticeEventFormatter: NoticeEventFormatter?): CharSequence? {
|
||||
/**
|
||||
* Information related to an event and used to display preview in contextual bottomsheet.
|
||||
*/
|
||||
class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
initialState: MessageActionState,
|
||||
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
|
||||
private val session: Session,
|
||||
private val noticeEventFormatter: NoticeEventFormatter,
|
||||
private val stringProvider: StringProvider
|
||||
) : VectorViewModel<MessageActionState>(initialState) {
|
||||
|
||||
private val eventId = initialState.eventId
|
||||
private val informationData = initialState.informationData
|
||||
private val room = session.getRoom(initialState.roomId)
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: MessageActionState): MessageActionsViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
|
||||
|
||||
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
|
||||
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.messageActionViewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeEvent()
|
||||
observeReactions()
|
||||
observeEventAction()
|
||||
}
|
||||
|
||||
fun toggleReportMenu() = withState {
|
||||
setState {
|
||||
copy(
|
||||
expendedReportContentMenu = it.expendedReportContentMenu.not()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeEvent() {
|
||||
if (room == null) return
|
||||
RxRoom(room)
|
||||
.liveTimelineEvent(eventId)
|
||||
.unwrap()
|
||||
.execute {
|
||||
copy(
|
||||
timelineEvent = it,
|
||||
messageBody = computeMessageBody(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeEventAction() {
|
||||
if (room == null) return
|
||||
RxRoom(room)
|
||||
.liveTimelineEvent(eventId)
|
||||
.map {
|
||||
actionsForEvent(it)
|
||||
}
|
||||
.execute {
|
||||
copy(actions = it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeReactions() {
|
||||
if (room == null) return
|
||||
RxRoom(room)
|
||||
.liveAnnotationSummary(eventId)
|
||||
.map { annotations ->
|
||||
quickEmojis.map { emoji ->
|
||||
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
|
||||
}
|
||||
}
|
||||
.execute {
|
||||
copy(quickStates = it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeMessageBody(timelineEvent: Async<TimelineEvent>): CharSequence? {
|
||||
return when (timelineEvent()?.root?.getClearType()) {
|
||||
EventType.MESSAGE -> {
|
||||
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
eventHtmlRenderer?.render(messageContent.formattedBody
|
||||
?: messageContent.body)
|
||||
eventHtmlRenderer.get().render(messageContent.formattedBody
|
||||
?: messageContent.body)
|
||||
} else {
|
||||
messageContent?.body
|
||||
}
|
||||
|
@ -72,54 +177,177 @@ data class MessageActionState(
|
|||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> {
|
||||
timelineEvent()?.let { noticeEventFormatter?.format(it) }
|
||||
timelineEvent()?.let { noticeEventFormatter.format(it) }
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Information related to an event and used to display preview in contextual bottomsheet.
|
||||
*/
|
||||
class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
initialState: MessageActionState,
|
||||
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
|
||||
session: Session,
|
||||
private val noticeEventFormatter: NoticeEventFormatter
|
||||
) : VectorViewModel<MessageActionState>(initialState) {
|
||||
private fun actionsForEvent(optionalEvent: Optional<TimelineEvent>): List<SimpleAction> {
|
||||
val event = optionalEvent.getOrNull() ?: return emptyList()
|
||||
|
||||
private val eventId = initialState.eventId
|
||||
private val room = session.getRoom(initialState.roomId)
|
||||
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
|
||||
?: event.root.getClearContent().toModel()
|
||||
val type = messageContent?.type
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: MessageActionState): MessageActionsViewModel
|
||||
}
|
||||
return arrayListOf<SimpleAction>().apply {
|
||||
if (event.root.sendState.hasFailed()) {
|
||||
if (canRetry(event)) {
|
||||
add(SimpleAction.Resend(eventId))
|
||||
}
|
||||
add(SimpleAction.Remove(eventId))
|
||||
} else if (event.root.sendState.isSending()) {
|
||||
// TODO is uploading attachment?
|
||||
if (canCancel(event)) {
|
||||
add(SimpleAction.Cancel(eventId))
|
||||
}
|
||||
} else {
|
||||
if (!event.root.isRedacted()) {
|
||||
if (canReply(event, messageContent)) {
|
||||
add(SimpleAction.Reply(eventId))
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
|
||||
if (canEdit(event, session.myUserId)) {
|
||||
add(SimpleAction.Edit(eventId))
|
||||
}
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
|
||||
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.messageActionViewModelFactory.create(state)
|
||||
if (canRedact(event, session.myUserId)) {
|
||||
add(SimpleAction.Delete(eventId))
|
||||
}
|
||||
|
||||
if (canCopy(type)) {
|
||||
// TODO copy images? html? see ClipBoard
|
||||
add(SimpleAction.Copy(messageContent!!.body))
|
||||
}
|
||||
|
||||
if (event.canReact()) {
|
||||
add(SimpleAction.AddReaction(eventId))
|
||||
}
|
||||
|
||||
if (canQuote(event, messageContent)) {
|
||||
add(SimpleAction.Quote(eventId))
|
||||
}
|
||||
|
||||
if (canViewReactions(event)) {
|
||||
add(SimpleAction.ViewReactions(informationData))
|
||||
}
|
||||
|
||||
if (event.hasBeenEdited()) {
|
||||
add(SimpleAction.ViewEditHistory(informationData))
|
||||
}
|
||||
|
||||
if (canShare(type)) {
|
||||
if (messageContent is MessageImageContent) {
|
||||
session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
|
||||
add(SimpleAction.Share(url))
|
||||
}
|
||||
}
|
||||
// TODO
|
||||
}
|
||||
|
||||
if (event.root.sendState == SendState.SENT) {
|
||||
// TODO Can be redacted
|
||||
|
||||
// TODO sent by me or sufficient power level
|
||||
}
|
||||
}
|
||||
|
||||
add(SimpleAction.ViewSource(event.root.toContentStringWithIndent()))
|
||||
if (event.isEncrypted()) {
|
||||
val decryptedContent = event.root.toClearContentStringWithIndent()
|
||||
?: stringProvider.getString(R.string.encryption_information_decryption_error)
|
||||
add(SimpleAction.ViewDecryptedSource(decryptedContent))
|
||||
}
|
||||
add(SimpleAction.CopyPermalink(eventId))
|
||||
|
||||
if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
|
||||
// not sent by me
|
||||
add(SimpleAction.ReportContent(eventId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeEvent()
|
||||
private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
private fun observeEvent() {
|
||||
if (room == null) return
|
||||
RxRoom(room)
|
||||
.liveTimelineEvent(eventId)
|
||||
.unwrap()
|
||||
.execute {
|
||||
copy(timelineEvent = it)
|
||||
}
|
||||
private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
|
||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
return when (messageContent?.type) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_NOTICE,
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.MSGTYPE_IMAGE,
|
||||
MessageType.MSGTYPE_VIDEO,
|
||||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun resolveBody(state: MessageActionState): CharSequence? {
|
||||
return state.messageBody(eventHtmlRenderer.get(), noticeEventFormatter)
|
||||
private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean {
|
||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
return when (messageContent?.type) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_NOTICE,
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.FORMAT_MATRIX_HTML,
|
||||
MessageType.MSGTYPE_LOCATION -> {
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
|
||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
// TODO if user is admin or moderator
|
||||
return event.root.senderId == myUserId
|
||||
}
|
||||
|
||||
private fun canRetry(event: TimelineEvent): Boolean {
|
||||
return event.root.sendState.hasFailed() && event.root.isTextMessage()
|
||||
}
|
||||
|
||||
private fun canViewReactions(event: TimelineEvent): Boolean {
|
||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
// TODO if user is admin or moderator
|
||||
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
|
||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
// TODO if user is admin or moderator
|
||||
val messageContent = event.root.getClearContent().toModel<MessageContent>()
|
||||
return event.root.senderId == myUserId && (
|
||||
messageContent?.type == MessageType.MSGTYPE_TEXT
|
||||
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
|
||||
)
|
||||
}
|
||||
|
||||
private fun canCopy(type: String?): Boolean {
|
||||
return when (type) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_NOTICE,
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.FORMAT_MATRIX_HTML,
|
||||
MessageType.MSGTYPE_LOCATION -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun canShare(type: String?): Boolean {
|
||||
return when (type) {
|
||||
MessageType.MSGTYPE_IMAGE,
|
||||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_VIDEO -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
/*
|
||||
* 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.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Fragment showing the list of available contextual action for a given message.
|
||||
*/
|
||||
class MessageMenuFragment : VectorBaseFragment() {
|
||||
|
||||
@Inject lateinit var messageMenuViewModelFactory: MessageMenuViewModel.Factory
|
||||
private val viewModel: MessageMenuViewModel by fragmentViewModel(MessageMenuViewModel::class)
|
||||
private var addSeparators = false
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_message_menu
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
|
||||
val linearLayout = view as? LinearLayout
|
||||
if (linearLayout != null) {
|
||||
val inflater = LayoutInflater.from(linearLayout.context)
|
||||
linearLayout.removeAllViews()
|
||||
var insertIndex = 0
|
||||
val actions = state.actions()
|
||||
actions?.forEachIndexed { index, action ->
|
||||
inflateActionView(action, inflater, linearLayout)?.let {
|
||||
it.setOnClickListener {
|
||||
interactionListener?.didSelectMenuAction(action)
|
||||
}
|
||||
linearLayout.addView(it, insertIndex)
|
||||
insertIndex++
|
||||
if (addSeparators) {
|
||||
if (index < actions.size - 1) {
|
||||
linearLayout.addView(inflateSeparatorView(), insertIndex)
|
||||
insertIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun inflateActionView(action: SimpleAction, inflater: LayoutInflater, container: ViewGroup?): View? {
|
||||
return inflater.inflate(R.layout.adapter_item_action, container, false)?.apply {
|
||||
findViewById<ImageView>(R.id.action_icon)?.setImageResource(action.iconResId)
|
||||
findViewById<TextView>(R.id.action_title)?.setText(action.titleRes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun inflateSeparatorView(): View {
|
||||
val frame = FrameLayout(requireContext())
|
||||
frame.setBackgroundColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_list_divider_color))
|
||||
frame.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, requireContext().resources.displayMetrics.density.toInt())
|
||||
return frame
|
||||
}
|
||||
|
||||
interface InteractionListener {
|
||||
fun didSelectMenuAction(simpleAction: SimpleAction)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(pa: TimelineEventFragmentArgs): MessageMenuFragment {
|
||||
val args = Bundle()
|
||||
args.putParcelable(MvRx.KEY_ARG, pa)
|
||||
val fragment = MessageMenuFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,279 +0,0 @@
|
|||
/*
|
||||
* 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.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.airbnb.mvrx.*
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.rx.RxRoom
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.canReact
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
|
||||
sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) {
|
||||
data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction)
|
||||
data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy)
|
||||
data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit)
|
||||
data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote)
|
||||
data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply)
|
||||
data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share)
|
||||
data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw)
|
||||
data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash)
|
||||
data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete)
|
||||
data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round)
|
||||
data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source)
|
||||
data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source)
|
||||
data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink)
|
||||
data class Flag(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag)
|
||||
data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0)
|
||||
data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions)
|
||||
data class ViewEditHistory(val messageInformationData: MessageInformationData) :
|
||||
SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history)
|
||||
}
|
||||
|
||||
data class MessageMenuState(
|
||||
val roomId: String,
|
||||
val eventId: String,
|
||||
val informationData: MessageInformationData,
|
||||
val actions: Async<List<SimpleAction>> = Uninitialized
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages list actions for a given message (copy / paste / forward...)
|
||||
*/
|
||||
class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: MessageMenuState,
|
||||
private val session: Session,
|
||||
private val stringProvider: StringProvider) : VectorViewModel<MessageMenuState>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: MessageMenuState): MessageMenuViewModel
|
||||
}
|
||||
|
||||
private val room = session.getRoom(initialState.roomId)
|
||||
?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
|
||||
|
||||
private val eventId = initialState.eventId
|
||||
private val informationData: MessageInformationData = initialState.informationData
|
||||
|
||||
companion object : MvRxViewModelFactory<MessageMenuViewModel, MessageMenuState> {
|
||||
override fun create(viewModelContext: ViewModelContext, state: MessageMenuState): MessageMenuViewModel? {
|
||||
val fragment: MessageMenuFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.messageMenuViewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeEvent()
|
||||
}
|
||||
|
||||
private fun observeEvent() {
|
||||
RxRoom(room)
|
||||
.liveTimelineEvent(eventId)
|
||||
.map {
|
||||
actionsForEvent(it)
|
||||
}
|
||||
.execute {
|
||||
copy(actions = it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun actionsForEvent(optionalEvent: Optional<TimelineEvent>): List<SimpleAction> {
|
||||
val event = optionalEvent.getOrNull() ?: return emptyList()
|
||||
|
||||
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
|
||||
?: event.root.getClearContent().toModel()
|
||||
val type = messageContent?.type
|
||||
|
||||
return arrayListOf<SimpleAction>().apply {
|
||||
if (event.root.sendState.hasFailed()) {
|
||||
if (canRetry(event)) {
|
||||
add(SimpleAction.Resend(eventId))
|
||||
}
|
||||
add(SimpleAction.Remove(eventId))
|
||||
} else if (event.root.sendState.isSending()) {
|
||||
// TODO is uploading attachment?
|
||||
if (canCancel(event)) {
|
||||
add(SimpleAction.Cancel(eventId))
|
||||
}
|
||||
} else {
|
||||
if (!event.root.isRedacted()) {
|
||||
if (canReply(event, messageContent)) {
|
||||
add(SimpleAction.Reply(eventId))
|
||||
}
|
||||
|
||||
if (canEdit(event, session.myUserId)) {
|
||||
add(SimpleAction.Edit(eventId))
|
||||
}
|
||||
|
||||
if (canRedact(event, session.myUserId)) {
|
||||
add(SimpleAction.Delete(eventId))
|
||||
}
|
||||
|
||||
if (canCopy(type)) {
|
||||
// TODO copy images? html? see ClipBoard
|
||||
add(SimpleAction.Copy(messageContent!!.body))
|
||||
}
|
||||
|
||||
if (event.canReact()) {
|
||||
add(SimpleAction.AddReaction(eventId))
|
||||
}
|
||||
|
||||
if (canQuote(event, messageContent)) {
|
||||
add(SimpleAction.Quote(eventId))
|
||||
}
|
||||
|
||||
if (canViewReactions(event)) {
|
||||
add(SimpleAction.ViewReactions(informationData))
|
||||
}
|
||||
|
||||
if (event.hasBeenEdited()) {
|
||||
add(SimpleAction.ViewEditHistory(informationData))
|
||||
}
|
||||
|
||||
if (canShare(type)) {
|
||||
if (messageContent is MessageImageContent) {
|
||||
session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
|
||||
add(SimpleAction.Share(url))
|
||||
}
|
||||
}
|
||||
// TODO
|
||||
}
|
||||
|
||||
if (event.root.sendState == SendState.SENT) {
|
||||
// TODO Can be redacted
|
||||
|
||||
// TODO sent by me or sufficient power level
|
||||
}
|
||||
}
|
||||
|
||||
add(SimpleAction.ViewSource(event.root.toContentStringWithIndent()))
|
||||
if (event.isEncrypted()) {
|
||||
val decryptedContent = event.root.toClearContentStringWithIndent()
|
||||
?: stringProvider.getString(R.string.encryption_information_decryption_error)
|
||||
add(SimpleAction.ViewDecryptedSource(decryptedContent))
|
||||
}
|
||||
add(SimpleAction.CopyPermalink(eventId))
|
||||
|
||||
if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
|
||||
// not sent by me
|
||||
add(SimpleAction.Flag(eventId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
|
||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
return when (messageContent?.type) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_NOTICE,
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.MSGTYPE_IMAGE,
|
||||
MessageType.MSGTYPE_VIDEO,
|
||||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean {
|
||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
return when (messageContent?.type) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_NOTICE,
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.FORMAT_MATRIX_HTML,
|
||||
MessageType.MSGTYPE_LOCATION -> {
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
|
||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
// TODO if user is admin or moderator
|
||||
return event.root.senderId == myUserId
|
||||
}
|
||||
|
||||
private fun canRetry(event: TimelineEvent): Boolean {
|
||||
return event.root.sendState.hasFailed() && event.root.isTextMessage()
|
||||
}
|
||||
|
||||
private fun canViewReactions(event: TimelineEvent): Boolean {
|
||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
// TODO if user is admin or moderator
|
||||
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
|
||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
// TODO if user is admin or moderator
|
||||
val messageContent = event.root.getClearContent().toModel<MessageContent>()
|
||||
return event.root.senderId == myUserId && (
|
||||
messageContent?.type == MessageType.MSGTYPE_TEXT
|
||||
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
|
||||
)
|
||||
}
|
||||
|
||||
private fun canCopy(type: String?): Boolean {
|
||||
return when (type) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_NOTICE,
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.FORMAT_MATRIX_HTML,
|
||||
MessageType.MSGTYPE_LOCATION -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun canShare(type: String?): Boolean {
|
||||
return when (type) {
|
||||
MessageType.MSGTYPE_IMAGE,
|
||||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_VIDEO -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
/*
|
||||
* 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.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.EmojiCompatFontProvider
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.adapter_item_action_quick_reaction.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Quick Reaction Fragment (agree / like reactions)
|
||||
*/
|
||||
class QuickReactionFragment : VectorBaseFragment() {
|
||||
|
||||
private val viewModel: QuickReactionViewModel by fragmentViewModel(QuickReactionViewModel::class)
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
@Inject lateinit var fontProvider: EmojiCompatFontProvider
|
||||
@Inject lateinit var quickReactionViewModelFactory: QuickReactionViewModel.Factory
|
||||
|
||||
override fun getLayoutResId() = R.layout.adapter_item_action_quick_reaction
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
private lateinit var textViews: List<TextView>
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
textViews = listOf(quickReaction0, quickReaction1, quickReaction2, quickReaction3,
|
||||
quickReaction4, quickReaction5, quickReaction6, quickReaction7)
|
||||
textViews.forEachIndexed { index, textView ->
|
||||
textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT
|
||||
textView.setOnClickListener {
|
||||
viewModel.didSelect(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
val quickReactionsStates = it.quickStates() ?: return@withState
|
||||
quickReactionsStates.forEachIndexed { index, qs ->
|
||||
textViews[index].text = qs.reaction
|
||||
textViews[index].alpha = if (qs.isSelected) 0.2f else 1f
|
||||
}
|
||||
|
||||
if (it.result != null) {
|
||||
interactionListener?.didQuickReactWith(it.result.reaction, it.result.isSelected, it.eventId)
|
||||
}
|
||||
}
|
||||
|
||||
interface InteractionListener {
|
||||
fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(pa: TimelineEventFragmentArgs): QuickReactionFragment {
|
||||
val args = Bundle()
|
||||
args.putParcelable(MvRx.KEY_ARG, pa)
|
||||
val fragment = QuickReactionFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* 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.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import com.airbnb.mvrx.*
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.rx.RxRoom
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
|
||||
/**
|
||||
* Quick reactions state, it's a toggle with 3rd state
|
||||
*/
|
||||
data class ToggleState(
|
||||
val reaction: String,
|
||||
val isSelected: Boolean
|
||||
)
|
||||
|
||||
data class QuickReactionState(
|
||||
val roomId: String,
|
||||
val eventId: String,
|
||||
val informationData: MessageInformationData,
|
||||
val quickStates: Async<List<ToggleState>> = Uninitialized,
|
||||
val result: ToggleState? = null
|
||||
/** Pair of 'clickedOn' and current toggles state*/
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick reaction view model
|
||||
*/
|
||||
class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState: QuickReactionState,
|
||||
private val session: Session) : VectorViewModel<QuickReactionState>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: QuickReactionState): QuickReactionViewModel
|
||||
}
|
||||
|
||||
private val room = session.getRoom(initialState.roomId)
|
||||
private val eventId = initialState.eventId
|
||||
|
||||
companion object : MvRxViewModelFactory<QuickReactionViewModel, QuickReactionState> {
|
||||
|
||||
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: QuickReactionState): QuickReactionViewModel? {
|
||||
val fragment: QuickReactionFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.quickReactionViewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeReactions()
|
||||
}
|
||||
|
||||
private fun observeReactions() {
|
||||
if (room == null) return
|
||||
RxRoom(room)
|
||||
.liveAnnotationSummary(eventId)
|
||||
.map { annotations ->
|
||||
quickEmojis.map { emoji ->
|
||||
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe
|
||||
?: false)
|
||||
}
|
||||
}
|
||||
.execute {
|
||||
copy(quickStates = it)
|
||||
}
|
||||
}
|
||||
|
||||
fun didSelect(index: Int) = withState {
|
||||
val selectedReaction = it.quickStates()?.get(index) ?: return@withState
|
||||
val isSelected = selectedReaction.isSelected
|
||||
setState {
|
||||
copy(result = ToggleState(selectedReaction.reaction, !isSelected))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
|
||||
sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) {
|
||||
data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction)
|
||||
data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy)
|
||||
data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit)
|
||||
data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote)
|
||||
data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply)
|
||||
data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share)
|
||||
data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw)
|
||||
data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash)
|
||||
data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete)
|
||||
data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round)
|
||||
data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source)
|
||||
data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source)
|
||||
data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink)
|
||||
data class ReportContent(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag)
|
||||
data class ReportContentSpam(val eventId: String) : SimpleAction(R.string.report_content_spam, R.drawable.ic_report_spam)
|
||||
data class ReportContentInappropriate(val eventId: String) : SimpleAction(R.string.report_content_inappropriate, R.drawable.ic_report_inappropriate)
|
||||
data class ReportContentCustom(val eventId: String) : SimpleAction(R.string.report_content_custom, R.drawable.ic_report_custom)
|
||||
data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0)
|
||||
data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions)
|
||||
data class ViewEditHistory(val messageInformationData: MessageInformationData) :
|
||||
SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history)
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||
package im.vector.riotx.features.home.room.detail.timeline.edithistory
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
@ -21,17 +21,20 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import com.airbnb.epoxy.EpoxyRecyclerView
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -44,8 +47,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
@Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory
|
||||
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
|
||||
|
||||
@BindView(R.id.bottom_sheet_display_reactions_list)
|
||||
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
||||
@BindView(R.id.bottomSheetRecyclerView)
|
||||
lateinit var recyclerView: RecyclerView
|
||||
|
||||
private val epoxyController by lazy {
|
||||
ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
|
||||
|
@ -56,22 +59,23 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
|
||||
val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
|
||||
ButterKnife.bind(this, view)
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
epoxyRecyclerView.setController(epoxyController)
|
||||
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
|
||||
LinearLayout.VERTICAL)
|
||||
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
|
||||
recyclerView.adapter = epoxyController.adapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL)
|
||||
recyclerView.addItemDecoration(dividerItemDecoration)
|
||||
bottomSheetTitle.text = context?.getString(R.string.message_edits)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
epoxyController.setData(it)
|
||||
super.invalidate()
|
||||
}
|
||||
|
||||
companion object {
|
|
@ -13,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||
package im.vector.riotx.features.home.room.detail.timeline.edithistory
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Spannable
|
||||
|
@ -41,7 +41,7 @@ import timber.log.Timber
|
|||
import java.util.*
|
||||
|
||||
/**
|
||||
* Epoxy controller for reaction event list
|
||||
* Epoxy controller for edit history list
|
||||
*/
|
||||
class ViewEditHistoryEpoxyController(private val context: Context,
|
||||
val dateFormatter: VectorDateFormatter,
|
|
@ -13,7 +13,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||
package im.vector.riotx.features.home.room.detail.timeline.edithistory
|
||||
|
||||
import com.airbnb.mvrx.*
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
|
@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.room.model.message.isReply
|
|||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.date.VectorDateFormatter
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||
package im.vector.riotx.features.home.room.detail.timeline.reactions
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
|
@ -14,37 +14,38 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||
package im.vector.riotx.features.home.room.detail.timeline.reactions
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import com.airbnb.epoxy.EpoxyRecyclerView
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Bottom sheet displaying list of reactions for a given event ordered by timestamp
|
||||
*/
|
||||
class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
private val viewModel: ViewReactionViewModel by fragmentViewModel(ViewReactionViewModel::class)
|
||||
private val viewModel: ViewReactionsViewModel by fragmentViewModel(ViewReactionsViewModel::class)
|
||||
|
||||
@Inject lateinit var viewReactionViewModelFactory: ViewReactionViewModel.Factory
|
||||
@Inject lateinit var viewReactionsViewModelFactory: ViewReactionsViewModel.Factory
|
||||
|
||||
@BindView(R.id.bottom_sheet_display_reactions_list)
|
||||
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
||||
@BindView(R.id.bottomSheetRecyclerView)
|
||||
lateinit var recyclerView: RecyclerView
|
||||
|
||||
@Inject lateinit var epoxyController: ViewReactionsEpoxyController
|
||||
|
||||
|
@ -53,26 +54,25 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
|
||||
val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
|
||||
ButterKnife.bind(this, view)
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
epoxyRecyclerView.setController(epoxyController)
|
||||
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
|
||||
LinearLayout.VERTICAL)
|
||||
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||
recyclerView.adapter = epoxyController.adapter
|
||||
bottomSheetTitle.text = context?.getString(R.string.reactions)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) {
|
||||
epoxyController.setData(it)
|
||||
super.invalidate()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionBottomSheet {
|
||||
fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionsBottomSheet {
|
||||
val args = Bundle()
|
||||
val parcelableArgs = TimelineEventFragmentArgs(
|
||||
informationData.eventId,
|
||||
|
@ -80,7 +80,7 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
informationData
|
||||
)
|
||||
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
|
||||
return ViewReactionBottomSheet().apply { arguments = args }
|
||||
return ViewReactionsBottomSheet().apply { arguments = args }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||
package im.vector.riotx.features.home.room.detail.timeline.reactions
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import com.airbnb.mvrx.Fail
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||
package im.vector.riotx.features.home.room.detail.timeline.reactions
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
|
@ -30,6 +30,7 @@ import im.vector.matrix.rx.RxRoom
|
|||
import im.vector.matrix.rx.unwrap
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.date.VectorDateFormatter
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
|
||||
|
@ -53,10 +54,10 @@ data class ReactionInfo(
|
|||
/**
|
||||
* Used to display the list of members that reacted to a given event
|
||||
*/
|
||||
class ViewReactionViewModel @AssistedInject constructor(@Assisted
|
||||
class ViewReactionsViewModel @AssistedInject constructor(@Assisted
|
||||
initialState: DisplayReactionsViewState,
|
||||
private val session: Session,
|
||||
private val dateFormatter: VectorDateFormatter
|
||||
private val session: Session,
|
||||
private val dateFormatter: VectorDateFormatter
|
||||
) : VectorViewModel<DisplayReactionsViewState>(initialState) {
|
||||
|
||||
private val roomId = initialState.roomId
|
||||
|
@ -66,14 +67,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
|
|||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: DisplayReactionsViewState): ViewReactionViewModel
|
||||
fun create(initialState: DisplayReactionsViewState): ViewReactionsViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<ViewReactionViewModel, DisplayReactionsViewState> {
|
||||
companion object : MvRxViewModelFactory<ViewReactionsViewModel, DisplayReactionsViewState> {
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? {
|
||||
val fragment: ViewReactionBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.viewReactionViewModelFactory.create(state)
|
||||
override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionsViewModel? {
|
||||
val fragment: ViewReactionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.viewReactionsViewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
|
8
vector/src/main/res/drawable/ic_report_custom.xml
Normal file
8
vector/src/main/res/drawable/ic_report_custom.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<vector android:autoMirrored="true" android:height="22dp"
|
||||
android:viewportHeight="22" android:viewportWidth="22"
|
||||
android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#00000000" android:fillType="evenOdd"
|
||||
android:pathData="M21,1L10,12M21,1l-7,20 -4,-9 -9,-4z"
|
||||
android:strokeColor="#9E9E9E" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
</vector>
|
12
vector/src/main/res/drawable/ic_report_inappropriate.xml
Normal file
12
vector/src/main/res/drawable/ic_report_inappropriate.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector android:autoMirrored="true" android:height="22dp"
|
||||
android:viewportHeight="22" android:viewportWidth="22"
|
||||
android:width="22dp" 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="#9E9E9E" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000" android:fillType="evenOdd"
|
||||
android:pathData="M3.93,3.93l14.14,14.14"
|
||||
android:strokeColor="#9E9E9E" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
</vector>
|
8
vector/src/main/res/drawable/ic_report_spam.xml
Normal file
8
vector/src/main/res/drawable/ic_report_spam.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<vector android:autoMirrored="true" android:height="22dp"
|
||||
android:viewportHeight="22" android:viewportWidth="18"
|
||||
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#00000000" android:fillType="evenOdd"
|
||||
android:pathData="M1,14s1,-1 4,-1 5,2 8,2 4,-1 4,-1V2s-1,1 -4,1 -5,-2 -8,-2 -4,1 -4,1v12zM1,21v-7"
|
||||
android:strokeColor="#9E9E9E" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
</vector>
|
11
vector/src/main/res/layout/bottom_sheet_generic_list.xml
Normal file
11
vector/src/main/res/layout/bottom_sheet_generic_list.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/bottomSheetRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fadeScrollbars="false"
|
||||
android:scrollbars="vertical"
|
||||
tools:itemCount="5"
|
||||
tools:listitem="@layout/item_bottom_sheet_action" />
|
|
@ -3,29 +3,25 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="400dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottomSheetTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="44dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="8dp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="16sp"
|
||||
tools:text="@string/reactions" />
|
||||
|
||||
<com.airbnb.epoxy.EpoxyRecyclerView
|
||||
android:id="@+id/bottom_sheet_display_reactions_list"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/bottomSheetRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent"
|
||||
android:fadeScrollbars="false"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
tools:itemCount="15"
|
||||
tools:listitem="@layout/item_simple_reaction_info">
|
||||
tools:listitem="@layout/item_simple_reaction_info" />
|
||||
|
||||
</com.airbnb.epoxy.EpoxyRecyclerView>
|
||||
</LinearLayout>
|
|
@ -1,149 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="@string/bottom_sheet_behavior">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/bottom_sheet_message_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bottom_sheet_message_preview_avatar"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_margin="@dimen/layout_horizontal_margin"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/circle"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_sender"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:singleLine="true"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_timestamp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
|
||||
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_body"
|
||||
tools:text="Friday 8pm" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/messageStatusInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="16dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/messageStatusProgress"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageStatusText"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:drawableStart="@drawable/ic_warning_small"
|
||||
android:drawablePadding="4dp"
|
||||
tools:text="@string/unable_to_send_message" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/quickReactTopDivider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/bottom_sheet_quick_reaction_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:background="@android:color/holo_green_light"
|
||||
tools:layout_height="180dp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/quickReactBottomDivider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/bottom_sheet_menu_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:background="@android:color/holo_blue_dark"
|
||||
tools:layout_height="250dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
28
vector/src/main/res/layout/dialog_report_content.xml
Normal file
28
vector/src/main/res/layout/dialog_report_content.xml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/layout_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingLeft="?dialogPreferredPadding"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="?dialogPreferredPadding"
|
||||
android:paddingRight="?dialogPreferredPadding">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/VectorTextInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/report_content_custom_hint"
|
||||
app:counterEnabled="true"
|
||||
app:counterMaxLength="240">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/dialog_report_content_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
|
@ -2,23 +2,29 @@
|
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="50dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="@dimen/layout_horizontal_margin"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="8dp"
|
||||
tools:layout_height="50dp">
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<Space
|
||||
android:id="@+id/action_start_space"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/action_icon"
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:tint="?riotx_text_secondary"
|
||||
|
@ -28,11 +34,19 @@
|
|||
android:id="@+id/action_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="17sp"
|
||||
tools:text="@string/delete" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/action_expand"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:tint="?riotx_text_secondary"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/ic_material_expand_more_black"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
6
vector/src/main/res/layout/item_bottom_sheet_divider.xml
Normal file
6
vector/src/main/res/layout/item_bottom_sheet_divider.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/quickReactTopDivider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
|
@ -0,0 +1,78 @@
|
|||
<?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/bottom_sheet_message_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bottom_sheet_message_preview_avatar"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_margin="@dimen/layout_horizontal_margin"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/circle"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_sender"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:singleLine="true"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_timestamp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
|
||||
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_body"
|
||||
tools:text="Friday 8pm" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/messageStatusInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/messageStatusProgress"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageStatusText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:drawableStart="@drawable/ic_warning_small"
|
||||
android:drawablePadding="4dp"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textStyle="bold"
|
||||
tools:text="@string/unable_to_send_message" />
|
||||
|
||||
</LinearLayout>
|
|
@ -117,7 +117,16 @@
|
|||
android:layout_marginBottom="4dp"
|
||||
app:dividerDrawable="@drawable/reaction_divider"
|
||||
app:flexWrap="wrap"
|
||||
app:showDivider="middle" />
|
||||
app:showDivider="middle"
|
||||
tools:background="#F0E0F0"
|
||||
tools:layout_height="40dp">
|
||||
|
||||
<!-- ReactionButtons will be added here in the code -->
|
||||
<!--im.vector.riotx.features.reactions.widget.ReactionButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" /-->
|
||||
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
|
||||
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||
android:id="@+id/readReceiptsView"
|
||||
|
|
|
@ -37,4 +37,19 @@
|
|||
|
||||
<string name="error_file_too_big">"The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."</string>
|
||||
|
||||
|
||||
<string name="report_content_spam">"It's spam"</string>
|
||||
<string name="report_content_inappropriate">"It's inappropriate"</string>
|
||||
<string name="report_content_custom">"Custom report"</string>
|
||||
<string name="report_content_custom_title">"Report this content"</string>
|
||||
<string name="report_content_custom_hint">"Reason for reporting this content"</string>
|
||||
<string name="report_content_custom_submit">"REPORT"</string>
|
||||
<string name="block_user">"BLOCK USER"</string>
|
||||
|
||||
<string name="content_reported_title">"Content reported"</string>
|
||||
<string name="content_reported_content">"This content was reported.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
|
||||
<string name="content_reported_as_spam_title">"Reported as spam"</string>
|
||||
<string name="content_reported_as_spam_content">"This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
|
||||
<string name="content_reported_as_inappropriate_title">"Reported as inappropriate"</string>
|
||||
<string name="content_reported_as_inappropriate_content">"This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
|
||||
</resources>
|
||||
|
|
Loading…
Add table
Reference in a new issue