UserListViewModel.kt 7.99 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
 * Copyright (c) 2020 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.app.features.userdirectory

import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
24
25
26
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
27
28
29
30
31
32
33
34
35
36
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.toggle
import im.vector.app.core.platform.VectorViewModel
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
Valere's avatar
Valere committed
37
import org.matrix.android.sdk.api.util.toOptional
38
39
40
41
42
43
44
45
46
47
48
49
50
import org.matrix.android.sdk.rx.rx
import java.util.concurrent.TimeUnit

private typealias KnownUsersSearch = String
private typealias DirectoryUsersSearch = String

class UserListViewModel @AssistedInject constructor(@Assisted initialState: UserListViewState,
                                                    private val session: Session)
    : VectorViewModel<UserListViewState, UserListAction, UserListViewEvents>(initialState) {

    private val knownUsersSearch = BehaviorRelay.create<KnownUsersSearch>()
    private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()

51
    @AssistedFactory
52
    interface Factory {
Valere's avatar
Valere committed
53
        fun create(initialState: UserListViewState): UserListViewModel
54
55
56
57
58
59
60
61
62
    }

    companion object : MvRxViewModelFactory<UserListViewModel, UserListViewState> {

        override fun create(viewModelContext: ViewModelContext, state: UserListViewState): UserListViewModel? {
            val factory = when (viewModelContext) {
                is FragmentViewModelContext -> viewModelContext.fragment as? Factory
                is ActivityViewModelContext -> viewModelContext.activity as? Factory
            }
Valere's avatar
Valere committed
63
            return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
64
65
66
67
68
69
70
71
72
        }
    }

    init {
        observeUsers()
    }

    override fun handle(action: UserListAction) {
        when (action) {
73
74
75
76
            is UserListAction.SearchUsers -> handleSearchUsers(action.value)
            is UserListAction.ClearSearchUsers -> handleClearSearchUsers()
            is UserListAction.AddPendingSelection -> handleSelectUser(action)
            is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action)
77
78
79
80
81
82
83
84
85
86
87
88
89
90
            UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink()
        }.exhaustive
    }

    private fun handleSearchUsers(searchTerm: String) {
        setState {
            copy(searchTerm = searchTerm)
        }
        knownUsersSearch.accept(searchTerm)
        directoryUsersSearch.accept(searchTerm)
    }

    private fun handleShareMyMatrixToLink() {
        session.permalinkService().createPermalink(session.myUserId)?.let {
Benoit Marty's avatar
typo    
Benoit Marty committed
91
            _viewEvents.post(UserListViewEvents.OpenShareMatrixToLink(it))
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
        }
    }

    private fun handleClearSearchUsers() {
        knownUsersSearch.accept("")
        directoryUsersSearch.accept("")
        setState {
            copy(searchTerm = "")
        }
    }

    private fun observeUsers() = withState { state ->
        knownUsersSearch
                .throttleLast(300, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .switchMap {
                    session.rx().livePagedUsers(it, state.excludedUserIds)
                }
                .execute { async ->
                    copy(knownUsers = async)
                }

        directoryUsersSearch
                .debounce(300, TimeUnit.MILLISECONDS)
                .switchMapSingle { search ->
                    val stream = if (search.isBlank()) {
Valere's avatar
Valere committed
118
                        Single.just(emptyList<User>())
119
                    } else {
Valere's avatar
Valere committed
120
                        val searchObservable = session.rx()
Benoit Marty's avatar
Benoit Marty committed
121
                                .searchUsersDirectory(search, 50, state.excludedUserIds.orEmpty())
122
123
124
                                .map { users ->
                                    users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
                                }
Valere's avatar
Valere committed
125
126
127
128
129
130
131
132
133
134
135
136
137
138
                        // If it's a valid user id try to use Profile API
                        // because directory only returns users that are in public rooms or share a room with you, where as
                        // profile will work other federations
                        if (!MatrixPatterns.isUserId(search)) {
                            searchObservable
                        } else {
                            val profileObservable = session.rx().getProfileInfo(search)
                                    .map { json ->
                                        User(
                                                userId = search,
                                                displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String,
                                                avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
                                        ).toOptional()
                                    }
139
140
141
142
143
                                    .onErrorReturn {
                                        // Profile API can be restricted and doesn't have to return result.
                                        // In this case allow inviting valid user ids.
                                        User(
                                                userId = search,
Onuray Sahin's avatar
Onuray Sahin committed
144
                                                displayName = null,
145
146
147
                                                avatarUrl = null
                                        ).toOptional()
                                    }
Valere's avatar
Valere committed
148

Benoit Marty's avatar
Benoit Marty committed
149
150
151
152
153
154
155
156
157
158
159
160
161
                            Single.zip(
                                    searchObservable,
                                    profileObservable,
                                    { searchResults, optionalProfile ->
                                        val profile = optionalProfile.getOrNull() ?: return@zip searchResults
                                        val searchContainsProfile = searchResults.any { it.userId == profile.userId }
                                        if (searchContainsProfile) {
                                            searchResults
                                        } else {
                                            listOf(profile) + searchResults
                                        }
                                    }
                            )
Valere's avatar
Valere committed
162
                        }
163
164
165
166
167
168
169
170
171
                    }
                    stream.toAsync {
                        copy(directoryUsers = it)
                    }
                }
                .subscribe()
                .disposeOnClear()
    }

172
173
174
    private fun handleSelectUser(action: UserListAction.AddPendingSelection) = withState { state ->
        val selections = state.pendingSelections.toggle(action.pendingSelection, singleElement = state.singleSelection)
        setState { copy(pendingSelections = selections) }
175
176
    }

177
178
179
    private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingSelection) = withState { state ->
        val selections = state.pendingSelections.minus(action.pendingSelection)
        setState { copy(pendingSelections = selections) }
180
181
    }
}