Merge pull request #619 from vector-im/feature/attachments
Feature/attachments
This commit is contained in:
commit
70a14f6350
|
@ -14,3 +14,5 @@
|
||||||
/tmp
|
/tmp
|
||||||
|
|
||||||
ktlint
|
ktlint
|
||||||
|
.idea/copyright/New_vector.xml
|
||||||
|
.idea/copyright/profiles_settings.xml
|
||||||
|
|
|
@ -8,6 +8,8 @@ Improvements:
|
||||||
- Persist active tab between sessions (#503)
|
- Persist active tab between sessions (#503)
|
||||||
- Do not upload file too big for the homeserver (#587)
|
- Do not upload file too big for the homeserver (#587)
|
||||||
- Handle read markers (#84)
|
- Handle read markers (#84)
|
||||||
|
- Attachments: start using system pickers (#52)
|
||||||
|
- Attachments: start handling incoming share (#58)
|
||||||
- Mark all messages as read (#396)
|
- Mark all messages as read (#396)
|
||||||
- Add ability to report content (#515)
|
- Add ability to report content (#515)
|
||||||
|
|
||||||
|
|
|
@ -316,7 +316,7 @@ dependencies {
|
||||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||||
|
|
||||||
// File picker
|
// File picker
|
||||||
implementation 'com.github.jaiselrahman:FilePicker:1.2.2'
|
implementation 'com.kbeanie:multipicker:1.6@aar'
|
||||||
|
|
||||||
// DI
|
// DI
|
||||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package="im.vector.riotx">
|
package="im.vector.riotx">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".VectorApplication"
|
android:name=".VectorApplication"
|
||||||
|
@ -79,6 +80,22 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".features.share.IncomingShareActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.OPENABLE" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.OPENABLE" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
|
|
@ -339,11 +339,6 @@ SOFTWARE.
|
||||||
<br/>
|
<br/>
|
||||||
Copyright 2014 Leo Lin
|
Copyright 2014 Leo Lin
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<b>FilePicker</b>
|
|
||||||
<br/>
|
|
||||||
Copyright (c) 2018, Jaisel Rahman
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<b>diff-match-patch</b>
|
<b>diff-match-patch</b>
|
||||||
<br/>
|
<br/>
|
||||||
|
@ -359,6 +354,11 @@ SOFTWARE.
|
||||||
<br/>
|
<br/>
|
||||||
Copyright 2017 Gabriel Ittner.
|
Copyright 2017 Gabriel Ittner.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Android-multipicker-library</b>
|
||||||
|
<br/>
|
||||||
|
Copyright 2018 Kumar Bibek
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<pre>
|
<pre>
|
||||||
Apache License
|
Apache License
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
import arrow.core.Option
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.riotx.core.utils.RxStore
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ActiveSessionObservableStore @Inject constructor() : RxStore<Option<Session>>()
|
|
@ -14,56 +14,59 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home
|
package im.vector.riotx
|
||||||
|
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleObserver
|
||||||
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
import arrow.core.Option
|
import arrow.core.Option
|
||||||
import com.airbnb.mvrx.ActivityViewModelContext
|
|
||||||
import com.airbnb.mvrx.MvRxState
|
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
|
||||||
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.group.model.GroupSummary
|
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.features.home.HomeRoomListObservableStore
|
||||||
import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID
|
import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID
|
||||||
import im.vector.riotx.features.home.group.SelectedGroupStore
|
import im.vector.riotx.features.home.group.SelectedGroupStore
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
import io.reactivex.functions.BiFunction
|
import io.reactivex.functions.BiFunction
|
||||||
|
import io.reactivex.rxkotlin.addTo
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
data class EmptyState(val isEmpty: Boolean = true) : MvRxState
|
/**
|
||||||
|
* This class handles the global app state. At the moment, it only manages room list.
|
||||||
|
* It requires to be added to ProcessLifecycleOwner.get().lifecycle
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class AppStateHandler @Inject constructor(
|
||||||
|
private val sessionObservableStore: ActiveSessionObservableStore,
|
||||||
|
private val homeRoomListStore: HomeRoomListObservableStore,
|
||||||
|
private val selectedGroupStore: SelectedGroupStore) : LifecycleObserver {
|
||||||
|
|
||||||
class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: EmptyState,
|
private val compositeDisposable = CompositeDisposable()
|
||||||
private val session: Session,
|
|
||||||
private val selectedGroupStore: SelectedGroupStore,
|
|
||||||
private val homeRoomListStore: HomeRoomListObservableStore
|
|
||||||
) : VectorViewModel<EmptyState>(initialState) {
|
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||||
interface Factory {
|
fun entersForeground() {
|
||||||
fun create(initialState: EmptyState): HomeActivityViewModel
|
observeRoomsAndGroup()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> {
|
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||||
|
fun entersBackground() {
|
||||||
@JvmStatic
|
compositeDisposable.clear()
|
||||||
override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
|
|
||||||
val homeActivity: HomeActivity = (viewModelContext as ActivityViewModelContext).activity()
|
|
||||||
return homeActivity.homeActivityViewModelFactory.create(state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
private fun observeRoomsAndGroup() {
|
||||||
observeRoomAndGroup()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeRoomAndGroup() {
|
|
||||||
Observable
|
Observable
|
||||||
.combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>(
|
.combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>(
|
||||||
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
|
sessionObservableStore.observe()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.switchMap {
|
||||||
|
it.orNull()?.rx()?.liveRoomSummaries()
|
||||||
|
?: Observable.just(emptyList())
|
||||||
|
}
|
||||||
|
.throttleLast(300, TimeUnit.MILLISECONDS),
|
||||||
selectedGroupStore.observe(),
|
selectedGroupStore.observe(),
|
||||||
BiFunction { rooms, selectedGroupOption ->
|
BiFunction { rooms, selectedGroupOption ->
|
||||||
val selectedGroup = selectedGroupOption.orNull()
|
val selectedGroup = selectedGroupOption.orNull()
|
||||||
|
@ -91,6 +94,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
|
||||||
.subscribe {
|
.subscribe {
|
||||||
homeRoomListStore.post(it)
|
homeRoomListStore.post(it)
|
||||||
}
|
}
|
||||||
.disposeOnClear()
|
.addTo(compositeDisposable)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -74,6 +74,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||||
@Inject lateinit var vectorPreferences: VectorPreferences
|
@Inject lateinit var vectorPreferences: VectorPreferences
|
||||||
@Inject lateinit var versionProvider: VersionProvider
|
@Inject lateinit var versionProvider: VersionProvider
|
||||||
@Inject lateinit var notificationUtils: NotificationUtils
|
@Inject lateinit var notificationUtils: NotificationUtils
|
||||||
|
@Inject lateinit var appStateHandler: AppStateHandler
|
||||||
lateinit var vectorComponent: VectorComponent
|
lateinit var vectorComponent: VectorComponent
|
||||||
private var fontThreadHandler: Handler? = null
|
private var fontThreadHandler: Handler? = null
|
||||||
|
|
||||||
|
@ -134,6 +135,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||||
FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
|
FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
|
||||||
// This should be done as early as possible
|
// This should be done as early as possible
|
||||||
initKnownEmojiHashSet(appContext)
|
initKnownEmojiHashSet(appContext)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,10 @@
|
||||||
|
|
||||||
package im.vector.riotx.core.di
|
package im.vector.riotx.core.di
|
||||||
|
|
||||||
|
import arrow.core.Option
|
||||||
import im.vector.matrix.android.api.auth.Authenticator
|
import im.vector.matrix.android.api.auth.Authenticator
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.riotx.ActiveSessionObservableStore
|
||||||
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
||||||
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
|
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
@ -26,6 +28,7 @@ import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator,
|
class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator,
|
||||||
|
private val sessionObservableStore: ActiveSessionObservableStore,
|
||||||
private val keyRequestHandler: KeyRequestHandler,
|
private val keyRequestHandler: KeyRequestHandler,
|
||||||
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
|
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
|
||||||
) {
|
) {
|
||||||
|
@ -34,12 +37,14 @@ class ActiveSessionHolder @Inject constructor(private val authenticator: Authent
|
||||||
|
|
||||||
fun setActiveSession(session: Session) {
|
fun setActiveSession(session: Session) {
|
||||||
activeSession.set(session)
|
activeSession.set(session)
|
||||||
|
sessionObservableStore.post(Option.fromNullable(session))
|
||||||
keyRequestHandler.start(session)
|
keyRequestHandler.start(session)
|
||||||
incomingVerificationRequestHandler.start(session)
|
incomingVerificationRequestHandler.start(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearActiveSession() {
|
fun clearActiveSession() {
|
||||||
activeSession.set(null)
|
activeSession.set(null)
|
||||||
|
sessionObservableStore.post(Option.empty())
|
||||||
keyRequestHandler.stop()
|
keyRequestHandler.stop()
|
||||||
incomingVerificationRequestHandler.stop()
|
incomingVerificationRequestHandler.stop()
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,7 @@ import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
|
||||||
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
|
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
|
||||||
import im.vector.riotx.features.settings.*
|
import im.vector.riotx.features.settings.*
|
||||||
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
||||||
|
import im.vector.riotx.features.share.IncomingShareActivity
|
||||||
import im.vector.riotx.features.ui.UiStateRepository
|
import im.vector.riotx.features.ui.UiStateRepository
|
||||||
|
|
||||||
@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
|
@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
|
||||||
|
@ -183,6 +184,8 @@ interface ScreenComponent {
|
||||||
|
|
||||||
fun inject(reactionButton: ReactionButton)
|
fun inject(reactionButton: ReactionButton)
|
||||||
|
|
||||||
|
fun inject(incomingShareActivity: IncomingShareActivity)
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(vectorComponent: VectorComponent,
|
fun create(vectorComponent: VectorComponent,
|
||||||
|
|
|
@ -48,3 +48,10 @@ fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) {
|
||||||
}
|
}
|
||||||
if (updateCursor) setSelection(text?.length ?: 0)
|
if (updateCursor) setSelection(text?.length ?: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun View.getMeasurements(): Pair<Int, Int> {
|
||||||
|
measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
val width = measuredWidth
|
||||||
|
val height = measuredHeight
|
||||||
|
return width to height
|
||||||
|
}
|
||||||
|
|
|
@ -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.core.utils
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
|
|
||||||
|
class KeyboardStateUtils(activity: Activity) : ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
|
||||||
|
private val contentView: View = activity.findViewById<View>(android.R.id.content).also {
|
||||||
|
it.viewTreeObserver.addOnGlobalLayoutListener(this)
|
||||||
|
}
|
||||||
|
var isKeyboardShowing: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun onGlobalLayout() {
|
||||||
|
val rect = Rect()
|
||||||
|
contentView.getWindowVisibleDisplayFrame(rect)
|
||||||
|
val screenHeight = contentView.rootView.height
|
||||||
|
|
||||||
|
val keypadHeight = screenHeight - rect.bottom
|
||||||
|
isKeyboardShowing = keypadHeight > screenHeight * 0.15
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -51,8 +52,9 @@ const val PERMISSIONS_FOR_MEMBER_DETAILS = PERMISSION_READ_CONTACTS
|
||||||
const val PERMISSIONS_FOR_ROOM_AVATAR = PERMISSION_CAMERA
|
const val PERMISSIONS_FOR_ROOM_AVATAR = PERMISSION_CAMERA
|
||||||
const val PERMISSIONS_FOR_VIDEO_RECORDING = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO
|
const val PERMISSIONS_FOR_VIDEO_RECORDING = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO
|
||||||
const val PERMISSIONS_FOR_WRITING_FILES = PERMISSION_WRITE_EXTERNAL_STORAGE
|
const val PERMISSIONS_FOR_WRITING_FILES = PERMISSION_WRITE_EXTERNAL_STORAGE
|
||||||
|
const val PERMISSIONS_FOR_PICKING_CONTACT = PERMISSION_READ_CONTACTS
|
||||||
|
|
||||||
private const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED
|
const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED
|
||||||
|
|
||||||
// Request code to ask permission to the system (arbitrary values)
|
// Request code to ask permission to the system (arbitrary values)
|
||||||
const val PERMISSION_REQUEST_CODE = 567
|
const val PERMISSION_REQUEST_CODE = 567
|
||||||
|
@ -64,6 +66,7 @@ const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572
|
||||||
const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
|
const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
|
||||||
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
|
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
|
||||||
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
|
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
|
||||||
|
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log the used permissions statuses.
|
* Log the used permissions statuses.
|
||||||
|
@ -98,8 +101,9 @@ fun logPermissionStatuses(context: Context) {
|
||||||
*/
|
*/
|
||||||
fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
requestCode: Int = PERMISSION_REQUEST_CODE): Boolean {
|
requestCode: Int,
|
||||||
return checkPermissions(permissionsToBeGrantedBitMap, activity, null, requestCode)
|
@StringRes rationaleMessage: Int = 0): Boolean {
|
||||||
|
return checkPermissions(permissionsToBeGrantedBitMap, activity, null, requestCode, rationaleMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -111,8 +115,9 @@ fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||||
*/
|
*/
|
||||||
fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||||
fragment: Fragment,
|
fragment: Fragment,
|
||||||
requestCode: Int = PERMISSION_REQUEST_CODE): Boolean {
|
requestCode: Int,
|
||||||
return checkPermissions(permissionsToBeGrantedBitMap, fragment.activity, fragment, requestCode)
|
@StringRes rationaleMessage: Int = 0): Boolean {
|
||||||
|
return checkPermissions(permissionsToBeGrantedBitMap, fragment.activity, fragment, requestCode, rationaleMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -136,7 +141,9 @@ fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||||
private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||||
activity: Activity?,
|
activity: Activity?,
|
||||||
fragment: Fragment?,
|
fragment: Fragment?,
|
||||||
requestCode: Int): Boolean {
|
requestCode: Int,
|
||||||
|
@StringRes rationaleMessage: Int
|
||||||
|
): Boolean {
|
||||||
var isPermissionGranted = false
|
var isPermissionGranted = false
|
||||||
|
|
||||||
// sanity check
|
// sanity check
|
||||||
|
@ -159,7 +166,6 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||||
val permissionListAlreadyDenied = ArrayList<String>()
|
val permissionListAlreadyDenied = ArrayList<String>()
|
||||||
val permissionsListToBeGranted = ArrayList<String>()
|
val permissionsListToBeGranted = ArrayList<String>()
|
||||||
var isRequestPermissionRequired = false
|
var isRequestPermissionRequired = false
|
||||||
var explanationMessage = ""
|
|
||||||
|
|
||||||
// retrieve the permissions to be granted according to the request code bit map
|
// retrieve the permissions to be granted according to the request code bit map
|
||||||
if (PERMISSION_CAMERA == permissionsToBeGrantedBitMap and PERMISSION_CAMERA) {
|
if (PERMISSION_CAMERA == permissionsToBeGrantedBitMap and PERMISSION_CAMERA) {
|
||||||
|
@ -199,58 +205,11 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||||
}
|
}
|
||||||
|
|
||||||
// if some permissions were already denied: display a dialog to the user before asking again.
|
// if some permissions were already denied: display a dialog to the user before asking again.
|
||||||
if (!permissionListAlreadyDenied.isEmpty()) {
|
if (permissionListAlreadyDenied.isNotEmpty() && rationaleMessage != 0) {
|
||||||
if (permissionsToBeGrantedBitMap == PERMISSIONS_FOR_VIDEO_IP_CALL || permissionsToBeGrantedBitMap == PERMISSIONS_FOR_AUDIO_IP_CALL) {
|
|
||||||
// Permission request for VOIP call
|
|
||||||
if (permissionListAlreadyDenied.contains(Manifest.permission.CAMERA)
|
|
||||||
&& permissionListAlreadyDenied.contains(Manifest.permission.RECORD_AUDIO)) {
|
|
||||||
// Both missing
|
|
||||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera_and_audio)
|
|
||||||
} else if (permissionListAlreadyDenied.contains(Manifest.permission.RECORD_AUDIO)) {
|
|
||||||
// Audio missing
|
|
||||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio)
|
|
||||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio_explanation)
|
|
||||||
} else if (permissionListAlreadyDenied.contains(Manifest.permission.CAMERA)) {
|
|
||||||
// Camera missing
|
|
||||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera)
|
|
||||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera_explanation)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
permissionListAlreadyDenied.forEach {
|
|
||||||
when (it) {
|
|
||||||
Manifest.permission.CAMERA -> {
|
|
||||||
if (explanationMessage.isNotEmpty()) {
|
|
||||||
explanationMessage += "\n\n"
|
|
||||||
}
|
|
||||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera)
|
|
||||||
}
|
|
||||||
Manifest.permission.RECORD_AUDIO -> {
|
|
||||||
if (explanationMessage.isNotEmpty()) {
|
|
||||||
explanationMessage += "\n\n"
|
|
||||||
}
|
|
||||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio)
|
|
||||||
}
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE -> {
|
|
||||||
if (explanationMessage.isNotEmpty()) {
|
|
||||||
explanationMessage += "\n\n"
|
|
||||||
}
|
|
||||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_storage)
|
|
||||||
}
|
|
||||||
Manifest.permission.READ_CONTACTS -> {
|
|
||||||
if (!explanationMessage.isEmpty()) {
|
|
||||||
explanationMessage += "\n\n"
|
|
||||||
}
|
|
||||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_contacts)
|
|
||||||
}
|
|
||||||
else -> Timber.v("## checkPermissions(): already denied permission not supported")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// display the dialog with the info text
|
// display the dialog with the info text
|
||||||
AlertDialog.Builder(activity)
|
AlertDialog.Builder(activity)
|
||||||
.setTitle(R.string.permissions_rationale_popup_title)
|
.setTitle(R.string.permissions_rationale_popup_title)
|
||||||
.setMessage(explanationMessage)
|
.setMessage(rationaleMessage)
|
||||||
.setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() }
|
.setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() }
|
||||||
.setPositiveButton(R.string.ok) { _, _ ->
|
.setPositiveButton(R.string.ok) { _, _ ->
|
||||||
if (permissionsListToBeGranted.isNotEmpty()) {
|
if (permissionsListToBeGranted.isNotEmpty()) {
|
||||||
|
|
|
@ -0,0 +1,233 @@
|
||||||
|
/*
|
||||||
|
* 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.attachments
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Pair
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewAnimationUtils
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.AnimationSet
|
||||||
|
import android.view.animation.OvershootInterpolator
|
||||||
|
import android.view.animation.ScaleAnimation
|
||||||
|
import android.view.animation.TranslateAnimation
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.PopupWindow
|
||||||
|
import androidx.core.view.doOnNextLayout
|
||||||
|
import com.amulyakhare.textdrawable.TextDrawable
|
||||||
|
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.getMeasurements
|
||||||
|
import im.vector.riotx.core.utils.PERMISSIONS_EMPTY
|
||||||
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
|
||||||
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
private const val ANIMATION_DURATION = 250
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is the view presenting choices for picking attachments.
|
||||||
|
* It will return result through [Callback].
|
||||||
|
*/
|
||||||
|
class AttachmentTypeSelectorView(context: Context,
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
var callback: Callback?)
|
||||||
|
: PopupWindow(context) {
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onTypeSelected(type: Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val iconColorGenerator = ColorGenerator.MATERIAL
|
||||||
|
|
||||||
|
private var galleryButton: ImageButton
|
||||||
|
private var cameraButton: ImageButton
|
||||||
|
private var fileButton: ImageButton
|
||||||
|
private var stickersButton: ImageButton
|
||||||
|
private var audioButton: ImageButton
|
||||||
|
private var contactButton: ImageButton
|
||||||
|
|
||||||
|
private var anchor: View? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
val root = FrameLayout(context)
|
||||||
|
val layout = inflater.inflate(R.layout.view_attachment_type_selector, root, true)
|
||||||
|
galleryButton = layout.findViewById<ImageButton>(R.id.attachmentGalleryButton).configure(Type.GALLERY)
|
||||||
|
cameraButton = layout.findViewById<ImageButton>(R.id.attachmentCameraButton).configure(Type.CAMERA)
|
||||||
|
fileButton = layout.findViewById<ImageButton>(R.id.attachmentFileButton).configure(Type.FILE)
|
||||||
|
stickersButton = layout.findViewById<ImageButton>(R.id.attachmentStickersButton).configure(Type.STICKER)
|
||||||
|
audioButton = layout.findViewById<ImageButton>(R.id.attachmentAudioButton).configure(Type.AUDIO)
|
||||||
|
contactButton = layout.findViewById<ImageButton>(R.id.attachmentContactButton).configure(Type.CONTACT)
|
||||||
|
contentView = root
|
||||||
|
width = LinearLayout.LayoutParams.MATCH_PARENT
|
||||||
|
height = LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
animationStyle = 0
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
setBackgroundDrawable(BitmapDrawable())
|
||||||
|
inputMethodMode = INPUT_METHOD_NOT_NEEDED
|
||||||
|
isFocusable = true
|
||||||
|
isTouchable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(anchor: View, isKeyboardOpen: Boolean) {
|
||||||
|
this.anchor = anchor
|
||||||
|
val anchorCoordinates = IntArray(2)
|
||||||
|
anchor.getLocationOnScreen(anchorCoordinates)
|
||||||
|
if (isKeyboardOpen) {
|
||||||
|
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height)
|
||||||
|
} else {
|
||||||
|
val contentViewHeight = if (contentView.height == 0) {
|
||||||
|
contentView.getMeasurements().second
|
||||||
|
} else {
|
||||||
|
contentView.height
|
||||||
|
}
|
||||||
|
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight)
|
||||||
|
}
|
||||||
|
contentView.doOnNextLayout {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
animateWindowInCircular(anchor, contentView)
|
||||||
|
} else {
|
||||||
|
animateWindowInTranslate(contentView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
animateButtonIn(galleryButton, ANIMATION_DURATION / 2)
|
||||||
|
animateButtonIn(cameraButton, ANIMATION_DURATION / 2)
|
||||||
|
animateButtonIn(fileButton, ANIMATION_DURATION / 4)
|
||||||
|
animateButtonIn(audioButton, ANIMATION_DURATION / 2)
|
||||||
|
animateButtonIn(contactButton, ANIMATION_DURATION / 4)
|
||||||
|
animateButtonIn(stickersButton, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismiss() {
|
||||||
|
val capturedAnchor = anchor
|
||||||
|
if (capturedAnchor != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
animateWindowOutCircular(capturedAnchor, contentView)
|
||||||
|
} else {
|
||||||
|
animateWindowOutTranslate(contentView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateButtonIn(button: View, delay: Int) {
|
||||||
|
val animation = AnimationSet(true)
|
||||||
|
val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f)
|
||||||
|
animation.addAnimation(scale)
|
||||||
|
animation.interpolator = OvershootInterpolator(1f)
|
||||||
|
animation.duration = ANIMATION_DURATION.toLong()
|
||||||
|
animation.startOffset = delay.toLong()
|
||||||
|
button.startAnimation(animation)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
private fun animateWindowInCircular(anchor: View, contentView: View) {
|
||||||
|
val coordinates = getClickCoordinates(anchor, contentView)
|
||||||
|
val animator = ViewAnimationUtils.createCircularReveal(contentView,
|
||||||
|
coordinates.first,
|
||||||
|
coordinates.second,
|
||||||
|
0f,
|
||||||
|
max(contentView.width, contentView.height).toFloat())
|
||||||
|
animator.duration = ANIMATION_DURATION.toLong()
|
||||||
|
animator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateWindowInTranslate(contentView: View) {
|
||||||
|
val animation = TranslateAnimation(0f, 0f, contentView.height.toFloat(), 0f)
|
||||||
|
animation.duration = ANIMATION_DURATION.toLong()
|
||||||
|
getContentView().startAnimation(animation)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
private fun animateWindowOutCircular(anchor: View, contentView: View) {
|
||||||
|
val coordinates = getClickCoordinates(anchor, contentView)
|
||||||
|
val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
|
||||||
|
coordinates.first,
|
||||||
|
coordinates.second,
|
||||||
|
max(getContentView().width, getContentView().height).toFloat(),
|
||||||
|
0f)
|
||||||
|
|
||||||
|
animator.duration = ANIMATION_DURATION.toLong()
|
||||||
|
animator.addListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
super@AttachmentTypeSelectorView.dismiss()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
animator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateWindowOutTranslate(contentView: View) {
|
||||||
|
val animation = TranslateAnimation(0f, 0f, 0f, (contentView.top + contentView.height).toFloat())
|
||||||
|
animation.duration = ANIMATION_DURATION.toLong()
|
||||||
|
animation.setAnimationListener(object : Animation.AnimationListener {
|
||||||
|
override fun onAnimationStart(animation: Animation) {}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animation) {
|
||||||
|
super@AttachmentTypeSelectorView.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(animation: Animation) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
getContentView().startAnimation(animation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getClickCoordinates(anchor: View, contentView: View): Pair<Int, Int> {
|
||||||
|
val anchorCoordinates = IntArray(2)
|
||||||
|
anchor.getLocationOnScreen(anchorCoordinates)
|
||||||
|
val contentCoordinates = IntArray(2)
|
||||||
|
contentView.getLocationOnScreen(contentCoordinates)
|
||||||
|
val x = anchorCoordinates[0] - contentCoordinates[0] + anchor.width / 2
|
||||||
|
val y = anchorCoordinates[1] - contentCoordinates[1]
|
||||||
|
return Pair(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ImageButton.configure(type: Type): ImageButton {
|
||||||
|
this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal))
|
||||||
|
this.setOnClickListener(TypeClickListener(type))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class TypeClickListener(private val type: Type) : View.OnClickListener {
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
dismiss()
|
||||||
|
callback?.onTypeSelected(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The all possible types to pick with their required permissions.
|
||||||
|
*/
|
||||||
|
enum class Type(val permissionsBit: Int) {
|
||||||
|
|
||||||
|
CAMERA(PERMISSIONS_EMPTY),
|
||||||
|
GALLERY(PERMISSIONS_FOR_WRITING_FILES),
|
||||||
|
FILE(PERMISSIONS_FOR_WRITING_FILES),
|
||||||
|
STICKER(PERMISSIONS_EMPTY),
|
||||||
|
AUDIO(PERMISSIONS_FOR_WRITING_FILES),
|
||||||
|
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
/*
|
||||||
|
* 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.attachments
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.kbeanie.multipicker.api.Picker.*
|
||||||
|
import com.kbeanie.multipicker.core.PickerManager
|
||||||
|
import im.vector.matrix.android.BuildConfig
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import im.vector.riotx.core.platform.Restorable
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
|
||||||
|
private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class helps to handle attachments by providing simple methods.
|
||||||
|
* The process is asynchronous and you must implement [Callback] methods to get the data or a failure.
|
||||||
|
*/
|
||||||
|
class AttachmentsHelper private constructor(private val context: Context,
|
||||||
|
private val pickerManagerFactory: PickerManagerFactory) : Restorable {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(fragment: Fragment, callback: Callback): AttachmentsHelper {
|
||||||
|
return AttachmentsHelper(fragment.requireContext(), FragmentPickerManagerFactory(fragment, callback))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(activity: Activity, callback: Callback): AttachmentsHelper {
|
||||||
|
return AttachmentsHelper(activity, ActivityPickerManagerFactory(activity, callback))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
||||||
|
if (BuildConfig.LOG_PRIVATE_DATA) {
|
||||||
|
Timber.v("On contact attachment ready: $contactAttachment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>)
|
||||||
|
fun onAttachmentsProcessFailed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture path allows to handle camera image picking. It must be restored if the activity gets killed.
|
||||||
|
private var capturePath: String? = null
|
||||||
|
// The pending type is set if we have to handle permission request. It must be restored if the activity gets killed.
|
||||||
|
var pendingType: AttachmentTypeSelectorView.Type? = null
|
||||||
|
|
||||||
|
private val imagePicker by lazy {
|
||||||
|
pickerManagerFactory.createImagePicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val videoPicker by lazy {
|
||||||
|
pickerManagerFactory.createVideoPicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cameraImagePicker by lazy {
|
||||||
|
pickerManagerFactory.createCameraImagePicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val filePicker by lazy {
|
||||||
|
pickerManagerFactory.createFilePicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val audioPicker by lazy {
|
||||||
|
pickerManagerFactory.createAudioPicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val contactPicker by lazy {
|
||||||
|
pickerManagerFactory.createContactPicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restorable
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
capturePath?.also {
|
||||||
|
outState.putString(CAPTURE_PATH_KEY, it)
|
||||||
|
}
|
||||||
|
pendingType?.also {
|
||||||
|
outState.putSerializable(PENDING_TYPE_KEY, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
|
||||||
|
capturePath = savedInstanceState?.getString(CAPTURE_PATH_KEY)
|
||||||
|
if (capturePath != null) {
|
||||||
|
cameraImagePicker.reinitialize(capturePath)
|
||||||
|
}
|
||||||
|
pendingType = savedInstanceState?.getSerializable(PENDING_TYPE_KEY) as? AttachmentTypeSelectorView.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public Methods
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the process for handling file picking
|
||||||
|
*/
|
||||||
|
fun selectFile() {
|
||||||
|
filePicker.pickFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the process for handling image picking
|
||||||
|
*/
|
||||||
|
fun selectGallery() {
|
||||||
|
imagePicker.pickImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the process for handling audio picking
|
||||||
|
*/
|
||||||
|
fun selectAudio() {
|
||||||
|
audioPicker.pickAudio()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the process for handling capture image picking
|
||||||
|
*/
|
||||||
|
fun openCamera() {
|
||||||
|
capturePath = cameraImagePicker.pickImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the process for handling contact picking
|
||||||
|
*/
|
||||||
|
fun selectContact() {
|
||||||
|
contactPicker.pickContact()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This methods aims to handle on activity result data.
|
||||||
|
*
|
||||||
|
* @return true if it can handle the data, false otherwise
|
||||||
|
*/
|
||||||
|
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
val pickerManager = getPickerManagerForRequestCode(requestCode)
|
||||||
|
if (pickerManager != null) {
|
||||||
|
pickerManager.submit(data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This methods aims to handle share intent.
|
||||||
|
*
|
||||||
|
* @return true if it can handle the intent data, false otherwise
|
||||||
|
*/
|
||||||
|
fun handleShareIntent(intent: Intent): Boolean {
|
||||||
|
val type = intent.resolveType(context) ?: return false
|
||||||
|
if (type.startsWith("image")) {
|
||||||
|
imagePicker.submit(intent)
|
||||||
|
} else if (type.startsWith("video")) {
|
||||||
|
videoPicker.submit(intent)
|
||||||
|
} else if (type.startsWith("audio")) {
|
||||||
|
videoPicker.submit(intent)
|
||||||
|
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
|
||||||
|
filePicker.submit(intent)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPickerManagerForRequestCode(requestCode: Int): PickerManager? {
|
||||||
|
return when (requestCode) {
|
||||||
|
PICK_IMAGE_DEVICE -> imagePicker
|
||||||
|
PICK_IMAGE_CAMERA -> cameraImagePicker
|
||||||
|
PICK_FILE -> filePicker
|
||||||
|
PICK_CONTACT -> contactPicker
|
||||||
|
PICK_AUDIO -> audioPicker
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* 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.attachments
|
||||||
|
|
||||||
|
import com.kbeanie.multipicker.api.entity.*
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
|
||||||
|
fun ChosenContact.toContactAttachment(): ContactAttachment {
|
||||||
|
return ContactAttachment(
|
||||||
|
displayName = displayName,
|
||||||
|
photoUri = photoUri,
|
||||||
|
emails = emails.toList(),
|
||||||
|
phones = phones.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
|
||||||
|
return ContentAttachmentData(
|
||||||
|
path = originalPath,
|
||||||
|
mimeType = mimeType,
|
||||||
|
type = mapType(),
|
||||||
|
size = size,
|
||||||
|
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||||
|
name = displayName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
|
||||||
|
return ContentAttachmentData(
|
||||||
|
path = originalPath,
|
||||||
|
mimeType = mimeType,
|
||||||
|
type = mapType(),
|
||||||
|
size = size,
|
||||||
|
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||||
|
name = displayName,
|
||||||
|
duration = duration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
|
||||||
|
return ContentAttachmentData(
|
||||||
|
path = originalPath,
|
||||||
|
mimeType = mimeType,
|
||||||
|
type = mapType(),
|
||||||
|
name = displayName,
|
||||||
|
size = size,
|
||||||
|
height = height.toLong(),
|
||||||
|
width = width.toLong(),
|
||||||
|
exifOrientation = orientation,
|
||||||
|
date = createdAt?.time ?: System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
|
||||||
|
return ContentAttachmentData(
|
||||||
|
path = originalPath,
|
||||||
|
mimeType = mimeType,
|
||||||
|
type = ContentAttachmentData.Type.VIDEO,
|
||||||
|
size = size,
|
||||||
|
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||||
|
height = height.toLong(),
|
||||||
|
width = width.toLong(),
|
||||||
|
duration = duration,
|
||||||
|
name = displayName
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* 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.attachments
|
||||||
|
|
||||||
|
import com.kbeanie.multipicker.api.callbacks.AudioPickerCallback
|
||||||
|
import com.kbeanie.multipicker.api.callbacks.ContactPickerCallback
|
||||||
|
import com.kbeanie.multipicker.api.callbacks.FilePickerCallback
|
||||||
|
import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
|
||||||
|
import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback
|
||||||
|
import com.kbeanie.multipicker.api.entity.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback]
|
||||||
|
*/
|
||||||
|
class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback)
|
||||||
|
: ImagePickerCallback,
|
||||||
|
FilePickerCallback,
|
||||||
|
VideoPickerCallback,
|
||||||
|
AudioPickerCallback,
|
||||||
|
ContactPickerCallback {
|
||||||
|
|
||||||
|
override fun onContactChosen(contact: ChosenContact?) {
|
||||||
|
if (contact == null) {
|
||||||
|
callback.onAttachmentsProcessFailed()
|
||||||
|
} else {
|
||||||
|
val contactAttachment = contact.toContactAttachment()
|
||||||
|
callback.onContactAttachmentReady(contactAttachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAudiosChosen(audios: MutableList<ChosenAudio>?) {
|
||||||
|
if (audios.isNullOrEmpty()) {
|
||||||
|
callback.onAttachmentsProcessFailed()
|
||||||
|
} else {
|
||||||
|
val attachments = audios.map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
callback.onContentAttachmentsReady(attachments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFilesChosen(files: MutableList<ChosenFile>?) {
|
||||||
|
if (files.isNullOrEmpty()) {
|
||||||
|
callback.onAttachmentsProcessFailed()
|
||||||
|
} else {
|
||||||
|
val attachments = files.map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
callback.onContentAttachmentsReady(attachments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onImagesChosen(images: MutableList<ChosenImage>?) {
|
||||||
|
if (images.isNullOrEmpty()) {
|
||||||
|
callback.onAttachmentsProcessFailed()
|
||||||
|
} else {
|
||||||
|
val attachments = images.map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
callback.onContentAttachmentsReady(attachments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onVideosChosen(videos: MutableList<ChosenVideo>?) {
|
||||||
|
if (videos.isNullOrEmpty()) {
|
||||||
|
callback.onAttachmentsProcessFailed()
|
||||||
|
} else {
|
||||||
|
val attachments = videos.map {
|
||||||
|
it.toContentAttachmentData()
|
||||||
|
}
|
||||||
|
callback.onContentAttachmentsReady(attachments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(error: String?) {
|
||||||
|
callback.onAttachmentsProcessFailed()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* 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.attachments
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class holding values of a picked contact
|
||||||
|
* Can be send as a text message waiting for the protocol to handle contact.
|
||||||
|
*/
|
||||||
|
data class ContactAttachment(
|
||||||
|
val displayName: String,
|
||||||
|
val photoUri: String?,
|
||||||
|
val phones: List<String> = emptyList(),
|
||||||
|
val emails: List<String> = emptyList()
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun toHumanReadable(): String {
|
||||||
|
return buildString {
|
||||||
|
append(displayName)
|
||||||
|
phones.concatIn(this)
|
||||||
|
emails.concatIn(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<String>.concatIn(stringBuilder: StringBuilder) {
|
||||||
|
if (isNotEmpty()) {
|
||||||
|
stringBuilder.append("\n")
|
||||||
|
for (i in 0 until size - 1) {
|
||||||
|
val value = get(i)
|
||||||
|
stringBuilder.append(value).append("\n")
|
||||||
|
}
|
||||||
|
stringBuilder.append(last())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
* 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.attachments
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.kbeanie.multipicker.api.AudioPicker
|
||||||
|
import com.kbeanie.multipicker.api.CameraImagePicker
|
||||||
|
import com.kbeanie.multipicker.api.ContactPicker
|
||||||
|
import com.kbeanie.multipicker.api.FilePicker
|
||||||
|
import com.kbeanie.multipicker.api.ImagePicker
|
||||||
|
import com.kbeanie.multipicker.api.VideoPicker
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for creating different pickers. It allows to use with fragment or activity builders.
|
||||||
|
*/
|
||||||
|
interface PickerManagerFactory {
|
||||||
|
|
||||||
|
fun createImagePicker(): ImagePicker
|
||||||
|
|
||||||
|
fun createCameraImagePicker(): CameraImagePicker
|
||||||
|
|
||||||
|
fun createVideoPicker(): VideoPicker
|
||||||
|
|
||||||
|
fun createFilePicker(): FilePicker
|
||||||
|
|
||||||
|
fun createAudioPicker(): AudioPicker
|
||||||
|
|
||||||
|
fun createContactPicker(): ContactPicker
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActivityPickerManagerFactory(private val activity: Activity, callback: AttachmentsHelper.Callback) : PickerManagerFactory {
|
||||||
|
|
||||||
|
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
|
||||||
|
|
||||||
|
override fun createImagePicker(): ImagePicker {
|
||||||
|
return ImagePicker(activity).also {
|
||||||
|
it.setImagePickerCallback(attachmentsPickerCallback)
|
||||||
|
it.allowMultiple()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCameraImagePicker(): CameraImagePicker {
|
||||||
|
return CameraImagePicker(activity).also {
|
||||||
|
it.setImagePickerCallback(attachmentsPickerCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createVideoPicker(): VideoPicker {
|
||||||
|
return VideoPicker(activity).also {
|
||||||
|
it.setVideoPickerCallback(attachmentsPickerCallback)
|
||||||
|
it.allowMultiple()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFilePicker(): FilePicker {
|
||||||
|
return FilePicker(activity).also {
|
||||||
|
it.allowMultiple()
|
||||||
|
it.setFilePickerCallback(attachmentsPickerCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAudioPicker(): AudioPicker {
|
||||||
|
return AudioPicker(activity).also {
|
||||||
|
it.allowMultiple()
|
||||||
|
it.setAudioPickerCallback(attachmentsPickerCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createContactPicker(): ContactPicker {
|
||||||
|
return ContactPicker(activity).also {
|
||||||
|
it.setContactPickerCallback(attachmentsPickerCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FragmentPickerManagerFactory(private val fragment: Fragment, callback: AttachmentsHelper.Callback) : PickerManagerFactory {
|
||||||
|
|
||||||
|
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
|
||||||
|
|
||||||
|
override fun createImagePicker(): ImagePicker {
|
||||||
|
return ImagePicker(fragment).also {
|
||||||
|
it.setImagePickerCallback(attachmentsPickerCallback)
|
||||||
|
it.allowMultiple()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createCameraImagePicker(): CameraImagePicker {
|
||||||
|
return CameraImagePicker(fragment).also {
|
||||||
|
it.setImagePickerCallback(attachmentsPickerCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createVideoPicker(): VideoPicker {
|
||||||
|
return VideoPicker(fragment).also {
|
||||||
|
it.setVideoPickerCallback(attachmentsPickerCallback)
|
||||||
|
it.allowMultiple()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFilePicker(): FilePicker {
|
||||||
|
return FilePicker(fragment).also {
|
||||||
|
it.allowMultiple()
|
||||||
|
it.setFilePickerCallback(attachmentsPickerCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createAudioPicker(): AudioPicker {
|
||||||
|
return AudioPicker(fragment).also {
|
||||||
|
it.allowMultiple()
|
||||||
|
it.setAudioPickerCallback(attachmentsPickerCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createContactPicker(): ContactPicker {
|
||||||
|
return ContactPicker(fragment).also {
|
||||||
|
it.setContactPickerCallback(attachmentsPickerCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -130,7 +130,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportKeysManually() {
|
private fun exportKeysManually() {
|
||||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
|
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
|
||||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||||
override fun onPassphrase(passphrase: String) {
|
override fun onPassphrase(passphrase: String) {
|
||||||
showWaitingView()
|
showWaitingView()
|
||||||
|
|
|
@ -135,7 +135,13 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.findViewById<View>(R.id.keys_backup_setup_save)?.setOnClickListener {
|
dialog.findViewById<View>(R.id.keys_backup_setup_save)?.setOnClickListener {
|
||||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
|
val permissionsChecked = checkPermissions(
|
||||||
|
PERMISSIONS_FOR_WRITING_FILES,
|
||||||
|
this,
|
||||||
|
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
|
||||||
|
R.string.permissions_rationale_msg_keys_backup_export
|
||||||
|
)
|
||||||
|
if (permissionsChecked) {
|
||||||
exportRecoveryKeyToFile(recoveryKey)
|
exportRecoveryKeyToFile(recoveryKey)
|
||||||
}
|
}
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
|
|
|
@ -27,7 +27,6 @@ import androidx.core.view.isVisible
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import com.airbnb.mvrx.viewModel
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
|
@ -55,11 +54,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
object OpenGroup : Navigation()
|
object OpenGroup : Navigation()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
|
|
||||||
private lateinit var navigationViewModel: HomeNavigationViewModel
|
private lateinit var navigationViewModel: HomeNavigationViewModel
|
||||||
|
|
||||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||||
@Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory
|
|
||||||
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
|
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
|
||||||
@Inject lateinit var pushManager: PushersManager
|
@Inject lateinit var pushManager: PushersManager
|
||||||
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail
|
package im.vector.riotx.features.home.room.detail
|
||||||
|
|
||||||
import com.jaiselrahman.filepicker.model.MediaFile
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
|
@ -26,7 +26,7 @@ sealed class RoomDetailActions {
|
||||||
|
|
||||||
data class SaveDraft(val draft: String) : RoomDetailActions()
|
data class SaveDraft(val draft: String) : RoomDetailActions()
|
||||||
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
|
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
|
||||||
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
|
data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailActions()
|
||||||
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
|
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
|
||||||
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
|
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
|
||||||
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
|
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
|
||||||
|
|
|
@ -52,14 +52,12 @@ import com.github.piasy.biv.BigImageViewer
|
||||||
import com.github.piasy.biv.loader.ImageLoader
|
import com.github.piasy.biv.loader.ImageLoader
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
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
|
|
||||||
import com.otaliastudios.autocomplete.Autocomplete
|
import com.otaliastudios.autocomplete.Autocomplete
|
||||||
import com.otaliastudios.autocomplete.AutocompleteCallback
|
import com.otaliastudios.autocomplete.AutocompleteCallback
|
||||||
import com.otaliastudios.autocomplete.CharPolicy
|
import com.otaliastudios.autocomplete.CharPolicy
|
||||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
|
@ -82,6 +80,11 @@ import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
|
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
|
||||||
import im.vector.riotx.core.ui.views.NotificationAreaView
|
import im.vector.riotx.core.ui.views.NotificationAreaView
|
||||||
import im.vector.riotx.core.utils.*
|
import im.vector.riotx.core.utils.*
|
||||||
|
import im.vector.riotx.core.utils.Debouncer
|
||||||
|
import im.vector.riotx.core.utils.createUIHandler
|
||||||
|
import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
|
||||||
|
import im.vector.riotx.features.attachments.AttachmentsHelper
|
||||||
|
import im.vector.riotx.features.attachments.ContactAttachment
|
||||||
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
||||||
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
|
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
|
||||||
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
|
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
|
||||||
|
@ -112,6 +115,7 @@ import im.vector.riotx.features.media.VideoMediaViewerActivity
|
||||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||||
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
|
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
|
||||||
import im.vector.riotx.features.settings.VectorPreferences
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
|
import im.vector.riotx.features.share.SharedData
|
||||||
import im.vector.riotx.features.themes.ThemeUtils
|
import im.vector.riotx.features.themes.ThemeUtils
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||||
|
@ -125,20 +129,20 @@ import javax.inject.Inject
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class RoomDetailArgs(
|
data class RoomDetailArgs(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String? = null
|
val eventId: String? = null,
|
||||||
|
val sharedData: SharedData? = null
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
private const val CAMERA_VALUE_TITLE = "attachment"
|
private const val REACTION_SELECT_REQUEST_CODE = 0
|
||||||
private const val REQUEST_FILES_REQUEST_CODE = 0
|
|
||||||
private const val TAKE_IMAGE_REQUEST_CODE = 1
|
|
||||||
private const val REACTION_SELECT_REQUEST_CODE = 2
|
|
||||||
|
|
||||||
class RoomDetailFragment :
|
class RoomDetailFragment :
|
||||||
VectorBaseFragment(),
|
VectorBaseFragment(),
|
||||||
TimelineEventController.Callback,
|
TimelineEventController.Callback,
|
||||||
AutocompleteUserPresenter.Callback,
|
AutocompleteUserPresenter.Callback,
|
||||||
VectorInviteView.Callback,
|
VectorInviteView.Callback,
|
||||||
JumpToReadMarkerView.Callback {
|
JumpToReadMarkerView.Callback,
|
||||||
|
AttachmentTypeSelectorView.Callback,
|
||||||
|
AttachmentsHelper.Callback {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@ -199,9 +203,12 @@ class RoomDetailFragment :
|
||||||
|
|
||||||
private lateinit var actionViewModel: ActionsHandler
|
private lateinit var actionViewModel: ActionsHandler
|
||||||
private lateinit var layoutManager: LinearLayoutManager
|
private lateinit var layoutManager: LinearLayoutManager
|
||||||
|
private lateinit var attachmentsHelper: AttachmentsHelper
|
||||||
|
private lateinit var keyboardStateUtils: KeyboardStateUtils
|
||||||
|
|
||||||
@BindView(R.id.composerLayout)
|
@BindView(R.id.composerLayout)
|
||||||
lateinit var composerLayout: TextComposerView
|
lateinit var composerLayout: TextComposerView
|
||||||
|
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
|
||||||
|
|
||||||
private var lockSendButton = false
|
private var lockSendButton = false
|
||||||
|
|
||||||
|
@ -212,6 +219,8 @@ class RoomDetailFragment :
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
||||||
|
attachmentsHelper = AttachmentsHelper.create(this, this).register()
|
||||||
|
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
||||||
setupToolbar(roomToolbar)
|
setupToolbar(roomToolbar)
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupComposer()
|
setupComposer()
|
||||||
|
@ -275,6 +284,14 @@ class RoomDetailFragment :
|
||||||
roomDetailViewModel.requestLiveData.observeEvent(this) {
|
roomDetailViewModel.requestLiveData.observeEvent(this) {
|
||||||
displayRoomDetailActionResult(it)
|
displayRoomDetailActionResult(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
when (val sharedData = roomDetailArgs.sharedData) {
|
||||||
|
is SharedData.Text -> roomDetailViewModel.process(RoomDetailActions.SendMessage(sharedData.text, false))
|
||||||
|
is SharedData.Attachments -> roomDetailViewModel.process(RoomDetailActions.SendMedia(sharedData.attachmentData))
|
||||||
|
null -> Timber.v("No share data to process")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
@ -404,7 +421,7 @@ class RoomDetailFragment :
|
||||||
if (text != composerLayout.composerEditText.text.toString()) {
|
if (text != composerLayout.composerEditText.text.toString()) {
|
||||||
// Ignore update to avoid saving a draft
|
// Ignore update to avoid saving a draft
|
||||||
composerLayout.composerEditText.setText(text)
|
composerLayout.composerEditText.setText(text)
|
||||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,13 +440,14 @@ class RoomDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
|
||||||
if (resultCode == RESULT_OK && data != null) {
|
if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
|
||||||
REACTION_SELECT_REQUEST_CODE -> {
|
REACTION_SELECT_REQUEST_CODE -> {
|
||||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) ?: return
|
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
||||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) ?: return
|
?: return
|
||||||
|
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||||
|
?: return
|
||||||
// TODO check if already reacted with that?
|
// TODO check if already reacted with that?
|
||||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(eventId, reaction))
|
roomDetailViewModel.process(RoomDetailActions.SendReaction(eventId, reaction))
|
||||||
}
|
}
|
||||||
|
@ -598,47 +616,27 @@ class RoomDetailFragment :
|
||||||
composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
|
composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
|
||||||
roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString()))
|
roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString()))
|
||||||
}
|
}
|
||||||
|
composerLayout.callback = object : TextComposerView.Callback {
|
||||||
|
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
||||||
|
val shareIntent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
data = contentUri
|
||||||
|
}
|
||||||
|
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
|
||||||
|
if (!isHandled) {
|
||||||
|
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
return isHandled
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAttachmentButton() {
|
private fun setupAttachmentButton() {
|
||||||
composerLayout.attachmentButton.setOnClickListener {
|
composerLayout.attachmentButton.setOnClickListener {
|
||||||
val intent = Intent(requireContext(), FilePickerActivity::class.java)
|
if (!::attachmentTypeSelector.isInitialized) {
|
||||||
intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
|
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this)
|
||||||
.setCheckPermission(true)
|
|
||||||
.setShowFiles(true)
|
|
||||||
.setShowAudios(true)
|
|
||||||
.setSkipZeroSizeFiles(true)
|
|
||||||
.build())
|
|
||||||
startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE)
|
|
||||||
/*
|
|
||||||
val items = ArrayList<DialogListItem>()
|
|
||||||
// Send file
|
|
||||||
items.add(DialogListItem.SendFile)
|
|
||||||
// Send voice
|
|
||||||
|
|
||||||
if (vectorPreferences.isSendVoiceFeatureEnabled()) {
|
|
||||||
items.add(DialogListItem.SendVoice.INSTANCE)
|
|
||||||
}
|
}
|
||||||
|
attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing)
|
||||||
|
|
||||||
// Send sticker
|
|
||||||
//items.add(DialogListItem.SendSticker)
|
|
||||||
// Camera
|
|
||||||
|
|
||||||
//if (vectorPreferences.useNativeCamera()) {
|
|
||||||
items.add(DialogListItem.TakePhoto)
|
|
||||||
items.add(DialogListItem.TakeVideo)
|
|
||||||
//} else {
|
|
||||||
// items.add(DialogListItem.TakePhotoVideo.INSTANCE)
|
|
||||||
// }
|
|
||||||
val adapter = DialogSendItemAdapter(requireContext(), items)
|
|
||||||
AlertDialog.Builder(requireContext())
|
|
||||||
.setAdapter(adapter) { _, position ->
|
|
||||||
onSendChoiceClicked(items[position])
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -646,38 +644,6 @@ class RoomDetailFragment :
|
||||||
inviteView.callback = this
|
inviteView.callback = this
|
||||||
}
|
}
|
||||||
|
|
||||||
/* private fun onSendChoiceClicked(dialogListItem: DialogListItem) {
|
|
||||||
Timber.v("On send choice clicked: $dialogListItem")
|
|
||||||
when (dialogListItem) {
|
|
||||||
is DialogListItem.SendFile -> {
|
|
||||||
// launchFileIntent
|
|
||||||
}
|
|
||||||
is DialogListItem.SendVoice -> {
|
|
||||||
//launchAudioRecorderIntent()
|
|
||||||
}
|
|
||||||
is DialogListItem.SendSticker -> {
|
|
||||||
//startStickerPickerActivity()
|
|
||||||
}
|
|
||||||
is DialogListItem.TakePhotoVideo ->
|
|
||||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
|
|
||||||
// launchCamera()
|
|
||||||
}
|
|
||||||
is DialogListItem.TakePhoto ->
|
|
||||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) {
|
|
||||||
openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE)
|
|
||||||
}
|
|
||||||
is DialogListItem.TakeVideo ->
|
|
||||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) {
|
|
||||||
// launchNativeVideoRecorder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
|
|
||||||
private fun handleMediaIntent(data: Intent) {
|
|
||||||
val files: ArrayList<MediaFile> = data.getParcelableArrayListExtra(FilePickerActivity.MEDIA_FILES)
|
|
||||||
roomDetailViewModel.process(RoomDetailActions.SendMedia(files))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun renderState(state: RoomDetailViewState) {
|
private fun renderState(state: RoomDetailViewState) {
|
||||||
readMarkerHelper.updateWith(state)
|
readMarkerHelper.updateWith(state)
|
||||||
renderRoomSummary(state)
|
renderRoomSummary(state)
|
||||||
|
@ -941,11 +907,16 @@ class RoomDetailFragment :
|
||||||
if (allGranted(grantResults)) {
|
if (allGranted(grantResults)) {
|
||||||
if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) {
|
if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) {
|
||||||
val action = roomDetailViewModel.pendingAction
|
val action = roomDetailViewModel.pendingAction
|
||||||
|
|
||||||
if (action != null) {
|
if (action != null) {
|
||||||
roomDetailViewModel.pendingAction = null
|
roomDetailViewModel.pendingAction = null
|
||||||
roomDetailViewModel.process(action)
|
roomDetailViewModel.process(action)
|
||||||
}
|
}
|
||||||
|
} else if (requestCode == PERMISSION_REQUEST_CODE_PICK_ATTACHMENT) {
|
||||||
|
val pendingType = attachmentsHelper.pendingType
|
||||||
|
if (pendingType != null) {
|
||||||
|
attachmentsHelper.pendingType = null
|
||||||
|
launchAttachmentProcess(pendingType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1164,21 +1135,21 @@ class RoomDetailFragment :
|
||||||
val myDisplayName = session.getUser(session.myUserId)?.displayName
|
val myDisplayName = session.getUser(session.myUserId)?.displayName
|
||||||
if (myDisplayName == text) {
|
if (myDisplayName == text) {
|
||||||
// current user
|
// current user
|
||||||
if (composerLayout.composerEditText.text.isBlank()) {
|
if (composerLayout.composerEditText.text.isNullOrBlank()) {
|
||||||
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
|
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
|
||||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
|
||||||
// vibrate = true
|
// vibrate = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// another user
|
// another user
|
||||||
if (composerLayout.composerEditText.text.isBlank()) {
|
if (composerLayout.composerEditText.text.isNullOrBlank()) {
|
||||||
// Ensure displayName will not be interpreted as a Slash command
|
// Ensure displayName will not be interpreted as a Slash command
|
||||||
if (text.startsWith("/")) {
|
if (text.startsWith("/")) {
|
||||||
composerLayout.composerEditText.append("\\")
|
composerLayout.composerEditText.append("\\")
|
||||||
}
|
}
|
||||||
composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
|
composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
|
||||||
} else {
|
} else {
|
||||||
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
|
composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// vibrate = true
|
// vibrate = true
|
||||||
|
@ -1227,4 +1198,41 @@ class RoomDetailFragment :
|
||||||
override fun onClearReadMarkerClicked() {
|
override fun onClearReadMarkerClicked() {
|
||||||
roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
|
roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AttachmentTypeSelectorView.Callback
|
||||||
|
|
||||||
|
override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
|
||||||
|
if (checkPermissions(type.permissionsBit, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) {
|
||||||
|
launchAttachmentProcess(type)
|
||||||
|
} else {
|
||||||
|
attachmentsHelper.pendingType = type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
|
||||||
|
when (type) {
|
||||||
|
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
|
||||||
|
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
|
||||||
|
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery()
|
||||||
|
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
|
||||||
|
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact()
|
||||||
|
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachmentsHelper.Callback
|
||||||
|
|
||||||
|
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachmentsProcessFailed() {
|
||||||
|
Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
||||||
|
super.onContactAttachmentReady(contactAttachment)
|
||||||
|
val formattedContact = contactAttachment.toHumanReadable()
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.SendMessage(formattedContact, false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail
|
package im.vector.riotx.features.home.room.detail
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
@ -27,7 +26,6 @@ import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.MatrixPatterns
|
import im.vector.matrix.android.api.MatrixPatterns
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
||||||
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
||||||
|
@ -49,8 +47,6 @@ import im.vector.matrix.rx.unwrap
|
||||||
import im.vector.riotx.BuildConfig
|
import im.vector.riotx.BuildConfig
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.postLiveEvent
|
import im.vector.riotx.core.extensions.postLiveEvent
|
||||||
import im.vector.riotx.core.images.ImageTools
|
|
||||||
import im.vector.riotx.core.intent.getFilenameFromUri
|
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||||
import im.vector.riotx.core.utils.LiveEvent
|
import im.vector.riotx.core.utils.LiveEvent
|
||||||
|
@ -69,7 +65,6 @@ import java.util.concurrent.TimeUnit
|
||||||
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
|
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
|
||||||
userPreferencesProvider: UserPreferencesProvider,
|
userPreferencesProvider: UserPreferencesProvider,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val imageTools: ImageTools,
|
|
||||||
private val session: Session
|
private val session: Session
|
||||||
) : VectorViewModel<RoomDetailViewState>(initialState) {
|
) : VectorViewModel<RoomDetailViewState>(initialState) {
|
||||||
|
|
||||||
|
@ -471,32 +466,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
||||||
val attachments = action.mediaFiles.map {
|
val attachments = action.attachments
|
||||||
val pathWithScheme = if (it.path.startsWith("/")) {
|
|
||||||
"file://" + it.path
|
|
||||||
} else {
|
|
||||||
it.path
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri = Uri.parse(pathWithScheme)
|
|
||||||
val nameWithExtension = getFilenameFromUri(null, uri)
|
|
||||||
|
|
||||||
ContentAttachmentData(
|
|
||||||
size = it.size,
|
|
||||||
duration = it.duration,
|
|
||||||
date = it.date,
|
|
||||||
height = it.height,
|
|
||||||
width = it.width,
|
|
||||||
exifOrientation = imageTools.getOrientationForBitmap(uri),
|
|
||||||
name = nameWithExtension ?: it.name,
|
|
||||||
path = it.path,
|
|
||||||
mimeType = it.mimeType,
|
|
||||||
type = ContentAttachmentData.Type.values()[it.mediaType]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val homeServerCapabilities = session.getHomeServerCapabilities()
|
val homeServerCapabilities = session.getHomeServerCapabilities()
|
||||||
|
|
||||||
val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
|
val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
|
||||||
|
|
||||||
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
|
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
|
||||||
|
@ -505,7 +476,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
} else {
|
} else {
|
||||||
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
||||||
null -> room.sendMedias(attachments)
|
null -> room.sendMedias(attachments)
|
||||||
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
|
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
|
||||||
|
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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.composer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.view.inputmethod.InputConnection
|
||||||
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
|
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||||
|
|
||||||
|
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle)
|
||||||
|
: AppCompatEditText(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onRichContentSelected(contentUri: Uri): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||||
|
val ic: InputConnection = super.onCreateInputConnection(editorInfo)
|
||||||
|
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("*/*"))
|
||||||
|
|
||||||
|
val callback =
|
||||||
|
InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, _ ->
|
||||||
|
val lacksPermission = (flags and
|
||||||
|
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && lacksPermission) {
|
||||||
|
try {
|
||||||
|
inputContentInfo.requestPermission()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return@OnCommitContentListener false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback?.onRichContentSelected(inputContentInfo.contentUri) ?: false
|
||||||
|
}
|
||||||
|
return InputConnectionCompat.createWrapper(ic, editorInfo, callback)
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,9 +17,9 @@
|
||||||
package im.vector.riotx.features.home.room.detail.composer
|
package im.vector.riotx.features.home.room.detail.composer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -39,6 +39,10 @@ import im.vector.riotx.R
|
||||||
class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
|
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
interface Callback : ComposerEditText.Callback
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
@BindView(R.id.composer_related_message_sender)
|
@BindView(R.id.composer_related_message_sender)
|
||||||
lateinit var composerRelatedMessageTitle: TextView
|
lateinit var composerRelatedMessageTitle: TextView
|
||||||
@BindView(R.id.composer_related_message_preview)
|
@BindView(R.id.composer_related_message_preview)
|
||||||
|
@ -50,11 +54,11 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
@BindView(R.id.composer_related_message_close)
|
@BindView(R.id.composer_related_message_close)
|
||||||
lateinit var composerRelatedMessageCloseButton: ImageButton
|
lateinit var composerRelatedMessageCloseButton: ImageButton
|
||||||
@BindView(R.id.composerEditText)
|
@BindView(R.id.composerEditText)
|
||||||
lateinit var composerEditText: EditText
|
lateinit var composerEditText: ComposerEditText
|
||||||
@BindView(R.id.composer_avatar_view)
|
@BindView(R.id.composer_avatar_view)
|
||||||
lateinit var composerAvatarImageView: ImageView
|
lateinit var composerAvatarImageView: ImageView
|
||||||
|
|
||||||
var currentConstraintSetId: Int = -1
|
private var currentConstraintSetId: Int = -1
|
||||||
|
|
||||||
private val animationDuration = 100L
|
private val animationDuration = 100L
|
||||||
|
|
||||||
|
@ -62,6 +66,11 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
inflate(context, R.layout.merge_composer_layout, this)
|
inflate(context, R.layout.merge_composer_layout, this)
|
||||||
ButterKnife.bind(this)
|
ButterKnife.bind(this)
|
||||||
collapse(false)
|
collapse(false)
|
||||||
|
composerEditText.callback = object : Callback, ComposerEditText.Callback {
|
||||||
|
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
||||||
|
return callback?.onRichContentSelected(contentUri) ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
||||||
|
|
|
@ -42,9 +42,7 @@ class FilteredRoomsActivity : VectorBaseActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
configureToolbar(filteredRoomsToolbar)
|
configureToolbar(filteredRoomsToolbar)
|
||||||
|
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
roomListFragment = RoomListFragment.newInstance(RoomListParams(RoomListFragment.DisplayMode.FILTERED))
|
roomListFragment = RoomListFragment.newInstance(RoomListParams(RoomListFragment.DisplayMode.FILTERED))
|
||||||
replaceFragment(roomListFragment, R.id.filteredRoomsFragmentContainer, FRAGMENT_TAG)
|
replaceFragment(roomListFragment, R.id.filteredRoomsFragmentContainer, FRAGMENT_TAG)
|
||||||
|
@ -58,12 +56,10 @@ class FilteredRoomsActivity : VectorBaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String): Boolean {
|
override fun onQueryTextChange(newText: String): Boolean {
|
||||||
// TODO Create a viewModel and remove this public fun
|
|
||||||
roomListFragment.filterRoomsWith(newText)
|
roomListFragment.filterRoomsWith(newText)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Open the keyboard immediately
|
// Open the keyboard immediately
|
||||||
filteredRoomsSearchView.requestFocus()
|
filteredRoomsSearchView.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListFragment.Displa
|
||||||
RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
|
RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
|
||||||
RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
|
RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
|
||||||
RoomListFragment.DisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
|
RoomListFragment.DisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
|
||||||
|
RoomListFragment.DisplayMode.SHARE -> roomSummary.membership == Membership.JOIN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,13 +41,15 @@ import im.vector.riotx.core.platform.StateView
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotx.features.home.room.list.widget.FabMenuView
|
import im.vector.riotx.features.home.room.list.widget.FabMenuView
|
||||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||||
|
import im.vector.riotx.features.share.SharedData
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.fragment_room_list.*
|
import kotlinx.android.synthetic.main.fragment_room_list.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class RoomListParams(
|
data class RoomListParams(
|
||||||
val displayMode: RoomListFragment.DisplayMode
|
val displayMode: RoomListFragment.DisplayMode,
|
||||||
|
val sharedData: SharedData? = null
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
|
class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
|
||||||
|
@ -56,7 +58,8 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
||||||
HOME(R.string.bottom_action_home),
|
HOME(R.string.bottom_action_home),
|
||||||
PEOPLE(R.string.bottom_action_people_x),
|
PEOPLE(R.string.bottom_action_people_x),
|
||||||
ROOMS(R.string.bottom_action_rooms),
|
ROOMS(R.string.bottom_action_rooms),
|
||||||
FILTERED(/* Not used */ R.string.bottom_action_rooms)
|
FILTERED(/* Not used */ 0),
|
||||||
|
SHARE(/* Not used */ 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -106,8 +109,13 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
roomListViewModel.subscribe { renderState(it) }
|
roomListViewModel.subscribe { renderState(it) }
|
||||||
roomListViewModel.openRoomLiveData.observeEventFirstThrottle(this, 800L) {
|
roomListViewModel.openRoomLiveData.observeEventFirstThrottle(this, 800L) {
|
||||||
|
if (roomListParams.displayMode == DisplayMode.SHARE) {
|
||||||
|
val sharedData = roomListParams.sharedData ?: return@observeEventFirstThrottle
|
||||||
|
navigator.openRoomForSharing(requireActivity(), it, sharedData)
|
||||||
|
} else {
|
||||||
navigator.openRoom(requireActivity(), it)
|
navigator.openRoom(requireActivity(), it)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createChatFabMenu.listener = this
|
createChatFabMenu.listener = this
|
||||||
|
|
||||||
|
@ -124,7 +132,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
||||||
DisplayMode.HOME -> createChatFabMenu.isVisible = true
|
DisplayMode.HOME -> createChatFabMenu.isVisible = true
|
||||||
DisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
|
DisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
|
||||||
DisplayMode.ROOMS -> createGroupRoomButton.isVisible = true
|
DisplayMode.ROOMS -> createGroupRoomButton.isVisible = true
|
||||||
DisplayMode.FILTERED -> Unit // No button in this mode
|
else -> Unit // No button in this mode
|
||||||
}
|
}
|
||||||
|
|
||||||
createChatRoomButton.setOnClickListener {
|
createChatRoomButton.setOnClickListener {
|
||||||
|
@ -150,7 +158,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
||||||
DisplayMode.HOME -> createChatFabMenu.hide()
|
DisplayMode.HOME -> createChatFabMenu.hide()
|
||||||
DisplayMode.PEOPLE -> createChatRoomButton.hide()
|
DisplayMode.PEOPLE -> createChatRoomButton.hide()
|
||||||
DisplayMode.ROOMS -> createGroupRoomButton.hide()
|
DisplayMode.ROOMS -> createGroupRoomButton.hide()
|
||||||
DisplayMode.FILTERED -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,7 +198,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
||||||
DisplayMode.HOME -> createChatFabMenu.show()
|
DisplayMode.HOME -> createChatFabMenu.show()
|
||||||
DisplayMode.PEOPLE -> createChatRoomButton.show()
|
DisplayMode.PEOPLE -> createChatRoomButton.show()
|
||||||
DisplayMode.ROOMS -> createGroupRoomButton.show()
|
DisplayMode.ROOMS -> createGroupRoomButton.show()
|
||||||
DisplayMode.FILTERED -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -261,7 +269,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
||||||
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_group),
|
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_group),
|
||||||
getString(R.string.room_list_rooms_empty_body)
|
getString(R.string.room_list_rooms_empty_body)
|
||||||
)
|
)
|
||||||
DisplayMode.FILTERED ->
|
else ->
|
||||||
// Always display the content in this mode, because if the footer
|
// Always display the content in this mode, because if the footer
|
||||||
StateView.State.Content
|
StateView.State.Content
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,6 +230,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
|
||||||
RoomListFragment.DisplayMode.PEOPLE -> chronologicalRoomComparator
|
RoomListFragment.DisplayMode.PEOPLE -> chronologicalRoomComparator
|
||||||
RoomListFragment.DisplayMode.ROOMS -> chronologicalRoomComparator
|
RoomListFragment.DisplayMode.ROOMS -> chronologicalRoomComparator
|
||||||
RoomListFragment.DisplayMode.FILTERED -> chronologicalRoomComparator
|
RoomListFragment.DisplayMode.FILTERED -> chronologicalRoomComparator
|
||||||
|
RoomListFragment.DisplayMode.SHARE -> chronologicalRoomComparator
|
||||||
}
|
}
|
||||||
|
|
||||||
return RoomSummaries().apply {
|
return RoomSummaries().apply {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.navigation
|
package im.vector.riotx.features.navigation
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
||||||
|
@ -33,6 +34,7 @@ import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
|
||||||
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
|
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
|
||||||
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
|
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
|
||||||
import im.vector.riotx.features.settings.VectorSettingsActivity
|
import im.vector.riotx.features.settings.VectorSettingsActivity
|
||||||
|
import im.vector.riotx.features.share.SharedData
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
@ -46,6 +48,13 @@ class DefaultNavigator @Inject constructor() : Navigator {
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData) {
|
||||||
|
val args = RoomDetailArgs(roomId, null, sharedData)
|
||||||
|
val intent = RoomDetailActivity.newIntent(activity, args)
|
||||||
|
activity.startActivity(intent)
|
||||||
|
activity.finish()
|
||||||
|
}
|
||||||
|
|
||||||
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String?) {
|
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String?) {
|
||||||
if (context is VectorBaseActivity) {
|
if (context is VectorBaseActivity) {
|
||||||
context.notImplemented("Open not joined room")
|
context.notImplemented("Open not joined room")
|
||||||
|
|
|
@ -16,13 +16,17 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.navigation
|
package im.vector.riotx.features.navigation
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
||||||
|
import im.vector.riotx.features.share.SharedData
|
||||||
|
|
||||||
interface Navigator {
|
interface Navigator {
|
||||||
|
|
||||||
fun openRoom(context: Context, roomId: String, eventId: String? = null)
|
fun openRoom(context: Context, roomId: String, eventId: String? = null)
|
||||||
|
|
||||||
|
fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData)
|
||||||
|
|
||||||
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String? = null)
|
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String? = null)
|
||||||
|
|
||||||
fun openRoomPreview(publicRoom: PublicRoom, context: Context)
|
fun openRoomPreview(publicRoom: PublicRoom, context: Context)
|
||||||
|
|
|
@ -211,7 +211,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() {
|
||||||
*/
|
*/
|
||||||
private fun exportKeys() {
|
private fun exportKeys() {
|
||||||
// We need WRITE_EXTERNAL permission
|
// We need WRITE_EXTERNAL permission
|
||||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
|
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||||
override fun onPassphrase(passphrase: String) {
|
override fun onPassphrase(passphrase: String) {
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* 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.share
|
||||||
|
|
||||||
|
import android.content.ClipDescription
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.kbeanie.multipicker.utils.IntentUtils
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
|
import im.vector.riotx.core.extensions.replaceFragment
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
|
import im.vector.riotx.features.attachments.AttachmentsHelper
|
||||||
|
import im.vector.riotx.features.home.LoadingFragment
|
||||||
|
import im.vector.riotx.features.home.room.list.RoomListFragment
|
||||||
|
import im.vector.riotx.features.home.room.list.RoomListParams
|
||||||
|
import im.vector.riotx.features.login.LoginActivity
|
||||||
|
import kotlinx.android.synthetic.main.activity_incoming_share.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class IncomingShareActivity :
|
||||||
|
VectorBaseActivity(), AttachmentsHelper.Callback {
|
||||||
|
|
||||||
|
@Inject lateinit var sessionHolder: ActiveSessionHolder
|
||||||
|
private lateinit var roomListFragment: RoomListFragment
|
||||||
|
private lateinit var attachmentsHelper: AttachmentsHelper
|
||||||
|
|
||||||
|
override fun getLayoutRes(): Int {
|
||||||
|
return R.layout.activity_incoming_share
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
injector.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
// If we are not logged in, stop the sharing process and open login screen.
|
||||||
|
// In the future, we might want to relaunch the sharing process after login.
|
||||||
|
if (!sessionHolder.hasActiveSession()) {
|
||||||
|
startLoginActivity()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configureToolbar(incomingShareToolbar)
|
||||||
|
if (isFirstCreation()) {
|
||||||
|
val loadingDetail = LoadingFragment.newInstance()
|
||||||
|
replaceFragment(loadingDetail, R.id.shareRoomListFragmentContainer)
|
||||||
|
}
|
||||||
|
attachmentsHelper = AttachmentsHelper.create(this, this).register()
|
||||||
|
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
|
||||||
|
var isShareManaged = attachmentsHelper.handleShareIntent(
|
||||||
|
IntentUtils.getPickerIntentForSharing(intent)
|
||||||
|
)
|
||||||
|
if (!isShareManaged) {
|
||||||
|
isShareManaged = handleTextShare(intent)
|
||||||
|
}
|
||||||
|
if (!isShareManaged) {
|
||||||
|
cannotManageShare()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cannotManageShare()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
||||||
|
val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Attachments(attachments))
|
||||||
|
roomListFragment = RoomListFragment.newInstance(roomListParams)
|
||||||
|
replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachmentsProcessFailed() {
|
||||||
|
cannotManageShare()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cannotManageShare() {
|
||||||
|
Toast.makeText(this, R.string.error_handling_incoming_share, Toast.LENGTH_LONG).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleTextShare(intent: Intent): Boolean {
|
||||||
|
if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) {
|
||||||
|
val sharedText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
|
||||||
|
return if (sharedText.isNullOrEmpty()) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Text(sharedText))
|
||||||
|
roomListFragment = RoomListFragment.newInstance(roomListParams)
|
||||||
|
replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startLoginActivity() {
|
||||||
|
val intent = LoginActivity.newIntent(this, null)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.features.share
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
sealed class SharedData: Parcelable {
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Text(val text: String): SharedData()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Attachments(val attachmentData: List<ContentAttachmentData>): SharedData()
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 707 B |
Binary file not shown.
After Width: | Height: | Size: 442 B |
Binary file not shown.
After Width: | Height: | Size: 928 B |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="?riotx_attachment_selector_border" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:bottom="1dp"
|
||||||
|
android:left="1dp"
|
||||||
|
android:right="1dp"
|
||||||
|
android:top="1dp">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="?riotx_attachment_selector_background" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
|
||||||
|
</shape>
|
||||||
|
|
||||||
|
</item>
|
||||||
|
|
||||||
|
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h3c1.66,0 3,-1.34 3,-3v-7c0,-4.97 -4.03,-9 -9,-9z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M9.4,10.5l4.77,-8.26C13.47,2.09 12.75,2 12,2c-2.4,0 -4.6,0.85 -6.32,2.25l3.66,6.35 0.06,-0.1zM21.54,9c-0.92,-2.92 -3.15,-5.26 -6,-6.34L11.88,9h9.66zM21.8,10h-7.49l0.29,0.5 4.76,8.25C21,16.97 22,14.61 22,12c0,-0.69 -0.07,-1.35 -0.2,-2zM8.54,12l-3.9,-6.75C3.01,7.03 2,9.39 2,12c0,0.69 0.07,1.35 0.2,2h7.49l-1.15,-2zM2.46,15c0.92,2.92 3.15,5.26 6,6.34L12.12,15L2.46,15zM13.73,15l-3.9,6.76c0.7,0.15 1.42,0.24 2.17,0.24 2.4,0 4.6,-0.85 6.32,-2.25l-3.66,-6.35 -0.93,1.6z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||||
|
<path android:fillColor="#FFFFFF" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/incomingShareToolbar"
|
||||||
|
style="@style/VectorToolbarStyle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:elevation="4dp"
|
||||||
|
app:contentInsetStart="0dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SearchView
|
||||||
|
android:id="@+id/incomingShareSearchView"
|
||||||
|
style="@style/VectorSearchView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:queryHint="@string/room_filtering_filter_hint"
|
||||||
|
app:searchIcon="@drawable/ic_filter" />
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/shareRoomListFragmentContainer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/incomingShareToolbar" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -143,7 +143,7 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/attachmentButton" />
|
app:layout_constraintStart_toEndOf="@id/attachmentButton" />
|
||||||
|
|
||||||
<EditText
|
<im.vector.riotx.features.home.room.detail.composer.ComposerEditText
|
||||||
android:id="@+id/composerEditText"
|
android:id="@+id/composerEditText"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -153,7 +153,7 @@
|
||||||
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
|
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
|
||||||
app:layout_constraintVertical_bias="1" />
|
app:layout_constraintVertical_bias="1" />
|
||||||
|
|
||||||
<EditText
|
<im.vector.riotx.features.home.room.detail.composer.ComposerEditText
|
||||||
android:id="@+id/composerEditText"
|
android:id="@+id/composerEditText"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -114,7 +114,7 @@
|
||||||
android:tint="?attr/colorAccent"
|
android:tint="?attr/colorAccent"
|
||||||
tools:ignore="MissingConstraints" />
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
<EditText
|
<im.vector.riotx.features.home.room.detail.composer.ComposerEditText
|
||||||
android:id="@+id/composerEditText"
|
android:id="@+id/composerEditText"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
<?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:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_attachment_type_selector"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:weightSum="3">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentCameraButton"
|
||||||
|
style="@style/AttachmentTypeSelectorButton"
|
||||||
|
android:src="@drawable/ic_attachment_camera_white_24dp"
|
||||||
|
tools:background="@color/colorAccent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/AttachmentTypeSelectorLabel"
|
||||||
|
android:text="@string/attachment_type_camera" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentGalleryButton"
|
||||||
|
style="@style/AttachmentTypeSelectorButton"
|
||||||
|
android:src="@drawable/ic_attachment_gallery_white_24dp"
|
||||||
|
tools:background="@color/colorAccent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/AttachmentTypeSelectorLabel"
|
||||||
|
android:text="@string/attachment_type_gallery" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentFileButton"
|
||||||
|
style="@style/AttachmentTypeSelectorButton"
|
||||||
|
android:src="@drawable/ic_attachment_file_white_24dp"
|
||||||
|
tools:background="@color/colorAccent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/AttachmentTypeSelectorLabel"
|
||||||
|
android:text="@string/attachment_type_file" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:weightSum="3">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentAudioButton"
|
||||||
|
style="@style/AttachmentTypeSelectorButton"
|
||||||
|
android:src="@drawable/ic_attachment_audio_white_24dp"
|
||||||
|
tools:background="@color/colorAccent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/AttachmentTypeSelectorLabel"
|
||||||
|
android:text="@string/attachment_type_audio" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentContactButton"
|
||||||
|
style="@style/AttachmentTypeSelectorButton"
|
||||||
|
android:src="@drawable/ic_attachment_contact_white_24dp"
|
||||||
|
tools:background="@color/colorAccent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/AttachmentTypeSelectorLabel"
|
||||||
|
android:text="@string/attachment_type_contact" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentStickersButton"
|
||||||
|
style="@style/AttachmentTypeSelectorButton"
|
||||||
|
android:src="@drawable/ic_attachment_stickers_white_24dp"
|
||||||
|
tools:background="@color/colorAccent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/AttachmentTypeSelectorLabel"
|
||||||
|
android:text="@string/attachment_type_sticker" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -147,6 +147,16 @@
|
||||||
<color name="riotx_touch_guard_bg_dark">#BF000000</color>
|
<color name="riotx_touch_guard_bg_dark">#BF000000</color>
|
||||||
<color name="riotx_touch_guard_bg_black">#BF000000</color>
|
<color name="riotx_touch_guard_bg_black">#BF000000</color>
|
||||||
|
|
||||||
|
<attr name="riotx_attachment_selector_background" format="color" />
|
||||||
|
<color name="riotx_attachment_selector_background_light">#FFFFFFFF</color>
|
||||||
|
<color name="riotx_attachment_selector_background_dark">#FF22262E</color>
|
||||||
|
<color name="riotx_attachment_selector_background_black">#FF090A0C</color>
|
||||||
|
|
||||||
|
<attr name="riotx_attachment_selector_border" format="color" />
|
||||||
|
<color name="riotx_attachment_selector_border_light">#FFE9EDF1</color>
|
||||||
|
<color name="riotx_attachment_selector_border_dark">#FF22262E</color>
|
||||||
|
<color name="riotx_attachment_selector_border_black">#FF090A0C</color>
|
||||||
|
|
||||||
<!-- (color from RiotWeb) -->
|
<!-- (color from RiotWeb) -->
|
||||||
<attr name="riotx_keys_backup_banner_accent_color" format="color" />
|
<attr name="riotx_keys_backup_banner_accent_color" format="color" />
|
||||||
<color name="riotx_keys_backup_banner_accent_color_light">#FFF8E3</color>
|
<color name="riotx_keys_backup_banner_accent_color_light">#FFF8E3</color>
|
||||||
|
|
|
@ -369,6 +369,9 @@
|
||||||
<string name="permissions_rationale_msg_contacts">Riot can check your address book to find other Matrix users based on their email and phone numbers. If you agree to share your address book for this purpose, please allow access on the next pop-up.</string>
|
<string name="permissions_rationale_msg_contacts">Riot can check your address book to find other Matrix users based on their email and phone numbers. If you agree to share your address book for this purpose, please allow access on the next pop-up.</string>
|
||||||
<string name="permissions_msg_contacts_warning_other_androids">Riot can check your address book to find other Matrix users based on their email and phone numbers.\n\nDo you agree to share your address book for this purpose?</string>
|
<string name="permissions_msg_contacts_warning_other_androids">Riot can check your address book to find other Matrix users based on their email and phone numbers.\n\nDo you agree to share your address book for this purpose?</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<string name="permissions_action_not_performed_missing_permissions">Sorry. Action not performed, due to missing permissions</string>
|
<string name="permissions_action_not_performed_missing_permissions">Sorry. Action not performed, due to missing permissions</string>
|
||||||
|
|
||||||
<!-- medias slider string -->
|
<!-- medias slider string -->
|
||||||
|
|
|
@ -37,6 +37,14 @@
|
||||||
|
|
||||||
<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="error_file_too_big">"The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."</string>
|
||||||
|
|
||||||
|
<string name="error_attachment">"An error occurred while retrieving the attachment."</string>
|
||||||
|
<string name="attachment_type_file">"File"</string>
|
||||||
|
<string name="attachment_type_contact">"Contact"</string>
|
||||||
|
<string name="attachment_type_camera">"Camera"</string>
|
||||||
|
<string name="attachment_type_audio">"Audio"</string>
|
||||||
|
<string name="attachment_type_gallery">"Gallery"</string>
|
||||||
|
<string name="attachment_type_sticker">"Sticker"</string>
|
||||||
|
<string name="error_handling_incoming_share">Couldn\'t handle share data</string>
|
||||||
|
|
||||||
<string name="report_content_spam">"It's spam"</string>
|
<string name="report_content_spam">"It's spam"</string>
|
||||||
<string name="report_content_inappropriate">"It's inappropriate"</string>
|
<string name="report_content_inappropriate">"It's inappropriate"</string>
|
||||||
|
@ -52,4 +60,7 @@
|
||||||
<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_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_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>
|
<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>
|
||||||
|
|
||||||
|
<string name="permissions_rationale_msg_keys_backup_export">Riot needs permission to save your E2E keys on disk.\n\nPlease allow access on the next pop-up to be able to export your keys manually.</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -324,4 +324,19 @@
|
||||||
<item name="android:textSize">12sp</item>
|
<item name="android:textSize">12sp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="AttachmentTypeSelectorButton">
|
||||||
|
<item name="android:layout_width">56dp</item>
|
||||||
|
<item name="android:layout_height">56dp</item>
|
||||||
|
<item name="android:scaleType">center</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AttachmentTypeSelectorLabel">
|
||||||
|
<item name="android:layout_width">wrap_content</item>
|
||||||
|
<item name="android:layout_height">wrap_content</item>
|
||||||
|
<item name="android:textColor">?riotx_text_primary</item>
|
||||||
|
<item name="android:textSize">14sp</item>
|
||||||
|
<item name="android:layout_marginTop">8dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -30,6 +30,8 @@
|
||||||
<item name="riotx_fab_label_bg">@color/riotx_fab_label_bg_black</item>
|
<item name="riotx_fab_label_bg">@color/riotx_fab_label_bg_black</item>
|
||||||
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_black</item>
|
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_black</item>
|
||||||
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_black</item>
|
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_black</item>
|
||||||
|
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_black</item>
|
||||||
|
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_black</item>
|
||||||
|
|
||||||
<!-- Drawables -->
|
<!-- Drawables -->
|
||||||
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_black</item>
|
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_black</item>
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
<item name="riotx_fab_label_bg">@color/riotx_fab_label_bg_dark</item>
|
<item name="riotx_fab_label_bg">@color/riotx_fab_label_bg_dark</item>
|
||||||
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_dark</item>
|
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_dark</item>
|
||||||
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_dark</item>
|
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_dark</item>
|
||||||
|
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_dark</item>
|
||||||
|
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_dark</item>
|
||||||
|
|
||||||
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_dark</item>
|
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_dark</item>
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,8 @@
|
||||||
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_light</item>
|
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_light</item>
|
||||||
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_light</item>
|
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_light</item>
|
||||||
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_light</item>
|
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_light</item>
|
||||||
|
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_light</item>
|
||||||
|
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_light</item>
|
||||||
|
|
||||||
<!-- Drawables -->
|
<!-- Drawables -->
|
||||||
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_light</item>
|
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_light</item>
|
||||||
|
|
Loading…
Reference in New Issue