Merge pull request #779 from vector-im/feature/fix_some_crashes

Fix some crashes and issues
This commit is contained in:
Benoit Marty 2019-12-19 14:02:19 +01:00 committed by GitHub
commit 0d36e9d8a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 302 additions and 87 deletions

View File

@ -7,6 +7,7 @@ Features ✨:
Improvements 🙌:
- Handle navigation to room via room alias (#201)
- Open matrix.to link in RiotX (#57)
- Limit sticker size in the timeline
Other changes:
- Use same default room colors than Riot-Web
@ -14,7 +15,9 @@ Other changes:
Bugfix 🐛:
- Scroll breadcrumbs to top when opened
- Render default room name when it starts with an emoji (#477)
- Do not display " (IRC)") in display names https://github.com/vector-im/riot-android/issues/444
- Do not display " (IRC)" in display names https://github.com/vector-im/riot-android/issues/444
- Fix rendering issue with HTML formatted body
- Disable click on Stickers (#703)
Translations 🗣:
-

View File

@ -30,7 +30,7 @@ data class ContentAttachmentData(
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
val name: String? = null,
val path: String,
val mimeType: String,
val mimeType: String?,
val type: Type
) : Parcelable {

View File

@ -25,7 +25,7 @@ data class VideoInfo(
/**
* The mimetype of the video e.g. "video/mp4".
*/
@Json(name = "mimetype") val mimeType: String,
@Json(name = "mimetype") val mimeType: String?,
/**
* The width of the video in pixels.

View File

@ -104,7 +104,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
root.getClearContent().toModel<MessageStickerContent>()
} else {
annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel()
?: root.getClearContent().toModel()
}
}
@ -116,7 +116,7 @@ fun TimelineEvent.getLastMessageBody(): String? {
if (lastMessageContent != null) {
return lastMessageContent.newContent?.toModel<MessageContent>()?.body
?: lastMessageContent.body
?: lastMessageContent.body
}
return null

View File

@ -49,7 +49,7 @@ object MXEncryptedAttachments {
* @param mimetype the mime type
* @return the encryption file info
*/
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult {
fun encryptAttachment(attachmentStream: InputStream, mimetype: String?): EncryptionResult {
val t0 = System.currentTimeMillis()
val secureRandom = SecureRandom()

View File

@ -83,15 +83,13 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
if (elementToDecrypt != null) {
Timber.v("## decrypt file")
MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error")
} else {
inputStream
MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
?: throw IllegalStateException("Decryption error")
}
writeToFile(inputStream, destFile)
destFile
}
.map { inputStream ->
writeToFile(inputStream, destFile)
destFile
}
} else {
Try.just(destFile)
}

View File

@ -43,9 +43,9 @@ internal class FileUploader @Inject constructor(@Authenticated
suspend fun uploadFile(file: File,
filename: String?,
mimeType: String,
mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull())
return upload(uploadBody, filename, progressListener)
}

View File

@ -52,7 +52,7 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
apiCall = roomAPI.createRoom(params)
}
val roomId = createRoomResponse.roomId!!
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId)

View File

@ -251,7 +251,7 @@ internal class LocalEchoEventFactory @Inject constructor(
type = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio",
audioInfo = AudioInfo(
mimeType = attachment.mimeType.takeIf { it.isNotBlank() } ?: "audio/mpeg",
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
size = attachment.size
),
url = attachment.path
@ -264,7 +264,7 @@ internal class LocalEchoEventFactory @Inject constructor(
type = MessageType.MSGTYPE_FILE,
body = attachment.name ?: "file",
info = FileInfo(
mimeType = attachment.mimeType.takeIf { it.isNotBlank() }
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() }
?: "application/octet-stream",
size = attachment.size
),

View File

@ -293,6 +293,7 @@ dependencies {
implementation 'me.gujun.android:span:1.7'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version"
implementation 'com.googlecode.htmlcompressor:htmlcompressor:1.4'
implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1'
implementation "androidx.autofill:autofill:$autofill_version"

View File

@ -31,4 +31,8 @@
<issue id="ViewConstructor" severity="error" />
<issue id="UseValueOf" severity="error" />
<!-- Ignore error from HtmlCompressor lib -->
<issue id="InvalidPackage">
<ignore path="**/htmlcompressor-1.4.jar"/>
</issue>
</lint>

View File

@ -359,6 +359,11 @@ SOFTWARE.
<br/>
Copyright 2018 Kumar Bibek
</li>
<li>
<b>htmlcompressor</b>
<br/>
Copyright 2017 Sergiy Kovalchuk
</li>
</ul>
<pre>
Apache License

View File

@ -38,6 +38,7 @@ import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.HomeRoomListDataSource
import im.vector.riotx.features.home.group.SelectedGroupDataSource
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.notifications.*
import im.vector.riotx.features.rageshake.BugReporter
@ -87,6 +88,8 @@ interface VectorComponent {
fun eventHtmlRenderer(): EventHtmlRenderer
fun vectorHtmlCompressor(): VectorHtmlCompressor
fun navigator(): Navigator
fun errorFormatter(): ErrorFormatter

View File

@ -0,0 +1,30 @@
/*
* 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.epoxy
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
/**
* Item of size (0, 0).
* It can be useful to avoid automatic scroll of RecyclerView with Epoxy controller, when the first valuable item changes.
*/
@EpoxyModelClass(layout = R.layout.item_zero)
abstract class ZeroItem : VectorEpoxyModel<ZeroItem.Holder>() {
class Holder : VectorEpoxyHolder()
}

View File

@ -0,0 +1,31 @@
/*
* 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.error
import im.vector.riotx.BuildConfig
import timber.log.Timber
/**
* throw in debug, only log in production. As this method does not always throw, next statement should be a return
*/
fun fatalError(message: String) {
if (BuildConfig.DEBUG) {
error(message)
} else {
Timber.e(message)
}
}

View File

@ -222,8 +222,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
override fun onResume() {
super.onResume()
Timber.v("onResume Activity ${this.javaClass.simpleName}")
Timber.i("onResume Activity ${this.javaClass.simpleName}")
configurationViewModel.onActivityResumed()

View File

@ -32,6 +32,7 @@ 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.utils.DimensionConverter
import timber.log.Timber
/**
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
@ -80,6 +81,11 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
super.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
Timber.i("onResume BottomSheet ${this.javaClass.simpleName}")
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
val dialog = this as? BottomSheetDialog

View File

@ -104,7 +104,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
@CallSuper
override fun onResume() {
super.onResume()
Timber.v("onResume Fragment ${this.javaClass.simpleName}")
Timber.i("onResume Fragment ${this.javaClass.simpleName}")
}
@CallSuper

View File

@ -0,0 +1,20 @@
/*
* 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.ui.model
// android.util.Size in API 21+
data class Size(val width: Int, val height: Int)

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.attachments
import com.kbeanie.multipicker.api.entity.*
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import timber.log.Timber
fun ChosenContact.toContactAttachment(): ContactAttachment {
return ContactAttachment(
@ -29,6 +30,7 @@ fun ChosenContact.toContactAttachment(): ContactAttachment {
}
fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
@ -40,6 +42,7 @@ fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
}
fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
@ -51,16 +54,17 @@ fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
)
}
fun ChosenFile.mapType(): ContentAttachmentData.Type {
private fun ChosenFile.mapType(): ContentAttachmentData.Type {
return when {
mimeType.startsWith("image/") -> ContentAttachmentData.Type.IMAGE
mimeType.startsWith("video/") -> ContentAttachmentData.Type.VIDEO
mimeType.startsWith("audio/") -> ContentAttachmentData.Type.AUDIO
else -> ContentAttachmentData.Type.FILE
mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE
mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO
mimeType?.startsWith("audio/") == true -> ContentAttachmentData.Type.AUDIO
else -> ContentAttachmentData.Type.FILE
}
}
fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
@ -75,6 +79,7 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
}
fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,

View File

@ -68,14 +68,14 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
}
private fun observeSelectionState() {
selectSubscribe(GroupListViewState::selectedGroup) {
if (it != null) {
selectSubscribe(GroupListViewState::selectedGroup) { groupSummary ->
if (groupSummary != null) {
val selectedGroup = _openGroupLiveData.value?.peekContent()
// We only wan to open group if the updated selectedGroup is a different one.
if (selectedGroup?.groupId != it.groupId) {
_openGroupLiveData.postLiveEvent(it)
// We only want to open group if the updated selectedGroup is a different one.
if (selectedGroup?.groupId != groupSummary.groupId) {
_openGroupLiveData.postLiveEvent(groupSummary)
}
val optionGroup = Option.fromNullable(it)
val optionGroup = Option.just(groupSummary)
selectedGroupStore.post(optionGroup)
}
}

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.breadcrumbs
import android.view.View
import com.airbnb.epoxy.EpoxyController
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.core.epoxy.zeroItem
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
@ -45,9 +46,13 @@ class BreadcrumbsController @Inject constructor(
override fun buildModels() {
val safeViewState = viewState ?: return
// Add a ZeroItem to avoid automatic scroll when the breadcrumbs are updated from another client
zeroItem {
id("top")
}
// An empty breadcrumbs list can only be temporary because when entering in a room,
// this one is added to the breadcrumbs
safeViewState.asyncBreadcrumbs.invoke()
?.forEach {
breadcrumbsItem {

View File

@ -86,8 +86,6 @@ import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotx.features.command.Command
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.permalink.NavigateToRoomInterceptor
import im.vector.riotx.features.permalink.PermalinkHandler
import im.vector.riotx.features.home.getColorFromUserId
import im.vector.riotx.features.home.room.detail.composer.TextComposerAction
import im.vector.riotx.features.home.room.detail.composer.TextComposerView
@ -109,6 +107,8 @@ import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.permalink.NavigateToRoomInterceptor
import im.vector.riotx.features.permalink.PermalinkHandler
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.share.SharedData
@ -416,8 +416,10 @@ class RoomDetailFragment @Inject constructor(
composerLayout.composerRelatedMessageAvatar
)
composerLayout.expand {
// need to do it here also when not using quick reply
focusComposerAndShowKeyboard()
if (isAdded) {
// need to do it here also when not using quick reply
focusComposerAndShowKeyboard()
}
}
focusComposerAndShowKeyboard()
}

View File

@ -41,6 +41,7 @@ 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 im.vector.riotx.features.html.VectorHtmlCompressor
import java.text.SimpleDateFormat
import java.util.*
@ -82,6 +83,7 @@ data class MessageActionState(
class MessageActionsViewModel @AssistedInject constructor(@Assisted
initialState: MessageActionState,
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
private val htmlCompressor: VectorHtmlCompressor,
private val session: Session,
private val noticeEventFormatter: NoticeEventFormatter,
private val stringProvider: StringProvider
@ -100,6 +102,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.messageActionViewModelFactory.create(state)
@ -167,11 +170,16 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun computeMessageBody(timelineEvent: Async<TimelineEvent>): CharSequence? {
return when (timelineEvent()?.root?.getClearType()) {
EventType.MESSAGE -> {
EventType.MESSAGE,
EventType.STICKER -> {
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
eventHtmlRenderer.get().render(messageContent.formattedBody
?: messageContent.body)
val html = messageContent.formattedBody
?.takeIf { it.isNotBlank() }
?.let { htmlCompressor.compress(it) }
?: messageContent.body
eventHtmlRenderer.get().render(html)
} else {
messageContent?.body
}

View File

@ -61,6 +61,7 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
companion object : MvRxViewModelFactory<ViewEditHistoryViewModel, ViewEditHistoryViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: ViewEditHistoryViewState): ViewEditHistoryViewModel? {
val fragment: ViewEditHistoryBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewEditHistoryViewModelFactory.create(state)

View File

@ -46,6 +46,7 @@ import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMoveme
import im.vector.riotx.features.home.room.detail.timeline.tools.linkify
import im.vector.riotx.features.html.CodeVisitor
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import me.gujun.android.span.span
@ -57,6 +58,7 @@ class MessageItemFactory @Inject constructor(
private val dimensionConverter: DimensionConverter,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val htmlRenderer: Lazy<EventHtmlRenderer>,
private val htmlCompressor: VectorHtmlCompressor,
private val stringProvider: StringProvider,
private val imageContentRenderer: ImageContentRenderer,
private val messageInformationDataFactory: MessageInformationDataFactory,
@ -179,10 +181,16 @@ class MessageItemFactory @Inject constructor(
.playable(messageContent.info?.mimeType == "image/gif")
.highlighted(highlight)
.mediaData(data)
.clickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view)
}))
.apply {
if (messageContent.type == MessageType.MSGTYPE_STICKER_LOCAL) {
mode(ImageContentRenderer.Mode.STICKER)
} else {
clickListener(
DebouncedClickListener(View.OnClickListener { view ->
callback?.onImageMessageClicked(messageContent, data, view)
}))
}
}
}
private fun buildVideoMessageItem(messageContent: MessageVideoContent,
@ -227,6 +235,7 @@ class MessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
return if (isFormatted) {
// First detect if the message contains some code block(s) or inline code
val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document
val codeVisitor = CodeVisitor()
codeVisitor.visit(localFormattedBody)
@ -240,7 +249,8 @@ class MessageItemFactory @Inject constructor(
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
}
CodeVisitor.Kind.NONE -> {
val formattedBody = htmlRenderer.get().render(messageContent.formattedBody!!)
val compressed = htmlCompressor.compress(messageContent.formattedBody!!)
val formattedBody = htmlRenderer.get().render(compressed)
buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
}
}

View File

@ -36,6 +36,8 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
@EpoxyAttribute
var playable: Boolean = false
@EpoxyAttribute
var mode = ImageContentRenderer.Mode.THUMBNAIL
@EpoxyAttribute
var clickListener: View.OnClickListener? = null
@EpoxyAttribute
lateinit var imageContentRenderer: ImageContentRenderer
@ -44,7 +46,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
override fun bind(holder: Holder) {
super.bind(holder)
imageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView)
imageContentRenderer.render(mediaData, mode, holder.imageView)
if (!attributes.informationData.sendState.hasFailed()) {
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, mediaData.isLocalFile(), holder.progressLayout)
} else {

View File

@ -68,6 +68,7 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted
companion object : MvRxViewModelFactory<ViewReactionsViewModel, DisplayReactionsViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionsViewModel? {
val fragment: ViewReactionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewReactionsViewModelFactory.create(state)

View File

@ -37,6 +37,7 @@ class RoomListQuickActionsViewModel @AssistedInject constructor(@Assisted initia
companion object : MvRxViewModelFactory<RoomListQuickActionsViewModel, RoomListQuickActionsState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomListQuickActionsState): RoomListQuickActionsViewModel? {
val fragment: RoomListQuickActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.roomListActionsViewModelFactory.create(state)

View File

@ -0,0 +1,40 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.html
import com.googlecode.htmlcompressor.compressor.Compressor
import com.googlecode.htmlcompressor.compressor.HtmlCompressor
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class VectorHtmlCompressor @Inject constructor() {
// All default options are suitable so far
private val htmlCompressor: Compressor = HtmlCompressor()
fun compress(html: String): String {
var result = htmlCompressor.compress(html)
// Trim space after <br> and <p>, unfortunately the method setRemoveSurroundingSpaces() from the doc does not exist
result = result.replace("<br> ", "<br>")
result = result.replace("<br/> ", "<br/>")
result = result.replace("<p> ", "<p>")
return result
}
}

View File

@ -31,11 +31,13 @@ import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequest
import im.vector.riotx.core.ui.model.Size
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.core.utils.isLocalFile
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
import javax.inject.Inject
import kotlin.math.min
class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val dimensionConverter: DimensionConverter) {
@ -56,17 +58,18 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
enum class Mode {
FULL_SIZE,
THUMBNAIL
THUMBNAIL,
STICKER
}
fun render(data: Data, mode: Mode, imageView: ImageView) {
val (width, height) = processSize(data, mode)
imageView.layoutParams.height = height
imageView.layoutParams.width = width
val size = processSize(data, mode)
imageView.layoutParams.width = size.width
imageView.layoutParams.height = size.height
// a11y
imageView.contentDescription = data.filename
createGlideRequest(data, mode, imageView, width, height)
createGlideRequest(data, mode, imageView, size)
.dontAnimate()
.transform(RoundedCorners(dimensionConverter.dpToPx(8)))
.thumbnail(0.3f)
@ -74,12 +77,12 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
}
fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
val (width, height) = processSize(data, mode)
val size = processSize(data, mode)
// a11y
imageView.contentDescription = data.filename
createGlideRequest(data, mode, imageView, width, height)
createGlideRequest(data, mode, imageView, size)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?,
model: Any?,
@ -102,7 +105,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView)
}
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, width: Int, height: Int): GlideRequest<Drawable> {
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
return if (data.elementToDecrypt != null) {
// Encrypted image
GlideApp
@ -112,8 +115,9 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
// Clear image
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = when (mode) {
Mode.FULL_SIZE -> contentUrlResolver.resolveFullSize(data.url)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
Mode.FULL_SIZE,
Mode.STICKER -> contentUrlResolver.resolveFullSize(data.url)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
}
// Fallback to base url
?: data.url
@ -144,23 +148,32 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
)
}
private fun processSize(data: Data, mode: Mode): Pair<Int, Int> {
private fun processSize(data: Data, mode: Mode): Size {
val maxImageWidth = data.maxWidth
val maxImageHeight = data.maxHeight
val width = data.width ?: maxImageWidth
val height = data.height ?: maxImageHeight
var finalHeight = -1
var finalWidth = -1
var finalHeight = -1
// if the image size is known
// compute the expected height
if (width > 0 && height > 0) {
if (mode == Mode.FULL_SIZE) {
finalHeight = height
finalWidth = width
} else {
finalHeight = Math.min(maxImageWidth * height / width, maxImageHeight)
finalWidth = finalHeight * width / height
when (mode) {
Mode.FULL_SIZE -> {
finalHeight = height
finalWidth = width
}
Mode.THUMBNAIL -> {
finalHeight = min(maxImageWidth * height / width, maxImageHeight)
finalWidth = finalHeight * width / height
}
Mode.STICKER -> {
// limit on width
val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2)
finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp)
finalHeight = finalWidth * height / width
}
}
}
// ensure that some values are properly initialized
@ -170,6 +183,6 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
if (finalWidth < 0) {
finalWidth = maxImageWidth
}
return Pair(finalWidth, finalHeight)
return Size(finalWidth, finalHeight)
}
}

View File

@ -19,8 +19,11 @@ package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.core.app.TaskStackBuilder
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.error.fatalError
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
@ -38,12 +41,18 @@ import im.vector.riotx.features.share.SharedData
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
import androidx.core.app.TaskStackBuilder
@Singleton
class DefaultNavigator @Inject constructor() : Navigator {
class DefaultNavigator @Inject constructor(
private val sessionHolder: ActiveSessionHolder
) : Navigator {
override fun openRoom(context: Context, roomId: String, eventId: String?, buildTask: Boolean) {
if (sessionHolder.getSafeActiveSession()?.getRoom(roomId) == null) {
fatalError("Trying to open an unknown room $roomId")
return
}
val args = RoomDetailArgs(roomId, eventId)
val intent = RoomDetailActivity.newIntent(context, args)
if (buildTask) {

View File

@ -57,7 +57,7 @@ class PermalinkHandler @Inject constructor(private val session: Session,
.observeOn(AndroidSchedulers.mainThread())
.map {
val roomId = it.getOrNull()
if (navigateToRoomInterceptor?.navToRoom(roomId) != true) {
if (navigateToRoomInterceptor?.navToRoom(roomId, permalinkData.eventId) != true) {
openRoom(context, roomId, permalinkData.eventId, buildTask)
}
true
@ -87,9 +87,9 @@ class PermalinkHandler @Inject constructor(private val session: Session,
}
/**
* Open room either joined, or not unknown
* Open room either joined, or not
*/
private fun openRoom(context: Context, roomId: String?, eventId: String? = null, buildTask: Boolean) {
private fun openRoom(context: Context, roomId: String?, eventId: String?, buildTask: Boolean) {
return if (roomId != null && session.getRoom(roomId) != null) {
navigator.openRoom(context, roomId, eventId, buildTask)
} else {

View File

@ -27,7 +27,6 @@ import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.home.LoadingFragment
import im.vector.riotx.features.login.LoginActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.debug.activity_test_material_theme.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject

View File

@ -77,8 +77,7 @@ class BugReportActivity : VectorBaseActivity() {
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
menu.findItem(R.id.ic_action_send_bug_report)?.let {
val isValid = bug_report_edit_text.text.toString().trim().length > 10
&& !bug_report_mask_view.isVisible
val isValid = !bug_report_mask_view.isVisible
it.isEnabled = isValid
it.icon.alpha = if (isValid) 255 else 100
@ -90,7 +89,11 @@ class BugReportActivity : VectorBaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.ic_action_send_bug_report -> {
sendBugReport()
if (bug_report_edit_text.text.toString().trim().length >= 10) {
sendBugReport()
} else {
bug_report_text_input_layout.error = getString(R.string.bug_report_error_too_short)
}
return true
}
}
@ -150,7 +153,7 @@ class BugReportActivity : VectorBaseActivity() {
val myProgress = progress.coerceIn(0, 100)
bug_report_progress_view.progress = myProgress
bug_report_progress_text_view.text = getString(R.string.send_bug_report_progress, "$myProgress")
bug_report_progress_text_view.text = getString(R.string.send_bug_report_progress, myProgress.toString())
}
override fun onUploadSucceed() {
@ -179,7 +182,7 @@ class BugReportActivity : VectorBaseActivity() {
@OnTextChanged(R.id.bug_report_edit_text)
internal fun textChanged() {
invalidateOptionsMenu()
bug_report_text_input_layout.error = null
}
@OnCheckedChanged(R.id.bug_report_button_include_screenshot)

View File

@ -33,6 +33,7 @@ import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.toOnOff
import im.vector.riotx.core.utils.getDeviceLocale
import im.vector.riotx.features.settings.VectorLocale
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotx.features.version.VersionProvider
import okhttp3.Call
@ -44,12 +45,15 @@ import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
import java.io.*
import java.io.File
import java.io.IOException
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.util.Locale
import java.util.*
import java.util.zip.GZIPOutputStream
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.collections.ArrayList
/**
* BugReporter creates and sends the bug reports.
@ -57,6 +61,7 @@ import javax.inject.Singleton
@Singleton
class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val versionProvider: VersionProvider,
private val vectorPreferences: VectorPreferences,
private val vectorFileLogger: VectorFileLogger) {
var inMultiWindowMode = false
@ -230,7 +235,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
.addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion())
.addFormDataPart("olm_version", olmVersion)
.addFormDataPart("device", Build.MODEL.trim())
.addFormDataPart("lazy_loading", true.toOnOff())
.addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff())
.addFormDataPart("multi_window", inMultiWindowMode.toOnOff())
.addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") "
+ Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME)

View File

@ -24,9 +24,7 @@ import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.*
import java.util.logging.*
import java.util.logging.Formatter
import javax.inject.Inject
@ -83,7 +81,8 @@ class VectorFileLogger @Inject constructor(val context: Context, private val vec
return if (vectorPreferences.labAllowedExtendedLogging()) {
false
} else {
priority < Log.ERROR
// Exclude debug and verbose logs
priority <= Log.DEBUG
}
}

View File

@ -42,6 +42,7 @@ class EmojiSearchResultViewModel @AssistedInject constructor(
companion object : MvRxViewModelFactory<EmojiSearchResultViewModel, EmojiSearchResultViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: EmojiSearchResultViewState): EmojiSearchResultViewModel? {
val activity: EmojiReactionPickerActivity = (viewModelContext as ActivityViewModelContext).activity()
return activity.emojiSearchResultViewModelFactory.create(state)

View File

@ -178,7 +178,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
copy(
asyncPublicRoomsRequest = Success(data.chunk!!),
// It's ok to append at the end of the list, so I use publicRooms.size()
publicRooms = publicRooms.appendAt(data.chunk!!, publicRooms.size),
publicRooms = publicRooms.appendAt(data.chunk!!, publicRooms.size)
// Rageshake #8206 tells that we can have several times the same room
.distinctBy { it.roomId },
hasMore = since != null
)
}

View File

@ -65,7 +65,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), HasScree
override fun onResume() {
super.onResume()
Timber.v("onResume Fragment ${this.javaClass.simpleName}")
Timber.i("onResume Fragment ${this.javaClass.simpleName}")
vectorActivity.supportActionBar?.setTitle(titleRes)
// find the view from parent activity
mLoadingView = vectorActivity.findViewById(R.id.vector_settings_spinner_views)

View File

@ -40,6 +40,7 @@ class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState:
companion object : MvRxViewModelFactory<PushGatewaysViewModel, PushGatewayViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: PushGatewayViewState): PushGatewaysViewModel? {
val fragment: PushGatewaysFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.pushGatewaysViewModelFactory.create(state)

View File

@ -1,5 +1,6 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -82,7 +83,9 @@
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:hint="@string/send_bug_report_placeholder"
android:textColorHint="?attr/vctr_default_text_hint_color">
android:textColorHint="?attr/vctr_default_text_hint_color"
app:counterEnabled="true"
app:counterMaxLength="500">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/bug_report_edit_text"

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dp"
android:layout_height="0dp" />

View File

@ -114,7 +114,7 @@
<!-- Replaced string is the homeserver url -->
<string name="login_signup_to">Sign up to %1$s</string>
<string name="login_signup_username_hint">Username</string>
<string name="login_signup_username_hint">Username or email</string>
<string name="login_signup_password_hint">Password</string>
<string name="login_signup_submit">Next</string>
<string name="login_signup_error_user_in_use">That username is taken</string>
@ -162,4 +162,5 @@
<string name="soft_logout_sso_not_same_user_error">The current session is for user %1$s and you provide credentials for user %2$s. This is not supported by RiotX.\nPlease first clear data, then sign in again on another account.</string>
<string name="permalink_malformed">Your matrix.to link was malformed</string>
<string name="bug_report_error_too_short">The description is too short</string>
</resources>