Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
A
AloqaIM-Android
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
AloqaIM-Android
Commits
35285092
Commit
35285092
authored
May 28, 2018
by
Lucio Maciel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Room database on Chat Rooms screen.
parent
54918bf1
Changes
41
Show whitespace changes
Inline
Side-by-side
Showing
41 changed files
with
1453 additions
and
185 deletions
+1453
-185
build.gradle
app/build.gradle
+2
-0
ChatRoomPresenter.kt
...rocket/android/chatroom/presentation/ChatRoomPresenter.kt
+1
-1
ChatRoomFragment.kt
.../java/chat/rocket/android/chatroom/ui/ChatRoomFragment.kt
+1
-1
HeaderItemHolder.kt
...chat/rocket/android/chatrooms/adapter/HeaderItemHolder.kt
+3
-0
HeaderViewHolder.kt
...chat/rocket/android/chatrooms/adapter/HeaderViewHolder.kt
+12
-0
ItemHolder.kt
.../java/chat/rocket/android/chatrooms/adapter/ItemHolder.kt
+5
-0
RoomItemHolder.kt
...a/chat/rocket/android/chatrooms/adapter/RoomItemHolder.kt
+5
-0
RoomMapper.kt
.../java/chat/rocket/android/chatrooms/adapter/RoomMapper.kt
+141
-0
RoomViewHolder.kt
...a/chat/rocket/android/chatrooms/adapter/RoomViewHolder.kt
+77
-0
RoomsAdapter.kt
...ava/chat/rocket/android/chatrooms/adapter/RoomsAdapter.kt
+58
-0
ViewHolder.kt
.../java/chat/rocket/android/chatrooms/adapter/ViewHolder.kt
+17
-0
Room.kt
.../java/chat/rocket/android/chatrooms/adapter/model/Room.kt
+16
-0
ChatRoomsFragmentModule.kt
...at/rocket/android/chatrooms/di/ChatRoomsFragmentModule.kt
+71
-1
FetchChatRoomsInteractor.kt
...cket/android/chatrooms/domain/FetchChatRoomsInteractor.kt
+74
-0
ChatRoomsRepository.kt
...t/android/chatrooms/infrastructure/ChatRoomsRepository.kt
+27
-0
ChatRoomsPresenter.kt
...cket/android/chatrooms/presentation/ChatRoomsPresenter.kt
+33
-11
ChatRoomsAdapter.kt
...java/chat/rocket/android/chatrooms/ui/ChatRoomsAdapter.kt
+1
-1
ChatRoomsFragment.kt
...ava/chat/rocket/android/chatrooms/ui/ChatRoomsFragment.kt
+81
-131
ChatRoomsViewModel.kt
.../rocket/android/chatrooms/viewmodel/ChatRoomsViewModel.kt
+51
-0
ChatRoomsViewModelFactory.kt
.../android/chatrooms/viewmodel/ChatRoomsViewModelFactory.kt
+19
-0
AppModule.kt
.../main/java/chat/rocket/android/dagger/module/AppModule.kt
+2
-2
BaseDao.kt
app/src/main/java/chat/rocket/android/db/BaseDao.kt
+12
-0
ChatRoomDao.kt
app/src/main/java/chat/rocket/android/db/ChatRoomDao.kt
+102
-0
DatabaseManager.kt
app/src/main/java/chat/rocket/android/db/DatabaseManager.kt
+307
-0
DatabaseManagerFactory.kt
...ain/java/chat/rocket/android/db/DatabaseManagerFactory.kt
+23
-0
RCDatabase.kt
app/src/main/java/chat/rocket/android/db/RCDatabase.kt
+17
-0
UserDao.kt
app/src/main/java/chat/rocket/android/db/UserDao.kt
+46
-0
ChatRoomEntity.kt
.../main/java/chat/rocket/android/db/model/ChatRoomEntity.kt
+49
-0
UserEntity.kt
app/src/main/java/chat/rocket/android/db/model/UserEntity.kt
+25
-0
LocalRepository.kt
...ava/chat/rocket/android/infrastructure/LocalRepository.kt
+0
-1
MembersFragment.kt
...in/java/chat/rocket/android/members/ui/MembersFragment.kt
+1
-1
ConnectionManager.kt
...ocket/android/server/infraestructure/ConnectionManager.kt
+98
-8
ConnectionManagerFactory.kt
...ndroid/server/infraestructure/ConnectionManagerFactory.kt
+6
-2
Date.kt
...src/main/java/chat/rocket/android/util/extensions/Date.kt
+8
-0
Numbers.kt
.../main/java/chat/rocket/android/util/extensions/Numbers.kt
+9
-0
ic_hashtag_12dp.xml
app/src/main/res/drawable/ic_hashtag_12dp.xml
+22
-0
ic_hashtag_unread_12dp.xml
app/src/main/res/drawable/ic_hashtag_unread_12dp.xml
+8
-16
ic_lock_12_dp.xml
app/src/main/res/drawable/ic_lock_12_dp.xml
+2
-4
ic_lock_unread_12_dp.xml
app/src/main/res/drawable/ic_lock_unread_12_dp.xml
+16
-0
item_chat.xml
app/src/main/res/layout/item_chat.xml
+2
-2
item_chatroom_header.xml
app/src/main/res/layout/item_chatroom_header.xml
+3
-3
No files found.
app/build.gradle
View file @
35285092
...
...
@@ -107,6 +107,8 @@ dependencies {
implementation
libraries
.
aVLoadingIndicatorView
implementation
"com.github.luciofm:livedata-ktx:b1e8bbc25a"
implementation
(
'com.crashlytics.sdk.android:crashlytics:2.9.2@aar'
)
{
transitive
=
true
}
...
...
app/src/main/java/chat/rocket/android/chatroom/presentation/ChatRoomPresenter.kt
View file @
35285092
...
...
@@ -577,7 +577,7 @@ class ChatRoomPresenter @Inject constructor(
try
{
val
chatRooms
=
getChatRoomsInteractor
.
getAll
(
currentServer
)
.
filterNot
{
it
.
type
is
RoomType
.
DirectMessage
||
it
.
type
is
RoomType
.
Live
c
hat
it
.
type
is
RoomType
.
DirectMessage
||
it
.
type
is
RoomType
.
Live
C
hat
}
.
map
{
chatRoom
->
val
name
=
chatRoom
.
name
...
...
app/src/main/java/chat/rocket/android/chatroom/ui/ChatRoomFragment.kt
View file @
35285092
...
...
@@ -595,7 +595,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
recycler_view
.
itemAnimator
=
DefaultItemAnimator
()
endlessRecyclerViewScrollListener
=
object
:
EndlessRecyclerViewScrollListener
(
recycler_view
.
layoutManager
as
LinearLayoutManager
)
{
override
fun
onLoadMore
(
page
:
Int
,
totalItemsCount
:
Int
,
recyclerView
:
RecyclerView
?
)
{
override
fun
onLoadMore
(
page
:
Int
,
totalItemsCount
:
Int
,
recyclerView
:
RecyclerView
)
{
presenter
.
loadMessages
(
chatRoomId
,
chatRoomType
,
page
*
30L
)
}
}
...
...
app/src/main/java/chat/rocket/android/chatrooms/adapter/HeaderItemHolder.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.adapter
data class
HeaderItemHolder
(
override
val
data
:
String
)
:
ItemHolder
<
String
>
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/adapter/HeaderViewHolder.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.adapter
import
android.view.View
import
kotlinx.android.synthetic.main.item_chatroom_header.view.*
class
HeaderViewHolder
(
itemView
:
View
)
:
ViewHolder
<
HeaderItemHolder
>(
itemView
)
{
override
fun
bindViews
(
data
:
HeaderItemHolder
)
{
with
(
itemView
)
{
text_chatroom_header
.
text
=
data
.
data
}
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/adapter/ItemHolder.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.adapter
interface
ItemHolder
<
T
>
{
val
data
:
T
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/adapter/RoomItemHolder.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.adapter
import
chat.rocket.android.chatrooms.adapter.model.Room
data class
RoomItemHolder
(
override
val
data
:
Room
)
:
ItemHolder
<
Room
>
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/adapter/RoomMapper.kt
View file @
35285092
package
chat.rocket.android.chatrooms.adapter
import
android.app.Application
import
android.text.SpannableStringBuilder
import
androidx.core.content.ContextCompat
import
androidx.core.text.bold
import
androidx.core.text.color
import
chat.rocket.android.R
import
chat.rocket.android.chatrooms.adapter.model.Room
import
chat.rocket.android.db.model.ChatRoom
import
chat.rocket.android.infrastructure.LocalRepository
import
chat.rocket.android.infrastructure.checkIfMyself
import
chat.rocket.android.server.domain.PublicSettings
import
chat.rocket.android.server.domain.showLastMessage
import
chat.rocket.android.server.domain.useRealName
import
chat.rocket.android.util.extensions.avatarUrl
import
chat.rocket.android.util.extensions.date
import
chat.rocket.android.util.extensions.localDateTime
import
chat.rocket.common.model.RoomType
import
chat.rocket.common.model.roomTypeOf
import
chat.rocket.common.model.userStatusOf
class
RoomMapper
(
private
val
context
:
Application
,
private
val
settings
:
PublicSettings
,
private
val
localRepository
:
LocalRepository
,
private
val
serverUrl
:
String
)
{
private
val
nameUnreadColor
=
ContextCompat
.
getColor
(
context
,
R
.
color
.
colorPrimaryText
)
private
val
nameColor
=
ContextCompat
.
getColor
(
context
,
R
.
color
.
colorSecondaryText
)
private
val
dateUnreadColor
=
ContextCompat
.
getColor
(
context
,
R
.
color
.
colorAccent
)
private
val
dateColor
=
ContextCompat
.
getColor
(
context
,
R
.
color
.
colorSecondaryText
)
private
val
messageUnreadColor
=
ContextCompat
.
getColor
(
context
,
android
.
R
.
color
.
primary_text_light
)
private
val
messageColor
=
ContextCompat
.
getColor
(
context
,
R
.
color
.
colorSecondaryText
)
fun
map
(
rooms
:
List
<
ChatRoom
>,
grouped
:
Boolean
):
List
<
ItemHolder
<*
>>
{
val
list
=
ArrayList
<
ItemHolder
<*>>(
rooms
.
size
+
4
)
var
lastType
:
String
?
=
null
rooms
.
forEach
{
room
->
if
(
grouped
&&
lastType
!=
room
.
chatRoom
.
type
)
{
list
.
add
(
HeaderItemHolder
(
roomType
(
room
.
chatRoom
.
type
)))
}
list
.
add
(
RoomItemHolder
(
map
(
room
)))
lastType
=
room
.
chatRoom
.
type
}
return
list
}
fun
map
(
chatRoom
:
ChatRoom
):
Room
{
return
with
(
chatRoom
.
chatRoom
)
{
val
isUnread
=
alert
||
unread
>
0
val
type
=
roomTypeOf
(
type
)
val
status
=
chatRoom
.
status
?.
let
{
userStatusOf
(
it
)
}
val
roomName
=
mapName
(
name
,
chatRoom
.
userFullname
,
isUnread
)
val
timestamp
=
mapDate
(
lastMessageTimestamp
?:
updatedAt
,
isUnread
)
val
avatar
=
if
(
type
is
RoomType
.
DirectMessage
)
{
serverUrl
.
avatarUrl
(
name
)
}
else
{
serverUrl
.
avatarUrl
(
name
,
isGroupOrChannel
=
true
)
}
val
unread
=
mapUnread
(
unread
)
val
lastMessage
=
mapLastMessage
(
chatRoom
.
lastMessageUserName
,
chatRoom
.
lastMessageUserFullName
,
lastMessageText
,
isUnread
)
Room
(
id
=
id
,
name
=
roomName
,
type
=
type
,
avatar
=
avatar
,
date
=
timestamp
,
unread
=
unread
,
alert
=
isUnread
,
lastMessage
=
lastMessage
,
status
=
status
)
}
}
private
fun
roomType
(
type
:
String
):
String
{
val
resources
=
context
.
resources
return
when
(
type
)
{
RoomType
.
CHANNEL
->
resources
.
getString
(
R
.
string
.
header_channel
)
RoomType
.
PRIVATE_GROUP
->
resources
.
getString
(
R
.
string
.
header_private_groups
)
RoomType
.
DIRECT_MESSAGE
->
resources
.
getString
(
R
.
string
.
header_direct_messages
)
RoomType
.
LIVECHAT
->
resources
.
getString
(
R
.
string
.
header_live_chats
)
else
->
resources
.
getString
(
R
.
string
.
header_unknown
)
}
}
private
fun
mapLastMessage
(
name
:
String
?,
fullName
:
String
?,
text
:
String
?,
unread
:
Boolean
):
CharSequence
?
{
return
if
(!
settings
.
showLastMessage
())
{
null
}
else
if
(
name
!=
null
&&
fullName
!=
null
&&
text
!=
null
)
{
val
user
=
if
(
localRepository
.
checkIfMyself
(
name
))
{
"${context.getString(R.string.msg_you)}: "
}
else
{
"${mapName(name, fullName, unread)}: "
}
val
color
=
if
(
unread
)
messageUnreadColor
else
messageColor
SpannableStringBuilder
()
.
color
(
color
)
{
bold
{
append
(
user
)
}
append
(
text
)
}
}
else
{
context
.
getText
(
R
.
string
.
msg_no_messages_yet
)
}
}
private
fun
mapName
(
name
:
String
,
fullName
:
String
?,
unread
:
Boolean
):
CharSequence
{
val
roomName
=
if
(
settings
.
useRealName
())
{
fullName
?:
name
}
else
{
name
}
val
color
=
if
(
unread
)
nameUnreadColor
else
nameColor
return
SpannableStringBuilder
()
.
color
(
color
)
{
append
(
roomName
)
}
}
private
fun
mapUnread
(
unread
:
Long
):
String
?
{
return
when
(
unread
)
{
0L
->
null
in
1
..
99
->
unread
.
toString
()
else
->
context
.
getString
(
R
.
string
.
msg_more_than_ninety_nine_unread_messages
)
}
}
private
fun
mapDate
(
date
:
Long
?,
unread
:
Boolean
):
CharSequence
?
{
return
date
?.
localDateTime
()
?.
date
(
context
)
?.
let
{
val
color
=
if
(
unread
)
dateUnreadColor
else
dateColor
SpannableStringBuilder
().
color
(
color
)
{
append
(
it
)
}
}
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/adapter/RoomViewHolder.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.adapter
import
android.content.res.Resources
import
android.graphics.drawable.Drawable
import
android.view.View
import
androidx.core.view.isGone
import
androidx.core.view.isVisible
import
chat.rocket.android.R
import
chat.rocket.common.model.RoomType
import
chat.rocket.common.model.UserStatus
import
kotlinx.android.synthetic.main.item_chat.view.*
import
kotlinx.android.synthetic.main.unread_messages_badge.view.*
class
RoomViewHolder
(
itemView
:
View
)
:
ViewHolder
<
RoomItemHolder
>(
itemView
)
{
private
val
resources
:
Resources
=
itemView
.
resources
private
val
channelUnread
:
Drawable
=
resources
.
getDrawable
(
R
.
drawable
.
ic_hashtag_unread_12dp
)
private
val
channel
:
Drawable
=
resources
.
getDrawable
(
R
.
drawable
.
ic_hashtag_12dp
)
private
val
groupUnread
:
Drawable
=
resources
.
getDrawable
(
R
.
drawable
.
ic_lock_unread_12_dp
)
private
val
group
:
Drawable
=
resources
.
getDrawable
(
R
.
drawable
.
ic_lock_12_dp
)
private
val
online
:
Drawable
=
resources
.
getDrawable
(
R
.
drawable
.
ic_status_online_12dp
)
private
val
away
:
Drawable
=
resources
.
getDrawable
(
R
.
drawable
.
ic_status_away_12dp
)
private
val
busy
:
Drawable
=
resources
.
getDrawable
(
R
.
drawable
.
ic_status_busy_12dp
)
private
val
offline
:
Drawable
=
resources
.
getDrawable
(
R
.
drawable
.
ic_status_invisible_12dp
)
override
fun
bindViews
(
data
:
RoomItemHolder
)
{
val
room
=
data
.
data
with
(
itemView
)
{
image_avatar
.
setImageURI
(
room
.
avatar
)
text_chat_name
.
text
=
room
.
name
if
(
room
.
lastMessage
!=
null
)
{
text_last_message
.
isVisible
=
true
text_last_message
.
text
=
room
.
lastMessage
}
else
{
text_last_message
.
isGone
=
true
}
if
(
room
.
date
!=
null
)
{
text_last_message_date_time
.
isVisible
=
true
text_last_message_date_time
.
text
=
room
.
date
}
else
{
text_last_message_date_time
.
isGone
=
true
}
if
(
room
.
unread
!=
null
)
{
text_total_unread_messages
.
isVisible
=
true
text_total_unread_messages
.
text
=
room
.
unread
}
else
{
text_total_unread_messages
.
isGone
=
true
}
if
(
room
.
status
!=
null
&&
room
.
type
is
RoomType
.
DirectMessage
)
{
image_chat_icon
.
setImageDrawable
(
getStatusDrawable
(
room
.
status
))
}
else
{
image_chat_icon
.
setImageDrawable
(
getRoomDrawable
(
room
.
type
,
room
.
alert
))
}
}
}
private
fun
getRoomDrawable
(
type
:
RoomType
,
alert
:
Boolean
):
Drawable
?
{
return
when
(
type
)
{
is
RoomType
.
Channel
->
if
(
alert
)
channelUnread
else
channel
is
RoomType
.
PrivateGroup
->
if
(
alert
)
groupUnread
else
group
else
->
null
}
}
private
fun
getStatusDrawable
(
status
:
UserStatus
):
Drawable
{
return
when
(
status
)
{
is
UserStatus
.
Online
->
online
is
UserStatus
.
Away
->
away
is
UserStatus
.
Busy
->
busy
else
->
offline
}
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/adapter/RoomsAdapter.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.adapter
import
android.view.ViewGroup
import
androidx.recyclerview.widget.RecyclerView
import
chat.rocket.android.R
import
chat.rocket.android.util.extensions.inflate
class
RoomsAdapter
:
RecyclerView
.
Adapter
<
ViewHolder
<*>>()
{
init
{
setHasStableIds
(
true
)
}
var
values
:
List
<
ItemHolder
<*>>
=
ArrayList
(
0
)
set
(
items
)
{
field
=
items
notifyDataSetChanged
()
}
override
fun
onCreateViewHolder
(
parent
:
ViewGroup
,
viewType
:
Int
):
ViewHolder
<
*
>
{
if
(
viewType
==
0
)
{
val
view
=
parent
.
inflate
(
R
.
layout
.
item_chat
)
return
RoomViewHolder
(
view
)
}
else
if
(
viewType
==
1
)
{
val
view
=
parent
.
inflate
(
R
.
layout
.
item_chatroom_header
)
return
HeaderViewHolder
(
view
)
}
throw
IllegalStateException
(
"View type must be either Room or Header"
)
}
override
fun
getItemCount
()
=
values
.
size
override
fun
getItemId
(
position
:
Int
):
Long
{
val
item
=
values
[
position
]
return
when
(
item
)
{
is
HeaderItemHolder
->
item
.
data
.
hashCode
().
toLong
()
is
RoomItemHolder
->
item
.
data
.
id
.
hashCode
().
toLong
()
else
->
throw
IllegalStateException
(
"View type must be either Room or Header"
)
}
}
override
fun
getItemViewType
(
position
:
Int
):
Int
{
return
when
(
values
[
position
])
{
is
RoomItemHolder
->
0
is
HeaderItemHolder
->
1
else
->
throw
IllegalStateException
(
"View type must be either Room or Header"
)
}
}
override
fun
onBindViewHolder
(
holder
:
ViewHolder
<
*
>,
position
:
Int
)
{
if
(
holder
is
RoomViewHolder
)
{
holder
.
bind
(
values
[
position
]
as
RoomItemHolder
)
}
else
if
(
holder
is
HeaderViewHolder
)
{
holder
.
bind
(
values
[
position
]
as
HeaderItemHolder
)
}
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/adapter/ViewHolder.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.adapter
import
android.view.View
import
androidx.recyclerview.widget.RecyclerView
abstract
class
ViewHolder
<
T
:
ItemHolder
<*>>(
itemView
:
View
)
:
RecyclerView
.
ViewHolder
(
itemView
)
{
var
data
:
T
?
=
null
fun
bind
(
data
:
T
)
{
this
.
data
=
data
bindViews
(
data
)
}
abstract
fun
bindViews
(
data
:
T
)
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/adapter/model/Room.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.adapter.model
import
chat.rocket.common.model.RoomType
import
chat.rocket.common.model.UserStatus
data class
Room
(
val
id
:
String
,
val
type
:
RoomType
,
val
name
:
CharSequence
,
val
avatar
:
String
,
val
date
:
CharSequence
?,
val
unread
:
String
?,
val
alert
:
Boolean
,
val
lastMessage
:
CharSequence
?,
val
status
:
UserStatus
?
)
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/di/ChatRoomsFragmentModule.kt
View file @
35285092
package
chat.rocket.android.chatrooms.di
import
android.app.Application
import
androidx.lifecycle.LifecycleOwner
import
chat.rocket.android.chatrooms.adapter.RoomMapper
import
chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import
chat.rocket.android.chatrooms.presentation.ChatRoomsView
import
chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import
chat.rocket.android.dagger.scope.PerFragment
import
chat.rocket.android.db.ChatRoomDao
import
chat.rocket.android.db.DatabaseManager
import
chat.rocket.android.db.DatabaseManagerFactory
import
chat.rocket.android.infrastructure.LocalRepository
import
chat.rocket.android.server.domain.GetCurrentServerInteractor
import
chat.rocket.android.server.domain.PublicSettings
import
chat.rocket.android.server.domain.SettingsRepository
import
chat.rocket.android.server.infraestructure.ConnectionManager
import
chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import
chat.rocket.android.server.infraestructure.RocketChatClientFactory
import
chat.rocket.core.RocketChatClient
import
dagger.Module
import
dagger.Provides
import
javax.inject.Named
@Module
@PerFragment
class
ChatRoomsFragmentModule
{
@Provides
@PerFragment
fun
chatRoomsView
(
frag
:
ChatRoomsFragment
):
ChatRoomsView
{
return
frag
}
@Provides
@PerFragment
fun
provideLifecycleOwner
(
frag
:
ChatRoomsFragment
):
LifecycleOwner
{
return
frag
}
@Provides
@PerFragment
@Named
(
"currentServer"
)
fun
provideCurrentServer
(
currentServerInteractor
:
GetCurrentServerInteractor
):
String
{
return
currentServerInteractor
.
get
()
!!
}
@Provides
@PerFragment
fun
provideRocketChatClient
(
factory
:
RocketChatClientFactory
,
@Named
(
"currentServer"
)
currentServer
:
String
):
RocketChatClient
{
return
factory
.
create
(
currentServer
)
}
@Provides
@PerFragment
fun
provideDatabaseManager
(
factory
:
DatabaseManagerFactory
,
@Named
(
"currentServer"
)
currentServer
:
String
):
DatabaseManager
{
return
factory
.
create
(
currentServer
)
}
@Provides
@PerFragment
fun
provideChatRoomDao
(
manager
:
DatabaseManager
):
ChatRoomDao
=
manager
.
chatRoomDao
()
@Provides
@PerFragment
fun
provideConnectionManager
(
factory
:
ConnectionManagerFactory
,
@Named
(
"currentServer"
)
currentServer
:
String
):
ConnectionManager
{
return
factory
.
create
(
currentServer
)
}
@Provides
@PerFragment
fun
provideFetchChatRoomsInteractor
(
client
:
RocketChatClient
,
dbManager
:
DatabaseManager
):
FetchChatRoomsInteractor
{
return
FetchChatRoomsInteractor
(
client
,
dbManager
)
}
@Provides
@PerFragment
fun
providePublicSettings
(
repository
:
SettingsRepository
,
@Named
(
"currentServer"
)
currentServer
:
String
):
PublicSettings
{
return
repository
.
get
(
currentServer
)
}
@Provides
@PerFragment
fun
provideRoomMapper
(
context
:
Application
,
repository
:
SettingsRepository
,
localRepository
:
LocalRepository
,
@Named
(
"currentServer"
)
serverUrl
:
String
):
RoomMapper
{
return
RoomMapper
(
context
,
repository
.
get
(
serverUrl
),
localRepository
,
serverUrl
)
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/domain/FetchChatRoomsInteractor.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.domain
import
chat.rocket.android.db.DatabaseManager
import
chat.rocket.android.db.model.ChatRoomEntity
import
chat.rocket.android.db.model.UserEntity
import
chat.rocket.android.util.retryIO
import
chat.rocket.core.RocketChatClient
import
chat.rocket.core.internal.rest.chatRooms
import
chat.rocket.core.model.ChatRoom
import
chat.rocket.core.model.userId
import
kotlinx.coroutines.experimental.CommonPool
import
kotlinx.coroutines.experimental.launch
import
timber.log.Timber
class
FetchChatRoomsInteractor
(
private
val
client
:
RocketChatClient
,
private
val
dbManager
:
DatabaseManager
)
{
suspend
fun
refreshChatRooms
()
{
launch
(
CommonPool
)
{
try
{
val
rooms
=
retryIO
(
"fetch chatRooms"
,
times
=
10
,
initialDelay
=
200
,
maxDelay
=
2000
)
{
client
.
chatRooms
().
update
.
map
{
room
->
mapChatRoom
(
room
)
}
}
Timber
.
d
(
"Refreshing rooms: $rooms"
)
dbManager
.
insert
(
rooms
)
}
catch
(
ex
:
Exception
)
{
Timber
.
d
(
ex
,
"Error getting chatrooms"
)
}
}
}
private
suspend
fun
mapChatRoom
(
room
:
ChatRoom
):
ChatRoomEntity
{
with
(
room
)
{
val
userId
=
userId
()
if
(
userId
!=
null
&&
dbManager
.
findUser
(
userId
)
==
null
)
{
Timber
.
d
(
"Missing user, inserting: $userId"
)
dbManager
.
insert
(
UserEntity
(
userId
))
}
lastMessage
?.
sender
?.
let
{
user
->
if
(
dbManager
.
findUser
(
user
.
id
!!
)
==
null
)
{
Timber
.
d
(
"Missing last message user, inserting: ${user.id}"
)
dbManager
.
insert
(
UserEntity
(
user
.
id
!!
,
user
.
username
,
user
.
name
))
}
}
return
ChatRoomEntity
(
id
=
id
,
subscriptionId
=
subscriptionId
,
type
=
type
.
toString
(),
name
=
name
,
userId
=
userId
,
readonly
=
readonly
,
isDefault
=
default
,
favorite
=
favorite
,
open
=
open
,
alert
=
alert
,
unread
=
unread
,
userMentions
=
userMentions
,
groupMentions
=
groupMentions
,
updatedAt
=
updatedAt
,
timestamp
=
timestamp
,
lastSeen
=
lastSeen
,
lastMessageText
=
lastMessage
?.
message
,
lastMessageUserId
=
lastMessage
?.
sender
?.
id
,
lastMessageTimestamp
=
lastMessage
?.
timestamp
)
}
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/infrastructure/ChatRoomsRepository.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.infrastructure
import
androidx.lifecycle.LiveData
import
chat.rocket.android.db.ChatRoomDao
import
chat.rocket.android.db.model.ChatRoom
import
javax.inject.Inject
class
ChatRoomsRepository
@Inject
constructor
(
val
dao
:
ChatRoomDao
){
fun
getChatRooms
(
order
:
Order
):
LiveData
<
List
<
ChatRoom
>>
{
return
when
(
order
)
{
Order
.
ACTIVITY
->
dao
.
getAll
()
Order
.
GROUPED_ACTIVITY
->
dao
.
getAllGrouped
()
Order
.
NAME
->
dao
.
getAllAlphabetically
()
Order
.
GROUPED_NAME
->
dao
.
getAllAlphabeticallyGrouped
()
}
}
fun
fetchChatRooms
()
{
}
enum
class
Order
{
ACTIVITY
,
GROUPED_ACTIVITY
,
NAME
,
GROUPED_NAME
,
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/presentation/ChatRoomsPresenter.kt
View file @
35285092
...
...
@@ -2,6 +2,7 @@ package chat.rocket.android.chatrooms.presentation
import
chat.rocket.android.R
import
chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import
chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import
chat.rocket.android.core.lifecycle.CancelStrategy
import
chat.rocket.android.helper.ChatRoomsSortOrder
import
chat.rocket.android.helper.Constants
...
...
@@ -9,9 +10,18 @@ import chat.rocket.android.helper.SharedPreferenceHelper
import
chat.rocket.android.helper.UserHelper
import
chat.rocket.android.infrastructure.LocalRepository
import
chat.rocket.android.main.presentation.MainNavigator
import
chat.rocket.android.server.domain.*
import
chat.rocket.android.server.domain.GetActiveUsersInteractor
import
chat.rocket.android.server.domain.GetChatRoomsInteractor
import
chat.rocket.android.server.domain.JobSchedulerInteractor
import
chat.rocket.android.server.domain.PermissionsInteractor
import
chat.rocket.android.server.domain.RefreshSettingsInteractor
import
chat.rocket.android.server.domain.SaveActiveUsersInteractor
import
chat.rocket.android.server.domain.SaveChatRoomsInteractor
import
chat.rocket.android.server.domain.SettingsRepository
import
chat.rocket.android.server.domain.hasShowLastMessage
import
chat.rocket.android.server.domain.showLastMessage
import
chat.rocket.android.server.domain.useRealName
import
chat.rocket.android.server.infraestructure.ConnectionManager
import
chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import
chat.rocket.android.server.infraestructure.chatRooms
import
chat.rocket.android.server.infraestructure.state
import
chat.rocket.android.util.extensions.launchUI
...
...
@@ -21,6 +31,7 @@ import chat.rocket.common.model.BaseRoom
import
chat.rocket.common.model.RoomType
import
chat.rocket.common.model.SimpleUser
import
chat.rocket.common.model.User
import
chat.rocket.common.model.roomTypeOf
import
chat.rocket.common.util.ifNull
import
chat.rocket.core.internal.model.Subscription
import
chat.rocket.core.internal.realtime.socket.model.State
...
...
@@ -31,18 +42,24 @@ import chat.rocket.core.internal.rest.permissions
import
chat.rocket.core.internal.rest.spotlight
import
chat.rocket.core.model.ChatRoom
import
chat.rocket.core.model.Room
import
kotlinx.coroutines.experimental.*
import
kotlinx.coroutines.experimental.CommonPool
import
kotlinx.coroutines.experimental.Deferred
import
kotlinx.coroutines.experimental.android.UI
import
kotlinx.coroutines.experimental.async
import
kotlinx.coroutines.experimental.channels.Channel
import
kotlinx.coroutines.experimental.delay
import
kotlinx.coroutines.experimental.launch
import
timber.log.Timber
import
javax.inject.Inject
import
javax.inject.Named
import
kotlin.reflect.KProperty1
class
ChatRoomsPresenter
@Inject
constructor
(
private
val
view
:
ChatRoomsView
,
private
val
strategy
:
CancelStrategy
,
private
val
navigator
:
MainNavigator
,
private
val
serverInteractor
:
GetCurrentServerInteractor
,
@Named
(
"currentServer"
)
private
val
currentServer
:
String
,
private
val
manager
:
ConnectionManager
,
private
val
getChatRoomsInteractor
:
GetChatRoomsInteractor
,
private
val
saveChatRoomsInteractor
:
SaveChatRoomsInteractor
,
private
val
saveActiveUsersInteractor
:
SaveActiveUsersInteractor
,
...
...
@@ -53,11 +70,9 @@ class ChatRoomsPresenter @Inject constructor(
private
val
permissionsInteractor
:
PermissionsInteractor
,
private
val
localRepository
:
LocalRepository
,
private
val
userHelper
:
UserHelper
,
settingsRepository
:
SettingsRepository
,
factory
:
ConnectionManagerFac
tory
private
val
roomsInteractor
:
FetchChatRoomsInteractor
,
settingsRepository
:
SettingsReposi
tory
)
{
private
val
manager
:
ConnectionManager
=
factory
.
create
(
serverInteractor
.
get
()
!!
)
private
val
currentServer
=
serverInteractor
.
get
()
!!
private
val
client
=
manager
.
client
private
var
reloadJob
:
Deferred
<
List
<
ChatRoom
>>?
=
null
private
val
settings
=
settingsRepository
.
get
(
currentServer
)
...
...
@@ -79,6 +94,8 @@ class ChatRoomsPresenter @Inject constructor(
view
.
updateChatRooms
(
getUserChatRooms
())
val
permissions
=
retryIO
{
client
.
permissions
()
}
permissionsInteractor
.
saveAll
(
permissions
)
roomsInteractor
.
refreshChatRooms
()
}
catch
(
ex
:
RocketChatException
)
{
ex
.
message
?.
let
{
view
.
showMessage
(
it
)
...
...
@@ -145,7 +162,6 @@ class ChatRoomsPresenter @Inject constructor(
* ChatRooms returned are filtered by name.
*/
fun
chatRoomsByName
(
name
:
String
)
{
val
currentServer
=
serverInteractor
.
get
()
!!
launchUI
(
strategy
)
{
try
{
val
roomList
=
getChatRoomsInteractor
.
getAllByName
(
currentServer
,
name
)
...
...
@@ -187,7 +203,8 @@ class ChatRoomsPresenter @Inject constructor(
return
users
.
map
{
ChatRoom
(
id
=
it
.
id
,
type
=
RoomType
.
DIRECT_MESSAGE
,
subscriptionId
=
it
.
id
,
type
=
roomTypeOf
(
RoomType
.
DIRECT_MESSAGE
),
user
=
SimpleUser
(
username
=
it
.
username
,
name
=
it
.
name
,
id
=
null
),
status
=
if
(
it
.
name
!=
null
)
{
getActiveUsersInteractor
.
getActiveUserByUsername
(
currentServer
,
it
.
name
!!
)
...
...
@@ -221,6 +238,7 @@ class ChatRoomsPresenter @Inject constructor(
return
rooms
.
map
{
ChatRoom
(
id
=
it
.
id
,
subscriptionId
=
it
.
id
,
type
=
it
.
type
,
user
=
it
.
user
,
status
=
if
(
it
.
name
!=
null
)
{
...
...
@@ -304,7 +322,7 @@ class ChatRoomsPresenter @Inject constructor(
is
RoomType
.
Channel
->
Constants
.
CHATROOM_CHANNEL
is
RoomType
.
PrivateGroup
->
Constants
.
CHATROOM_PRIVATE_GROUP
is
RoomType
.
DirectMessage
->
Constants
.
CHATROOM_DM
is
RoomType
.
Live
c
hat
->
Constants
.
CHATROOM_LIVE_CHAT
is
RoomType
.
Live
C
hat
->
Constants
.
CHATROOM_LIVE_CHAT
else
->
0
}
}
...
...
@@ -314,6 +332,7 @@ class ChatRoomsPresenter @Inject constructor(
chatRooms
.
forEach
{
val
newRoom
=
ChatRoom
(
id
=
it
.
id
,
subscriptionId
=
it
.
id
,
type
=
it
.
type
,
user
=
it
.
user
,
status
=
getActiveUsersInteractor
.
getActiveUserByUsername
(
...
...
@@ -458,6 +477,7 @@ class ChatRoomsPresenter @Inject constructor(
chatRoom
?.
apply
{
val
newRoom
=
ChatRoom
(
id
=
room
.
id
,
subscriptionId
=
this
.
subscriptionId
,
type
=
room
.
type
,
user
=
room
.
user
,
status
=
getActiveUsersInteractor
.
getActiveUserByUsername
(
...
...
@@ -498,6 +518,7 @@ class ChatRoomsPresenter @Inject constructor(
chatRoom
?.
apply
{
val
newRoom
=
ChatRoom
(
id
=
subscription
.
roomId
,
subscriptionId
=
subscription
.
id
,
type
=
subscription
.
type
,
user
=
user
,
status
=
getActiveUsersInteractor
.
getActiveUserByUsername
(
...
...
@@ -576,6 +597,7 @@ class ChatRoomsPresenter @Inject constructor(
getChatRoomsInteractor
.
getByName
(
currentServer
,
username
)
?.
let
{
val
newRoom
=
ChatRoom
(
id
=
it
.
id
,
subscriptionId
=
it
.
id
,
type
=
it
.
type
,
user
=
it
.
user
,
status
=
status
,
...
...
app/src/main/java/chat/rocket/android/chatrooms/ui/ChatRoomsAdapter.kt
View file @
35285092
...
...
@@ -92,7 +92,7 @@ class ChatRoomsAdapter(
private
fun
bindIcon
(
chatRoom
:
ChatRoom
,
imageView
:
ImageView
)
{
val
drawable
=
when
(
chatRoom
.
type
)
{
is
RoomType
.
Channel
->
DrawableHelper
.
getDrawableFromId
(
R
.
drawable
.
ic_hashtag_12dp
,
R
.
drawable
.
ic_hashtag_
unread_
12dp
,
context
)
is
RoomType
.
PrivateGroup
->
DrawableHelper
.
getDrawableFromId
(
...
...
app/src/main/java/chat/rocket/android/chatrooms/ui/ChatRoomsFragment.kt
View file @
35285092
package
chat.rocket.android.chatrooms.ui
import
android.app.AlertDialog
import
android.content.Context
import
android.content.SharedPreferences
import
android.os.Bundle
import
android.os.Handler
import
androidx.fragment.app.Fragment
import
android.view.LayoutInflater
import
android.view.Menu
import
android.view.MenuInflater
import
android.view.MenuItem
import
android.view.View
import
android.view.ViewGroup
import
android.widget.CheckBox
import
android.widget.RadioGroup
import
androidx.appcompat.app.AppCompatActivity
import
androidx.recyclerview.widget.DiffUtil
import
androidx.appcompat.widget.SearchView
import
androidx.fragment.app.Fragment
import
androidx.lifecycle.Observer
import
androidx.lifecycle.ViewModelProviders
import
androidx.recyclerview.widget.DefaultItemAnimator
import
androidx.recyclerview.widget.LinearLayoutManager
import
androidx.appcompat.widget.SearchView
import
android.view.*
import
android.widget.CheckBox
import
android.widget.RadioGroup
import
androidx.core.view.isVisible
import
chat.rocket.android.R
import
chat.rocket.android.chatrooms.adapter.RoomsAdapter
import
chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository
import
chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import
chat.rocket.android.chatrooms.presentation.ChatRoomsView
import
chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModel
import
chat.rocket.android.chatrooms.viewmodel.ChatRoomsViewModelFactory
import
chat.rocket.android.helper.ChatRoomsSortOrder
import
chat.rocket.android.helper.Constants
import
chat.rocket.android.helper.SharedPreferenceHelper
import
chat.rocket.android.infrastructure.LocalRepository
import
chat.rocket.android.server.domain.GetCurrentServerInteractor
import
chat.rocket.android.server.domain.SettingsRepository
import
chat.rocket.android.util.extensions.*
import
chat.rocket.android.util.extensions.fadeIn
import
chat.rocket.android.util.extensions.fadeOut
import
chat.rocket.android.util.extensions.inflate
import
chat.rocket.android.util.extensions.setVisible
import
chat.rocket.android.util.extensions.showToast
import
chat.rocket.android.util.extensions.ui
import
chat.rocket.android.widget.DividerItemDecoration
import
chat.rocket.common.model.RoomType
import
chat.rocket.core.internal.realtime.socket.model.State
import
chat.rocket.core.model.ChatRoom
import
dagger.android.support.AndroidSupportInjection
import
kotlinx.android.synthetic.main.fragment_chat_rooms.*
import
kotlinx.coroutines.experimental.Job
import
kotlinx.coroutines.experimental.NonCancellable.isActive
import
timber.log.Timber
import
javax.inject.Inject
...
...
@@ -40,18 +46,13 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject
lateinit
var
presenter
:
ChatRoomsPresenter
@Inject
lateinit
var
serverInteractor
:
GetCurrentServerInteractor
@Inject
lateinit
var
settingsRepository
:
SettingsRepository
@Inject
lateinit
var
localRepository
:
LocalRepository
private
lateinit
var
preferences
:
SharedPreferences
lateinit
var
factory
:
ChatRoomsViewModelFactory
lateinit
var
viewModel
:
ChatRoomsViewModel
private
var
searchView
:
SearchView
?
=
null
private
val
handler
=
Handler
()
private
var
listJob
:
Job
?
=
null
private
var
sectionedAdapter
:
SimpleSectionedRecyclerViewAdapter
?
=
null
companion
object
{
fun
newInstance
()
=
ChatRoomsFragment
()
}
...
...
@@ -60,7 +61,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
super
.
onCreate
(
savedInstanceState
)
AndroidSupportInjection
.
inject
(
this
)
setHasOptionsMenu
(
true
)
preferences
=
context
?.
getSharedPreferences
(
"temp"
,
Context
.
MODE_PRIVATE
)
!!
}
override
fun
onDestroy
()
{
...
...
@@ -78,14 +78,32 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override
fun
onViewCreated
(
view
:
View
,
savedInstanceState
:
Bundle
?)
{
super
.
onViewCreated
(
view
,
savedInstanceState
)
viewModel
=
ViewModelProviders
.
of
(
this
,
factory
).
get
(
ChatRoomsViewModel
::
class
.
java
)
val
adapter
=
RoomsAdapter
()
subscribeUi
(
adapter
)
setupToolbar
()
setupRecyclerView
()
presenter
.
loadChatRooms
()
}
override
fun
onDestroyView
()
{
listJob
?.
cancel
()
super
.
onDestroyView
()
private
fun
subscribeUi
(
adapter
:
RoomsAdapter
)
{
ui
{
recycler_view
.
layoutManager
=
LinearLayoutManager
(
it
,
LinearLayoutManager
.
VERTICAL
,
false
)
recycler_view
.
addItemDecoration
(
DividerItemDecoration
(
it
,
resources
.
getDimensionPixelSize
(
R
.
dimen
.
divider_item_decorator_bound_start
),
resources
.
getDimensionPixelSize
(
R
.
dimen
.
divider_item_decorator_bound_end
)))
recycler_view
.
itemAnimator
=
DefaultItemAnimator
()
recycler_view
.
adapter
=
adapter
viewModel
.
getChatRooms
().
observe
(
viewLifecycleOwner
,
Observer
{
rooms
->
rooms
?.
let
{
Timber
.
d
(
"Got items: $it"
)
adapter
.
values
=
it
}
})
updateSort
()
}
}
override
fun
onCreateOptionsMenu
(
menu
:
Menu
,
inflater
:
MenuInflater
)
{
...
...
@@ -108,6 +126,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override
fun
onOptionsItemSelected
(
item
:
MenuItem
):
Boolean
{
when
(
item
.
itemId
)
{
// TODO - simplify this
R
.
id
.
action_sort
->
{
val
dialogLayout
=
layoutInflater
.
inflate
(
R
.
layout
.
chatroom_sort_dialog
,
null
)
val
sortType
=
SharedPreferenceHelper
.
getInt
(
Constants
.
CHATROOM_SORT_TYPE_KEY
,
ChatRoomsSortOrder
.
ACTIVITY
)
...
...
@@ -127,22 +146,22 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
R
.
id
.
radio_sort_activity
->
1
else
->
1
})
presenter
.
updateSortedChatRooms
()
invalidateQueryOnSearch
()
}
})
groupByTypeCheckBox
.
isChecked
=
groupByType
groupByTypeCheckBox
.
setOnCheckedChangeListener
({
_
,
isChecked
->
SharedPreferenceHelper
.
putBoolean
(
Constants
.
CHATROOM_GROUP_BY_TYPE_KEY
,
isChecked
)
presenter
.
updateSortedChatRooms
()
invalidateQueryOnSearch
()
})
val
dialogSort
=
AlertDialog
.
Builder
(
context
)
.
setTitle
(
R
.
string
.
dialog_sort_title
)
.
setView
(
dialogLayout
)
.
setPositiveButton
(
"Done"
,
{
dialog
,
_
->
dialog
.
dismiss
()
})
.
setPositiveButton
(
"Done"
,
{
dialog
,
_
->
invalidateQueryOnSearch
()
updateSort
()
dialog
.
dismiss
()
})
dialogSort
.
show
()
}
...
...
@@ -150,6 +169,31 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
return
super
.
onOptionsItemSelected
(
item
)
}
private
fun
updateSort
()
{
val
sortType
=
SharedPreferenceHelper
.
getInt
(
Constants
.
CHATROOM_SORT_TYPE_KEY
,
ChatRoomsSortOrder
.
ACTIVITY
)
val
grouped
=
SharedPreferenceHelper
.
getBoolean
(
Constants
.
CHATROOM_GROUP_BY_TYPE_KEY
,
false
)
val
order
=
when
(
sortType
)
{
ChatRoomsSortOrder
.
ALPHABETICAL
->
{
if
(
grouped
)
{
ChatRoomsRepository
.
Order
.
GROUPED_NAME
}
else
{
ChatRoomsRepository
.
Order
.
NAME
}
}
ChatRoomsSortOrder
.
ACTIVITY
->
{
if
(
grouped
)
{
ChatRoomsRepository
.
Order
.
GROUPED_ACTIVITY
}
else
{
ChatRoomsRepository
.
Order
.
ACTIVITY
}
}
else
->
ChatRoomsRepository
.
Order
.
ACTIVITY
}
viewModel
.
setOrdering
(
order
)
}
private
fun
invalidateQueryOnSearch
()
{
searchView
?.
let
{
if
(!
searchView
!!
.
isIconified
)
{
...
...
@@ -159,24 +203,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
override
suspend
fun
updateChatRooms
(
newDataSet
:
List
<
ChatRoom
>)
{
listJob
?.
cancel
()
listJob
=
ui
{
val
adapter
=
recycler_view
.
adapter
as
SimpleSectionedRecyclerViewAdapter
// FIXME https://fabric.io/rocketchat3/android/apps/chat.rocket.android/issues/5ac2916c36c7b235275ccccf
// TODO - fix this bug to re-enable DiffUtil
/*val diff = async(CommonPool) {
DiffUtil.calculateDiff(RoomsDiffCallback(adapter.baseAdapter.dataSet, newDataSet))
}.await()*/
text_no_search
.
isVisible
=
newDataSet
.
isEmpty
()
if
(
isActive
)
{
adapter
.
baseAdapter
.
updateRooms
(
newDataSet
)
// TODO - fix crash to re-enable diff.dispatchUpdatesTo(adapter)
adapter
.
notifyDataSetChanged
()
//Set sections always after data set is updated
setSections
()
}
}
}
override
fun
showNoChatRoomsToDisplay
()
{
...
...
@@ -236,84 +262,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
(
activity
as
AppCompatActivity
?)
?.
supportActionBar
?.
title
=
getString
(
R
.
string
.
title_chats
)
}
private
fun
setupRecyclerView
()
{
ui
{
recycler_view
.
layoutManager
=
LinearLayoutManager
(
it
,
LinearLayoutManager
.
VERTICAL
,
false
)
recycler_view
.
addItemDecoration
(
DividerItemDecoration
(
it
,
resources
.
getDimensionPixelSize
(
R
.
dimen
.
divider_item_decorator_bound_start
),
resources
.
getDimensionPixelSize
(
R
.
dimen
.
divider_item_decorator_bound_end
)))
recycler_view
.
itemAnimator
=
DefaultItemAnimator
()
// TODO - use a ViewModel Mapper instead of using settings on the adapter
val
baseAdapter
=
ChatRoomsAdapter
(
it
,
settingsRepository
.
get
(
serverInteractor
.
get
()
!!
),
localRepository
)
{
chatRoom
->
presenter
.
loadChatRoom
(
chatRoom
)
}
sectionedAdapter
=
SimpleSectionedRecyclerViewAdapter
(
it
,
R
.
layout
.
item_chatroom_header
,
R
.
id
.
text_chatroom_header
,
baseAdapter
)
recycler_view
.
adapter
=
sectionedAdapter
}
}
private
fun
setSections
()
{
//Don't add section if not grouping by RoomType
if
(!
SharedPreferenceHelper
.
getBoolean
(
Constants
.
CHATROOM_GROUP_BY_TYPE_KEY
,
false
))
{
sectionedAdapter
?.
clearSections
()
return
}
val
sections
=
ArrayList
<
SimpleSectionedRecyclerViewAdapter
.
Section
>()
sectionedAdapter
?.
baseAdapter
?.
dataSet
?.
let
{
var
previousChatRoomType
=
""
for
((
position
,
chatRoom
)
in
it
.
withIndex
())
{
val
type
=
chatRoom
.
type
.
toString
()
if
(
type
!=
previousChatRoomType
)
{
val
title
=
when
(
type
)
{
RoomType
.
CHANNEL
.
toString
()
->
resources
.
getString
(
R
.
string
.
header_channel
)
RoomType
.
PRIVATE_GROUP
.
toString
()
->
resources
.
getString
(
R
.
string
.
header_private_groups
)
RoomType
.
DIRECT_MESSAGE
.
toString
()
->
resources
.
getString
(
R
.
string
.
header_direct_messages
)
RoomType
.
LIVECHAT
.
toString
()
->
resources
.
getString
(
R
.
string
.
header_live_chats
)
else
->
resources
.
getString
(
R
.
string
.
header_unknown
)
}
sections
.
add
(
SimpleSectionedRecyclerViewAdapter
.
Section
(
position
,
title
))
}
previousChatRoomType
=
chatRoom
.
type
.
toString
()
}
}
val
dummy
=
arrayOfNulls
<
SimpleSectionedRecyclerViewAdapter
.
Section
>(
sections
.
size
)
sectionedAdapter
?.
setSections
(
sections
.
toArray
(
dummy
))
}
private
fun
queryChatRoomsByName
(
name
:
String
?):
Boolean
{
presenter
.
chatRoomsByName
(
name
?:
""
)
//
presenter.chatRoomsByName(name ?: "")
return
true
}
class
RoomsDiffCallback
(
private
val
oldRooms
:
List
<
ChatRoom
>,
private
val
newRooms
:
List
<
ChatRoom
>)
:
DiffUtil
.
Callback
()
{
override
fun
areItemsTheSame
(
oldItemPosition
:
Int
,
newItemPosition
:
Int
):
Boolean
{
return
oldRooms
[
oldItemPosition
].
id
==
newRooms
[
newItemPosition
].
id
}
override
fun
getOldListSize
():
Int
{
return
oldRooms
.
size
}
override
fun
getNewListSize
():
Int
{
return
newRooms
.
size
}
override
fun
areContentsTheSame
(
oldItemPosition
:
Int
,
newItemPosition
:
Int
):
Boolean
{
return
oldRooms
[
oldItemPosition
].
updatedAt
==
newRooms
[
newItemPosition
].
updatedAt
}
override
fun
getChangePayload
(
oldItemPosition
:
Int
,
newItemPosition
:
Int
):
Any
?
{
return
newRooms
[
newItemPosition
]
}
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/viewmodel/ChatRoomsViewModel.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.viewmodel
import
androidx.lifecycle.LiveData
import
androidx.lifecycle.MutableLiveData
import
androidx.lifecycle.Transformations
import
androidx.lifecycle.ViewModel
import
chat.rocket.android.chatrooms.adapter.ItemHolder
import
chat.rocket.android.chatrooms.adapter.RoomMapper
import
chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import
chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository
import
chat.rocket.android.db.model.ChatRoom
import
kotlinx.coroutines.experimental.CommonPool
import
kotlinx.coroutines.experimental.launch
import
kotlinx.coroutines.experimental.withContext
import
me.henrytao.livedataktx.distinct
import
me.henrytao.livedataktx.nonNull
import
me.henrytao.livedataktx.map
import
timber.log.Timber
class
ChatRoomsViewModel
(
private
val
interactor
:
FetchChatRoomsInteractor
,
private
val
repository
:
ChatRoomsRepository
,
private
val
mapper
:
RoomMapper
)
:
ViewModel
()
{
private
val
ordering
:
MutableLiveData
<
ChatRoomsRepository
.
Order
>
=
MutableLiveData
()
init
{
ordering
.
value
=
ChatRoomsRepository
.
Order
.
ACTIVITY
}
fun
getChatRooms
():
LiveData
<
List
<
ItemHolder
<*
>>>
{
// TODO - add a loading status...
launch
{
interactor
.
refreshChatRooms
()
}
return
Transformations
.
switchMap
(
ordering
)
{
order
->
Timber
.
d
(
"Querying rooms for order: $order"
)
val
grouped
=
order
==
ChatRoomsRepository
.
Order
.
GROUPED_ACTIVITY
||
order
==
ChatRoomsRepository
.
Order
.
GROUPED_NAME
repository
.
getChatRooms
(
order
).
nonNull
()
.
distinct
()
.
map
{
rooms
->
Timber
.
d
(
"Mapping rooms to items: $rooms"
)
mapper
.
map
(
rooms
,
grouped
)
}
}
}
fun
setOrdering
(
order
:
ChatRoomsRepository
.
Order
)
{
ordering
.
value
=
order
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatrooms/viewmodel/ChatRoomsViewModelFactory.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.chatrooms.viewmodel
import
androidx.lifecycle.ViewModel
import
androidx.lifecycle.ViewModelProvider
import
chat.rocket.android.chatrooms.adapter.RoomMapper
import
chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import
chat.rocket.android.chatrooms.infrastructure.ChatRoomsRepository
import
javax.inject.Inject
class
ChatRoomsViewModelFactory
@Inject
constructor
(
private
val
interactor
:
FetchChatRoomsInteractor
,
private
val
repository
:
ChatRoomsRepository
,
private
val
mapper
:
RoomMapper
)
:
ViewModelProvider
.
NewInstanceFactory
()
{
@Suppress
(
"UNCHECKED_CAST"
)
override
fun
<
T
:
ViewModel
?
>
create
(
modelClass
:
Class
<
T
>)
=
ChatRoomsViewModel
(
interactor
,
repository
,
mapper
)
as
T
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/dagger/module/AppModule.kt
View file @
35285092
...
...
@@ -50,7 +50,7 @@ import javax.inject.Singleton
@Module
class
AppModule
{
@Provides
/*
@Provides
@Singleton
fun provideRocketChatClient(okHttpClient: OkHttpClient, repository: TokenRepository, logger: PlatformLogger): RocketChatClient {
return RocketChatClient.create {
...
...
@@ -61,7 +61,7 @@ class AppModule {
// TODO remove
restUrl = "https://open.rocket.chat"
}
}
}
*/
@Provides
fun
provideJob
():
Job
{
...
...
app/src/main/java/chat/rocket/android/db/BaseDao.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.db
import
androidx.room.Insert
import
androidx.room.OnConflictStrategy
interface
BaseDao
<
T
>
{
@Insert
fun
insert
(
vararg
obj
:
T
)
@Insert
(
onConflict
=
OnConflictStrategy
.
REPLACE
)
fun
insert
(
list
:
List
<
T
>)
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/db/ChatRoomDao.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.db
import
androidx.lifecycle.LiveData
import
androidx.room.Dao
import
androidx.room.Insert
import
androidx.room.OnConflictStrategy
import
androidx.room.Query
import
androidx.room.Transaction
import
androidx.room.Update
import
chat.rocket.android.db.model.ChatRoom
import
chat.rocket.android.db.model.ChatRoomEntity
import
chat.rocket.common.model.RoomType
@Dao
abstract
class
ChatRoomDao
:
BaseDao
<
ChatRoomEntity
>
{
@Transaction
@Query
(
"""
$BASE_QUERY
ORDER BY
CASE
WHEN lastMessageTimeStamp IS NOT NULL THEN lastMessageTimeStamp
ELSE updatedAt
END DESC
"""
)
abstract
fun
getAll
():
LiveData
<
List
<
ChatRoom
>>
@Transaction
@Query
(
"""
$BASE_QUERY
ORDER BY
$TYPE_ORDER,
CASE
WHEN lastMessageTimeStamp IS NOT NULL THEN lastMessageTimeStamp
ELSE updatedAt
END DESC
"""
)
abstract
fun
getAllGrouped
():
LiveData
<
List
<
ChatRoom
>>
@Transaction
@Query
(
"""
$BASE_QUERY
ORDER BY name
"""
)
abstract
fun
getAllAlphabetically
():
LiveData
<
List
<
ChatRoom
>>
@Transaction
@Query
(
"""
$BASE_QUERY
ORDER BY
$TYPE_ORDER,
name
"""
)
abstract
fun
getAllAlphabeticallyGrouped
():
LiveData
<
List
<
ChatRoom
>>
@Query
(
"SELECT * FROM chatrooms WHERE ID = :id"
)
abstract
fun
get
(
id
:
String
):
ChatRoom
?
@Query
(
"DELETE FROM chatrooms WHERE ID = :id"
)
abstract
fun
delete
(
id
:
String
)
@Insert
(
onConflict
=
OnConflictStrategy
.
REPLACE
)
abstract
fun
insertOrReplace
(
chatRooms
:
List
<
ChatRoomEntity
>)
@Insert
(
onConflict
=
OnConflictStrategy
.
REPLACE
)
abstract
fun
insertOrReplace
(
chatRoom
:
ChatRoomEntity
)
@Update
abstract
fun
update
(
list
:
List
<
ChatRoomEntity
>)
@Transaction
open
fun
update
(
toRemove
:
List
<
String
>,
toInsert
:
List
<
ChatRoomEntity
>,
toUpdate
:
List
<
ChatRoomEntity
>)
{
insertOrReplace
(
toInsert
)
update
(
toUpdate
)
toRemove
.
forEach
{
id
->
delete
(
id
)
}
}
companion
object
{
const
val
BASE_QUERY
=
"""
SELECT chatrooms.*,
users.username as username,
users.name as userFullname,
users.status,
lmUsers.username as lastMessageUserName,
lmUsers.name as lastMessageUserFullName
FROM chatrooms
LEFT JOIN users ON chatrooms.userId = users.id
LEFT JOIN users AS lmUsers ON chatrooms.lastMessageUserId = lmUsers.id
"""
const
val
TYPE_ORDER
=
"""
CASE
WHEN type = '${RoomType.CHANNEL}' THEN 1
WHEN type = '${RoomType.PRIVATE_GROUP}' THEN 2
WHEN type = '${RoomType.DIRECT_MESSAGE}' THEN 3
WHEN type = '${RoomType.LIVECHAT}' THEN 4
ELSE 5
END
"""
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/db/DatabaseManager.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.db
import
android.app.Application
import
chat.rocket.android.db.model.BaseUserEntity
import
chat.rocket.android.db.model.ChatRoomEntity
import
chat.rocket.android.db.model.UserEntity
import
chat.rocket.android.db.model.UserStatus
import
chat.rocket.android.util.extensions.removeTrailingSlash
import
chat.rocket.android.util.extensions.userId
import
chat.rocket.common.model.BaseRoom
import
chat.rocket.common.model.RoomType
import
chat.rocket.common.model.User
import
chat.rocket.core.internal.model.Subscription
import
chat.rocket.core.internal.realtime.socket.model.StreamMessage
import
chat.rocket.core.internal.realtime.socket.model.Type
import
chat.rocket.core.model.Room
import
kotlinx.coroutines.experimental.launch
import
kotlinx.coroutines.experimental.newSingleThreadContext
import
kotlinx.coroutines.experimental.withContext
import
timber.log.Timber
import
java.util.HashSet
class
DatabaseManager
(
val
context
:
Application
,
val
serverUrl
:
String
)
{
private
val
database
:
RCDatabase
=
androidx
.
room
.
Room
.
databaseBuilder
(
context
,
RCDatabase
::
class
.
java
,
serverUrl
.
databaseName
()).
fallbackToDestructiveMigration
()
.
build
()
private
val
dbContext
=
newSingleThreadContext
(
"$serverUrl-db-context"
)
private
val
insertSubs
=
HashMap
<
String
,
Subscription
>()
private
val
insertRooms
=
HashMap
<
String
,
Room
>()
private
val
updateSubs
=
LinkedHashMap
<
String
,
Subscription
>()
private
val
updateRooms
=
LinkedHashMap
<
String
,
Room
>()
fun
chatRoomDao
():
ChatRoomDao
=
database
.
chatRoomDao
()
fun
userDao
():
UserDao
=
database
.
userDao
()
fun
processUsersBatch
(
users
:
List
<
User
>)
{
launch
(
dbContext
)
{
val
dao
=
database
.
userDao
()
val
list
=
ArrayList
<
BaseUserEntity
>(
users
.
size
)
users
.
forEach
{
user
->
user
.
toEntity
()
?.
let
{
entity
->
list
.
add
(
entity
)
}
}
dao
.
upsert
(
list
)
}
}
fun
processStreamBatch
(
batch
:
List
<
StreamMessage
<
BaseRoom
>>)
{
launch
(
dbContext
)
{
val
toRemove
=
HashSet
<
String
>()
val
toInsert
=
ArrayList
<
ChatRoomEntity
>(
batch
.
size
/
2
)
val
toUpdate
=
ArrayList
<
ChatRoomEntity
>(
batch
.
size
)
batch
.
forEach
{
when
(
it
.
type
)
{
is
Type
.
Removed
->
toRemove
.
add
(
removeChatRoom
(
it
.
data
))
is
Type
.
Inserted
->
insertChatRoom
(
it
.
data
)
?.
let
{
toInsert
.
add
(
it
)
}
is
Type
.
Updated
->
{
when
(
it
.
data
)
{
is
Subscription
->
updateSubs
[(
it
.
data
as
Subscription
).
roomId
]
=
it
.
data
as
Subscription
is
Room
->
updateRooms
[(
it
.
data
as
Room
).
id
]
=
it
.
data
as
Room
}
}
}
}
toUpdate
.
addAll
(
createMatchingUpdates
())
toUpdate
.
addAll
(
createUpdates
())
try
{
val
filteredUpdate
=
toUpdate
.
filterNot
{
toRemove
.
contains
(
it
.
id
)
}
val
filteredInsert
=
toInsert
.
filterNot
{
toRemove
.
contains
(
it
.
id
)
}
Timber
.
d
(
"Running ChatRooms transaction: remove: $toRemove - insert: $toInsert - update: $filteredUpdate"
)
chatRoomDao
().
update
(
toRemove
.
toList
(),
filteredInsert
,
filteredUpdate
)
}
catch
(
ex
:
Exception
)
{
Timber
.
d
(
ex
,
"Error updating chatrooms"
)
}
}
}
private
suspend
fun
createUpdates
():
List
<
ChatRoomEntity
>
{
val
list
=
ArrayList
<
ChatRoomEntity
>()
updateSubs
.
forEach
{
(
_
,
subscription
)
->
updateSubscription
(
subscription
)
?.
let
{
list
.
add
(
it
)
}
}
updateRooms
.
forEach
{
(
_
,
room
)
->
updateRoom
(
room
)
?.
let
{
list
.
add
(
it
)
}
}
updateSubs
.
clear
()
updateRooms
.
clear
()
return
list
}
private
suspend
fun
createMatchingUpdates
():
List
<
ChatRoomEntity
>
{
val
list
=
ArrayList
<
ChatRoomEntity
>()
val
matches
=
ArrayList
<
String
>()
updateRooms
.
forEach
{
room
->
val
(
id
,
_
)
=
room
if
(
updateSubs
.
containsKey
(
id
))
{
matches
.
add
(
id
)
}
}
matches
.
forEach
{
id
->
val
room
=
updateRooms
.
remove
(
id
)
val
subscription
=
updateSubs
.
remove
(
id
)
list
.
add
(
fullChatRoomEntity
(
subscription
!!
,
room
!!
))
}
return
list
}
private
fun
removeChatRoom
(
data
:
BaseRoom
):
String
{
return
when
(
data
)
{
is
Subscription
->
data
.
roomId
else
->
data
.
id
}
}
private
suspend
fun
updateChatRoom
(
data
:
BaseRoom
):
ChatRoomEntity
?
{
return
when
(
data
)
{
is
Room
->
updateRoom
(
data
)
is
Subscription
->
updateSubscription
(
data
)
else
->
null
}
}
private
suspend
fun
updateRoom
(
data
:
Room
):
ChatRoomEntity
?
{
return
chatRoomDao
().
get
(
data
.
id
)
?.
let
{
current
->
with
(
data
)
{
val
chatRoom
=
current
.
chatRoom
lastMessage
?.
sender
?.
let
{
user
->
if
(
findUser
(
user
.
id
!!
)
==
null
)
{
Timber
.
d
(
"Missing last message user, inserting: ${user.id}"
)
insert
(
UserEntity
(
user
.
id
!!
,
user
.
username
,
user
.
name
))
}
}
chatRoom
.
copy
(
name
=
name
?:
chatRoom
.
name
,
readonly
=
readonly
,
updatedAt
=
updatedAt
?:
chatRoom
.
updatedAt
,
lastMessageText
=
lastMessage
?.
message
,
lastMessageUserId
=
lastMessage
?.
sender
?.
id
,
lastMessageTimestamp
=
lastMessage
?.
timestamp
)
}
}
}
private
suspend
fun
updateSubscription
(
data
:
Subscription
):
ChatRoomEntity
?
{
return
chatRoomDao
().
get
(
data
.
roomId
)
?.
let
{
current
->
with
(
data
)
{
val
userId
=
if
(
type
is
RoomType
.
DirectMessage
)
{
roomId
.
userId
(
user
?.
id
)
}
else
{
null
}
if
(
userId
!=
null
&&
findUser
(
userId
)
==
null
)
{
Timber
.
d
(
"Missing user, inserting: $userId"
)
insert
(
UserEntity
(
userId
))
}
val
chatRoom
=
current
.
chatRoom
chatRoom
.
copy
(
id
=
roomId
,
subscriptionId
=
id
,
type
=
type
.
toString
(),
name
=
name
,
userId
=
userId
?:
chatRoom
.
userId
,
readonly
=
readonly
?:
chatRoom
.
readonly
,
isDefault
=
isDefault
,
favorite
=
isFavorite
,
open
=
open
,
alert
=
alert
,
unread
=
unread
,
userMentions
=
userMentions
?:
chatRoom
.
userMentions
,
groupMentions
=
groupMentions
?:
chatRoom
.
groupMentions
,
updatedAt
=
updatedAt
?:
chatRoom
.
updatedAt
,
timestamp
=
timestamp
?:
chatRoom
.
timestamp
,
lastSeen
=
lastSeen
?:
chatRoom
.
lastSeen
)
}
}
}
private
suspend
fun
insertChatRoom
(
data
:
BaseRoom
):
ChatRoomEntity
?
{
return
when
(
data
)
{
is
Room
->
insertRoom
(
data
)
is
Subscription
->
insertSubscription
(
data
)
else
->
null
}
}
private
suspend
fun
insertRoom
(
data
:
Room
):
ChatRoomEntity
?
{
val
subscription
=
insertSubs
.
remove
(
data
.
id
)
return
if
(
subscription
!=
null
)
{
fullChatRoomEntity
(
subscription
,
data
)
}
else
{
insertRooms
[
data
.
id
]
=
data
null
}
}
private
suspend
fun
insertSubscription
(
data
:
Subscription
):
ChatRoomEntity
?
{
val
room
=
insertRooms
.
remove
(
data
.
roomId
)
return
if
(
room
!=
null
)
{
fullChatRoomEntity
(
data
,
room
)
}
else
{
insertSubs
[
data
.
roomId
]
=
data
null
}
}
private
suspend
fun
fullChatRoomEntity
(
subscription
:
Subscription
,
room
:
Room
):
ChatRoomEntity
{
val
userId
=
if
(
room
.
type
is
RoomType
.
DirectMessage
)
{
subscription
.
roomId
.
userId
(
subscription
.
user
?.
id
)
}
else
{
null
}
if
(
userId
!=
null
&&
findUser
(
userId
)
==
null
)
{
Timber
.
d
(
"Missing user, inserting: $userId"
)
insert
(
UserEntity
(
userId
))
}
room
.
lastMessage
?.
sender
?.
let
{
user
->
if
(
findUser
(
user
.
id
!!
)
==
null
)
{
Timber
.
d
(
"Missing last message user, inserting: ${user.id}"
)
insert
(
UserEntity
(
user
.
id
!!
,
user
.
username
,
user
.
name
))
}
}
return
ChatRoomEntity
(
id
=
room
.
id
,
subscriptionId
=
subscription
.
id
,
type
=
room
.
type
.
toString
(),
name
=
room
.
name
?:
subscription
.
name
,
userId
=
userId
,
readonly
=
subscription
.
readonly
,
isDefault
=
subscription
.
isDefault
,
favorite
=
subscription
.
isFavorite
,
open
=
subscription
.
open
,
alert
=
subscription
.
alert
,
unread
=
subscription
.
unread
,
userMentions
=
subscription
.
userMentions
,
groupMentions
=
subscription
.
groupMentions
,
updatedAt
=
subscription
.
updatedAt
,
timestamp
=
subscription
.
timestamp
,
lastSeen
=
subscription
.
lastSeen
,
lastMessageText
=
room
.
lastMessage
?.
message
,
lastMessageUserId
=
room
.
lastMessage
?.
sender
?.
id
,
lastMessageTimestamp
=
room
.
lastMessage
?.
timestamp
)
}
suspend
fun
insert
(
rooms
:
List
<
ChatRoomEntity
>)
{
withContext
(
dbContext
)
{
chatRoomDao
().
insert
(
rooms
)
}
}
suspend
fun
insert
(
user
:
UserEntity
)
{
withContext
(
dbContext
)
{
userDao
().
insert
(
user
)
}
}
fun
findUser
(
userId
:
String
):
String
?
=
userDao
().
findUser
(
userId
)
}
fun
User
.
toEntity
():
BaseUserEntity
?
{
return
if
(
name
==
null
&&
username
==
null
&&
utcOffset
==
null
&&
status
!=
null
)
{
UserStatus
(
id
=
id
,
status
=
status
.
toString
())
}
else
if
(
username
!=
null
){
UserEntity
(
id
,
username
,
name
,
status
?.
toString
()
?:
"offline"
,
utcOffset
)
}
else
{
null
}
}
private
fun
String
.
databaseName
():
String
{
val
tmp
=
this
.
removePrefix
(
"https://"
)
.
removePrefix
(
"http://"
)
.
removeTrailingSlash
()
.
replace
(
"."
,
"_"
)
return
"$tmp.db"
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/db/DatabaseManagerFactory.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.db
import
android.app.Application
import
timber.log.Timber
import
javax.inject.Inject
import
javax.inject.Singleton
@Singleton
class
DatabaseManagerFactory
@Inject
constructor
(
private
val
context
:
Application
)
{
private
val
cache
=
HashMap
<
String
,
DatabaseManager
>()
fun
create
(
serverUrl
:
String
):
DatabaseManager
{
cache
[
serverUrl
]
?.
let
{
Timber
.
d
(
"Returning cached database for $serverUrl"
)
return
it
}
Timber
.
d
(
"Returning FRESH database for $serverUrl"
)
val
db
=
DatabaseManager
(
context
,
serverUrl
)
cache
[
serverUrl
]
=
db
return
db
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/db/RCDatabase.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.db
import
androidx.room.Database
import
androidx.room.RoomDatabase
import
chat.rocket.android.db.model.ChatRoomEntity
import
chat.rocket.android.db.model.UserEntity
@Database
(
entities
=
[
UserEntity
::
class
,
ChatRoomEntity
::
class
],
version
=
1
,
exportSchema
=
true
)
abstract
class
RCDatabase
:
RoomDatabase
()
{
abstract
fun
userDao
():
UserDao
abstract
fun
chatRoomDao
():
ChatRoomDao
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/db/UserDao.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.db
import
androidx.room.Dao
import
androidx.room.Insert
import
androidx.room.OnConflictStrategy
import
androidx.room.Query
import
androidx.room.Transaction
import
androidx.room.Update
import
chat.rocket.android.db.model.BaseUserEntity
import
chat.rocket.android.db.model.UserEntity
import
chat.rocket.android.db.model.UserStatus
import
timber.log.Timber
@Dao
abstract
class
UserDao
:
BaseDao
<
UserEntity
>
{
@Update
(
onConflict
=
OnConflictStrategy
.
IGNORE
)
abstract
fun
update
(
user
:
UserEntity
):
Int
@Query
(
"UPDATE OR IGNORE users set STATUS = :status where ID = :id"
)
abstract
fun
update
(
id
:
String
,
status
:
String
):
Int
@Query
(
"SELECT id FROM users WHERE ID = :id"
)
abstract
fun
findUser
(
id
:
String
):
String
?
@Transaction
open
fun
upsert
(
user
:
BaseUserEntity
)
{
internalUpsert
(
user
)
}
@Transaction
open
fun
upsert
(
users
:
List
<
BaseUserEntity
>)
{
users
.
forEach
{
internalUpsert
(
it
)
}
}
private
inline
fun
internalUpsert
(
user
:
BaseUserEntity
)
{
val
count
=
if
(
user
is
UserStatus
)
{
update
(
user
.
id
,
user
.
status
)
}
else
{
update
(
user
as
UserEntity
)
}
if
(
count
==
0
&&
user
is
UserEntity
)
{
Timber
.
d
(
"missing user, inserting: ${user.id}"
)
insert
(
user
)
}
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/db/model/ChatRoomEntity.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.db.model
import
androidx.room.Embedded
import
androidx.room.Entity
import
androidx.room.ForeignKey
import
androidx.room.Index
import
androidx.room.PrimaryKey
@Entity
(
tableName
=
"chatrooms"
,
indices
=
[
Index
(
value
=
[
"userId"
]),
Index
(
value
=
[
"subscriptionId"
],
unique
=
true
),
Index
(
value
=
[
"updatedAt"
])
],
foreignKeys
=
[
ForeignKey
(
entity
=
UserEntity
::
class
,
parentColumns
=
[
"id"
],
childColumns
=
[
"userId"
]),
ForeignKey
(
entity
=
UserEntity
::
class
,
parentColumns
=
[
"id"
],
childColumns
=
[
"lastMessageUserId"
])
]
)
data class
ChatRoomEntity
(
@PrimaryKey
var
id
:
String
,
var
subscriptionId
:
String
,
var
type
:
String
,
var
name
:
String
,
var
userId
:
String
?,
var
readonly
:
Boolean
?
=
false
,
var
isDefault
:
Boolean
?
=
false
,
var
favorite
:
Boolean
?
=
false
,
var
open
:
Boolean
=
true
,
var
alert
:
Boolean
=
false
,
var
unread
:
Long
=
0
,
var
userMentions
:
Long
?
=
0
,
var
groupMentions
:
Long
?
=
0
,
var
updatedAt
:
Long
?
=
-
1
,
var
timestamp
:
Long
?
=
-
1
,
var
lastSeen
:
Long
?
=
-
1
,
var
lastMessageText
:
String
?,
var
lastMessageUserId
:
String
?,
var
lastMessageTimestamp
:
Long
?
)
data class
ChatRoom
(
@Embedded
var
chatRoom
:
ChatRoomEntity
,
var
username
:
String
?,
var
userFullname
:
String
?,
var
status
:
String
?,
var
lastMessageUserName
:
String
?,
var
lastMessageUserFullName
:
String
?
)
\ No newline at end of file
app/src/main/java/chat/rocket/android/db/model/UserEntity.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.db.model
import
androidx.room.Entity
import
androidx.room.Index
import
androidx.room.PrimaryKey
@Entity
(
tableName
=
"users"
,
indices
=
[(
Index
(
value
=
[
"username"
],
unique
=
true
))])
data class
UserEntity
(
@PrimaryKey
override
val
id
:
String
,
var
username
:
String
?
=
null
,
var
name
:
String
?
=
null
,
override
var
status
:
String
=
"offline"
,
var
utcOffset
:
Float
?
=
null
)
:
BaseUserEntity
data class
UserStatus
(
override
val
id
:
String
,
override
val
status
:
String
)
:
BaseUserEntity
interface
BaseUserEntity
{
val
id
:
String
val
status
:
String
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/infrastructure/LocalRepository.kt
View file @
35285092
...
...
@@ -21,7 +21,6 @@ interface LocalRepository {
companion
object
{
const
val
KEY_PUSH_TOKEN
=
"KEY_PUSH_TOKEN"
const
val
MIGRATION_FINISHED_KEY
=
"MIGRATION_FINISHED_KEY"
const
val
TOKEN_KEY
=
"token_"
const
val
SETTINGS_KEY
=
"settings_"
const
val
PERMISSIONS_KEY
=
"permissions_"
...
...
app/src/main/java/chat/rocket/android/members/ui/MembersFragment.kt
View file @
35285092
...
...
@@ -76,7 +76,7 @@ class MembersFragment : Fragment(), MembersView {
adapter
.
prependData
(
dataSet
)
if
(
dataSet
.
size
>=
59
)
{
// TODO Check why the API retorns the specified count -1
recycler_view
.
addOnScrollListener
(
object
:
EndlessRecyclerViewScrollListener
(
linearLayoutManager
)
{
override
fun
onLoadMore
(
page
:
Int
,
totalItemsCount
:
Int
,
recyclerView
:
RecyclerView
?
)
{
override
fun
onLoadMore
(
page
:
Int
,
totalItemsCount
:
Int
,
recyclerView
:
RecyclerView
)
{
presenter
.
loadChatRoomsMembers
(
chatRoomId
,
chatRoomType
,
page
*
60L
)
}
})
...
...
app/src/main/java/chat/rocket/android/server/infraestructure/ConnectionManager.kt
View file @
35285092
package
chat.rocket.android.server.infraestructure
import
androidx.lifecycle.MutableLiveData
import
chat.rocket.android.db.DatabaseManager
import
chat.rocket.common.model.BaseRoom
import
chat.rocket.common.model.User
import
chat.rocket.core.RocketChatClient
import
chat.rocket.core.internal.realtime.subscribeSubscriptions
import
chat.rocket.core.internal.realtime.subscribeRooms
import
chat.rocket.core.internal.realtime.subscribeUserData
import
chat.rocket.core.internal.realtime.subscribeActiveUsers
import
chat.rocket.core.internal.realtime.subscribeRoomMessages
import
chat.rocket.core.internal.realtime.unsubscribe
import
chat.rocket.core.internal.realtime.socket.connect
import
chat.rocket.core.internal.realtime.socket.disconnect
import
chat.rocket.core.internal.realtime.socket.model.State
import
chat.rocket.core.internal.realtime.socket.model.StreamMessage
import
chat.rocket.core.internal.realtime.subscribeActiveUsers
import
chat.rocket.core.internal.realtime.subscribeRoomMessages
import
chat.rocket.core.internal.realtime.subscribeRooms
import
chat.rocket.core.internal.realtime.subscribeSubscriptions
import
chat.rocket.core.internal.realtime.subscribeUserData
import
chat.rocket.core.internal.realtime.unsubscribe
import
chat.rocket.core.internal.rest.chatRooms
import
chat.rocket.core.model.Message
import
chat.rocket.core.model.Myself
import
kotlinx.coroutines.experimental.CommonPool
import
kotlinx.coroutines.experimental.Job
import
kotlinx.coroutines.experimental.channels.Channel
import
kotlinx.coroutines.experimental.channels.SendChannel
import
kotlinx.coroutines.experimental.channels.actor
import
kotlinx.coroutines.experimental.launch
import
kotlinx.coroutines.experimental.newSingleThreadContext
import
kotlinx.coroutines.experimental.selects.select
import
timber.log.Timber
import
java.util.concurrent.CopyOnWriteArrayList
import
kotlin.coroutines.experimental.CoroutineContext
import
kotlin.math.absoluteValue
class
ConnectionManager
(
internal
val
client
:
RocketChatClient
)
{
class
ConnectionManager
(
internal
val
client
:
RocketChatClient
,
private
val
dbManager
:
DatabaseManager
)
{
private
val
statusLiveData
=
MutableLiveData
<
State
>()
private
val
statusChannelList
=
CopyOnWriteArrayList
<
Channel
<
State
>>()
private
val
statusChannel
=
Channel
<
State
>(
Channel
.
CONFLATED
)
private
var
connectJob
:
Job
?
=
null
...
...
@@ -38,6 +51,9 @@ class ConnectionManager(internal val client: RocketChatClient) {
private
var
userDataId
:
String
?
=
null
private
var
activeUserId
:
String
?
=
null
private
val
activeUsersContext
=
newSingleThreadContext
(
"activeUsersContext"
)
private
val
roomsContext
=
newSingleThreadContext
(
"roomsContext"
)
fun
connect
()
{
if
(
connectJob
?.
isActive
==
true
&&
(
state
!
is
State
.
Disconnected
))
{
Timber
.
d
(
"Already connected, just returning..."
)
...
...
@@ -80,6 +96,8 @@ class ConnectionManager(internal val client: RocketChatClient) {
}
}
statusLiveData
.
postValue
(
status
)
for
(
channel
in
statusChannelList
)
{
Timber
.
d
(
"Sending status: $status to $channel"
)
channel
.
offer
(
status
)
...
...
@@ -87,24 +105,45 @@ class ConnectionManager(internal val client: RocketChatClient) {
}
}
var
totalBatchedUsers
=
0
val
userActor
=
createBatchActor
<
User
>(
activeUsersContext
,
parent
=
connectJob
,
maxSize
=
500
,
maxTime
=
1000
)
{
users
->
totalBatchedUsers
+=
users
.
size
Timber
.
d
(
"Processing Users batch: ${users.size} - $totalBatchedUsers"
)
// TODO - move this to an Interactor
dbManager
.
processUsersBatch
(
users
)
}
val
roomsActor
=
createBatchActor
<
StreamMessage
<
BaseRoom
>>(
roomsContext
,
parent
=
connectJob
,
maxSize
=
10
)
{
batch
->
Timber
.
d
(
"processing Stream batch: ${batch.size} - $batch"
)
dbManager
.
processStreamBatch
(
batch
)
}
// stream-notify-user - ${userId}/rooms-changed
launch
(
parent
=
connectJob
)
{
for
(
room
in
client
.
roomsChannel
)
{
Timber
.
d
(
"GOT Room streamed"
)
roomsActor
.
send
(
room
)
for
(
channel
in
roomAndSubscriptionChannels
)
{
channel
.
send
(
room
)
}
}
}
// stream-notify-user - ${userId}/subscriptions-changed
launch
(
parent
=
connectJob
)
{
for
(
subscription
in
client
.
subscriptionsChannel
)
{
Timber
.
d
(
"GOT Subscription streamed"
)
roomsActor
.
send
(
subscription
)
for
(
channel
in
roomAndSubscriptionChannels
)
{
channel
.
send
(
subscription
)
}
}
}
// stream-room-messages - $roomId
launch
(
parent
=
connectJob
)
{
for
(
message
in
client
.
messagesChannel
)
{
Timber
.
d
(
"Received new Message for room ${message.roomId}"
)
...
...
@@ -113,18 +152,24 @@ class ConnectionManager(internal val client: RocketChatClient) {
}
}
// userData
launch
(
parent
=
connectJob
)
{
for
(
myself
in
client
.
userDataChannel
)
{
Timber
.
d
(
"Got userData"
)
userActor
.
send
(
myself
.
asUser
())
for
(
channel
in
userDataChannels
)
{
channel
.
send
(
myself
)
}
}
}
var
totalUsers
=
0
// activeUsers
launch
(
parent
=
connectJob
)
{
for
(
user
in
client
.
activeUsersChannel
)
{
Timber
.
d
(
"Got activeUsers"
)
totalUsers
++
//Timber.d("Got activeUsers: $totalUsers")
userActor
.
send
(
user
)
for
(
channel
in
activeUsersChannels
)
{
channel
.
send
(
user
)
}
...
...
@@ -196,6 +241,51 @@ class ConnectionManager(internal val client: RocketChatClient) {
id
?.
let
{
client
.
unsubscribe
(
it
)
}
}
}
private
inline
fun
<
T
>
createBatchActor
(
context
:
CoroutineContext
=
CommonPool
,
parent
:
Job
?
=
null
,
maxSize
:
Int
=
100
,
maxTime
:
Int
=
500
,
crossinline
block
:
(
List
<
T
>)
->
Unit
):
SendChannel
<
T
>
{
return
actor
(
context
,
parent
=
parent
)
{
val
batch
=
ArrayList
<
T
>(
maxSize
)
var
deadline
=
0L
// deadline for sending this batch to callback block
while
(
true
)
{
// when deadline is reached or size is exceeded, pass the batch to the callback block
val
remainingTime
=
deadline
-
System
.
currentTimeMillis
()
if
(
batch
.
isNotEmpty
()
&&
remainingTime
<=
0
||
batch
.
size
>=
maxSize
)
{
Timber
.
d
(
"Processing batch: ${batch.size}"
)
block
(
batch
.
toList
())
batch
.
clear
()
continue
}
// wait until items is received or timeout reached
select
<
Unit
>
{
// when received -> add to batch
channel
.
onReceive
{
batch
.
add
(
it
)
//Timber.d("Adding user to batch: ${batch.size}")
// init deadline on first item added to batch
if
(
batch
.
size
==
1
)
deadline
=
System
.
currentTimeMillis
()
+
maxTime
}
// when timeout is reached just finish select, note: no timeout when batch is empty
if
(
batch
.
isNotEmpty
())
onTimeout
(
remainingTime
.
orZero
())
{}
}
if
(!
isActive
)
break
}
}
}
}
private
fun
Myself
.
asUser
():
User
{
return
User
(
id
,
name
,
username
,
status
,
utcOffset
,
null
,
roles
)
}
private
fun
Long
.
orZero
():
Long
{
return
if
(
this
<
0
)
0
else
this
}
suspend
fun
ConnectionManager
.
chatRooms
(
timestamp
:
Long
=
0
,
filterCustom
:
Boolean
=
true
)
=
...
...
app/src/main/java/chat/rocket/android/server/infraestructure/ConnectionManagerFactory.kt
View file @
35285092
package
chat.rocket.android.server.infraestructure
import
chat.rocket.android.db.DatabaseManagerFactory
import
timber.log.Timber
import
javax.inject.Inject
import
javax.inject.Singleton
@Singleton
class
ConnectionManagerFactory
@Inject
constructor
(
private
val
factory
:
RocketChatClientFactory
)
{
class
ConnectionManagerFactory
@Inject
constructor
(
private
val
factory
:
RocketChatClientFactory
,
private
val
dbFactory
:
DatabaseManagerFactory
)
{
private
val
cache
=
HashMap
<
String
,
ConnectionManager
>()
fun
create
(
url
:
String
):
ConnectionManager
{
...
...
@@ -15,7 +19,7 @@ class ConnectionManagerFactory @Inject constructor(private val factory: RocketCh
}
Timber
.
d
(
"Returning FRESH Manager for: $url"
)
val
manager
=
ConnectionManager
(
factory
.
create
(
url
))
val
manager
=
ConnectionManager
(
factory
.
create
(
url
)
,
dbFactory
.
create
(
url
)
)
cache
[
url
]
=
manager
return
manager
}
...
...
app/src/main/java/chat/rocket/android/util/extensions/Date.kt
View file @
35285092
package
chat.rocket.android.util.extensions
import
android.content.Context
import
org.threeten.bp.LocalDateTime
fun
LocalDateTime
?.
date
(
context
:
Context
):
String
?
{
return
this
?.
let
{
DateTimeHelper
.
getDate
(
it
,
context
)
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/util/extensions/Numbers.kt
0 → 100644
View file @
35285092
package
chat.rocket.android.util.extensions
import
org.threeten.bp.LocalDateTime
fun
Long
?.
localDateTime
():
LocalDateTime
?
{
return
this
?.
let
{
DateTimeHelper
.
getLocalDateTime
(
it
)
}
}
\ No newline at end of file
app/src/main/res/drawable/ic_hashtag_12dp.xml
0 → 100644
View file @
35285092
<vector
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:width=
"12dp"
android:height=
"12dp"
android:viewportHeight=
"12"
android:viewportWidth=
"12"
>
<path
android:fillColor=
"#787878"
android:fillType=
"evenOdd"
android:pathData=
"M2.4,0h1.2v12h-1.2z"
/>
<path
android:fillColor=
"#787878"
android:fillType=
"evenOdd"
android:pathData=
"M0,2.4h12v1.2h-12z"
/>
<path
android:fillColor=
"#787878"
android:fillType=
"evenOdd"
android:pathData=
"M0,8.4h12v1.2h-12z"
/>
<path
android:fillColor=
"#787878"
android:fillType=
"evenOdd"
android:pathData=
"M8.4,0h1.2v12h-1.2z"
/>
</vector>
\ No newline at end of file
app/src/main/res/drawable/ic_hashtag_unread_12dp.xml
View file @
35285092
...
...
@@ -4,27 +4,19 @@
android:viewportHeight=
"12"
android:viewportWidth=
"12"
>
<path
android:fillColor=
"#
9EA2A8
"
android:fillColor=
"#
DE000000
"
android:fillType=
"evenOdd"
android:pathData=
"M2.4,0h1.2v12h-1.2z"
android:strokeColor=
"#00000000"
android:strokeWidth=
"1"
/>
android:pathData=
"M2.4,0h1.2v12h-1.2z"
/>
<path
android:fillColor=
"#
9EA2A8
"
android:fillColor=
"#
DE000000
"
android:fillType=
"evenOdd"
android:pathData=
"M0,2.4h12v1.2h-12z"
android:strokeColor=
"#00000000"
android:strokeWidth=
"1"
/>
android:pathData=
"M0,2.4h12v1.2h-12z"
/>
<path
android:fillColor=
"#
9EA2A8
"
android:fillColor=
"#
DE000000
"
android:fillType=
"evenOdd"
android:pathData=
"M0,8.4h12v1.2h-12z"
android:strokeColor=
"#00000000"
android:strokeWidth=
"1"
/>
android:pathData=
"M0,8.4h12v1.2h-12z"
/>
<path
android:fillColor=
"#
9EA2A8
"
android:fillColor=
"#
DE000000
"
android:fillType=
"evenOdd"
android:pathData=
"M8.4,0h1.2v12h-1.2z"
android:strokeColor=
"#00000000"
android:strokeWidth=
"1"
/>
android:pathData=
"M8.4,0h1.2v12h-1.2z"
/>
</vector>
\ No newline at end of file
app/src/main/res/drawable/ic_lock_12_dp.xml
View file @
35285092
...
...
@@ -6,13 +6,11 @@
<path
android:pathData=
"M1.5,5.5h9v6h-9z"
android:strokeWidth=
"1"
android:fillColor=
"#00000000"
android:strokeColor=
"#9EA2A8"
android:strokeColor=
"#787878"
android:fillType=
"evenOdd"
/>
<path
android:pathData=
"M2.5,5.5L9.5,5.5L9.5,4C9.5,2.067 7.933,0.5 6,0.5C4.067,0.5 2.5,2.067 2.5,4L2.5,5.5Z"
android:strokeWidth=
"1"
android:fillColor=
"#00000000"
android:strokeColor=
"#9EA2A8"
android:strokeColor=
"#787878"
android:fillType=
"evenOdd"
/>
</vector>
app/src/main/res/drawable/ic_lock_unread_12_dp.xml
0 → 100644
View file @
35285092
<vector
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:width=
"12dp"
android:height=
"12dp"
android:viewportWidth=
"12"
android:viewportHeight=
"12"
>
<path
android:pathData=
"M1.5,5.5h9v6h-9z"
android:strokeWidth=
"1"
android:strokeColor=
"#DE000000"
android:fillType=
"evenOdd"
/>
<path
android:pathData=
"M2.5,5.5L9.5,5.5L9.5,4C9.5,2.067 7.933,0.5 6,0.5C4.067,0.5 2.5,2.067 2.5,4L2.5,5.5Z"
android:strokeWidth=
"1"
android:strokeColor=
"#DE000000"
android:fillType=
"evenOdd"
/>
</vector>
app/src/main/res/layout/item_chat.xml
View file @
35285092
...
...
@@ -27,7 +27,7 @@
app:layout_constraintBottom_toBottomOf=
"@+id/text_chat_name"
app:layout_constraintStart_toEndOf=
"@+id/image_avatar"
app:layout_constraintTop_toTopOf=
"@+id/text_chat_name"
tools:src=
"@drawable/ic_hashtag_12dp"
/>
tools:src=
"@drawable/ic_hashtag_
unread_
12dp"
/>
<TextView
...
...
@@ -58,6 +58,7 @@
android:lines=
"1"
android:maxLines=
"1"
android:textDirection=
"locale"
android:textColor=
"@color/colorSecondaryText"
app:layout_constraintBottom_toTopOf=
"@+id/text_last_message"
app:layout_constraintEnd_toStartOf=
"@+id/text_last_message_date_time"
app:layout_constraintStart_toEndOf=
"@+id/image_chat_icon"
...
...
@@ -76,7 +77,6 @@
android:ellipsize=
"end"
android:maxLines=
"2"
android:textDirection=
"locale"
tools:visibility=
"visible"
app:layout_constraintEnd_toStartOf=
"@+id/layout_unread_messages_badge"
app:layout_constraintTop_toTopOf=
"@+id/text_chat_name"
tools:text=
"11:45 AM"
/>
...
...
app/src/main/res/layout/item_chatroom_header.xml
View file @
35285092
...
...
@@ -4,10 +4,10 @@
android:layout_height=
"wrap_content"
android:orientation=
"vertical"
>
<View
<
!--<
View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background=
"@color/darkGray"
/>
android:background="@color/darkGray" />
-->
<TextView
android:id=
"@+id/text_chatroom_header"
...
...
@@ -22,6 +22,6 @@
<View
android:layout_width=
"match_parent"
android:layout_height=
"1dp"
android:background=
"@color/
darkGray
"
/>
android:background=
"@color/
quoteBar
"
/>
</LinearLayout>
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment