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
8685b5dd
Commit
8685b5dd
authored
Mar 30, 2018
by
Filipe de Lima Brito
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'develop-2.x' of github.com:RocketChat/Rocket.Chat.Android into fix/login-with-oauth
parents
29355692
2176c0ce
Changes
38
Show whitespace changes
Inline
Side-by-side
Showing
38 changed files
with
538 additions
and
379 deletions
+538
-379
RocketChatApplication.kt
...ain/java/chat/rocket/android/app/RocketChatApplication.kt
+0
-1
ChatRoomAdapter.kt
...a/chat/rocket/android/chatroom/adapter/ChatRoomAdapter.kt
+16
-8
CommandSuggestionsAdapter.kt
...ket/android/chatroom/adapter/CommandSuggestionsAdapter.kt
+1
-1
MessageAttachmentViewHolder.kt
...t/android/chatroom/adapter/MessageAttachmentViewHolder.kt
+29
-0
PeopleSuggestionsAdapter.kt
...cket/android/chatroom/adapter/PeopleSuggestionsAdapter.kt
+33
-6
ChatRoomPresenter.kt
...rocket/android/chatroom/presentation/ChatRoomPresenter.kt
+1
-2
ActionSnackbar.kt
...in/java/chat/rocket/android/chatroom/ui/ActionSnackbar.kt
+8
-12
ChatRoomFragment.kt
.../java/chat/rocket/android/chatroom/ui/ChatRoomFragment.kt
+49
-7
AudioAttachmentViewModel.kt
...et/android/chatroom/viewmodel/AudioAttachmentViewModel.kt
+3
-2
BaseViewModel.kt
...a/chat/rocket/android/chatroom/viewmodel/BaseViewModel.kt
+1
-0
ImageAttachmentViewModel.kt
...et/android/chatroom/viewmodel/ImageAttachmentViewModel.kt
+2
-1
MessageAttachmentViewModel.kt
.../android/chatroom/viewmodel/MessageAttachmentViewModel.kt
+24
-0
MessageViewModel.kt
...hat/rocket/android/chatroom/viewmodel/MessageViewModel.kt
+1
-0
UrlPreviewViewModel.kt
.../rocket/android/chatroom/viewmodel/UrlPreviewViewModel.kt
+2
-1
VideoAttachmentViewModel.kt
...et/android/chatroom/viewmodel/VideoAttachmentViewModel.kt
+2
-1
ViewModelMapper.kt
...chat/rocket/android/chatroom/viewmodel/ViewModelMapper.kt
+50
-108
PeopleSuggestionViewModel.kt
...hatroom/viewmodel/suggestion/PeopleSuggestionViewModel.kt
+1
-1
ChatRoomsPresenter.kt
...cket/android/chatrooms/presentation/ChatRoomsPresenter.kt
+21
-6
ChatRoomsAdapter.kt
...java/chat/rocket/android/chatrooms/ui/ChatRoomsAdapter.kt
+23
-9
ChatRoomsFragment.kt
...ava/chat/rocket/android/chatrooms/ui/ChatRoomsFragment.kt
+12
-5
MessageParser.kt
...src/main/java/chat/rocket/android/helper/MessageParser.kt
+58
-159
LocalRepository.kt
...ava/chat/rocket/android/infrastructure/LocalRepository.kt
+3
-1
ProfileFragment.kt
...in/java/chat/rocket/android/profile/ui/ProfileFragment.kt
+5
-3
CompletionStrategy.kt
...roid/widget/autocompletion/strategy/CompletionStrategy.kt
+1
-0
StringMatchingCompletionStrategy.kt
...letion/strategy/regex/StringMatchingCompletionStrategy.kt
+19
-4
TrieCompletionStrategy.kt
...et/autocompletion/strategy/trie/TrieCompletionStrategy.kt
+4
-0
SuggestionsAdapter.kt
...et/android/widget/autocompletion/ui/SuggestionsAdapter.kt
+14
-3
SuggestionsView.kt
...ocket/android/widget/autocompletion/ui/SuggestionsView.kt
+17
-16
EmojiParser.kt
...main/java/chat/rocket/android/widget/emoji/EmojiParser.kt
+19
-6
EmojiRepository.kt
.../java/chat/rocket/android/widget/emoji/EmojiRepository.kt
+6
-7
EmojiTypefaceSpan.kt
...ava/chat/rocket/android/widget/emoji/EmojiTypefaceSpan.kt
+5
-5
quote_vertical_bar.xml
app/src/main/res/drawable/quote_vertical_bar.xml
+3
-1
item_message_attachment.xml
app/src/main/res/layout/item_message_attachment.xml
+70
-0
message_action_bar.xml
app/src/main/res/layout/message_action_bar.xml
+11
-0
suggestion_member_item.xml
app/src/main/res/layout/suggestion_member_item.xml
+4
-3
strings.xml
app/src/main/res/values-pt-rBR/strings.xml
+8
-0
strings.xml
app/src/main/res/values/strings.xml
+8
-0
styles.xml
app/src/main/res/values/styles.xml
+4
-0
No files found.
app/src/main/java/chat/rocket/android/app/RocketChatApplication.kt
View file @
8685b5dd
...
...
@@ -24,7 +24,6 @@ import chat.rocket.android.server.domain.*
import
chat.rocket.android.server.domain.model.Account
import
chat.rocket.android.widget.emoji.EmojiRepository
import
chat.rocket.common.model.Token
import
chat.rocket.common.util.ifNull
import
chat.rocket.core.model.Value
import
com.crashlytics.android.Crashlytics
import
com.crashlytics.android.core.CrashlyticsCore
...
...
app/src/main/java/chat/rocket/android/chatroom/adapter/ChatRoomAdapter.kt
View file @
8685b5dd
...
...
@@ -49,6 +49,10 @@ class ChatRoomAdapter(
val
view
=
parent
.
inflate
(
R
.
layout
.
message_url_preview
)
UrlPreviewViewHolder
(
view
,
actionsListener
,
reactionListener
)
}
BaseViewModel
.
ViewType
.
MESSAGE_ATTACHMENT
->
{
val
view
=
parent
.
inflate
(
R
.
layout
.
item_message_attachment
)
MessageAttachmentViewHolder
(
view
,
actionsListener
,
reactionListener
)
}
else
->
{
throw
InvalidParameterException
(
"TODO - implement for ${viewType.toViewType()}"
)
}
...
...
@@ -87,6 +91,7 @@ class ChatRoomAdapter(
is
AudioAttachmentViewHolder
->
holder
.
bind
(
dataSet
[
position
]
as
AudioAttachmentViewModel
)
is
VideoAttachmentViewHolder
->
holder
.
bind
(
dataSet
[
position
]
as
VideoAttachmentViewModel
)
is
UrlPreviewViewHolder
->
holder
.
bind
(
dataSet
[
position
]
as
UrlPreviewViewModel
)
is
MessageAttachmentViewHolder
->
holder
.
bind
(
dataSet
[
position
]
as
MessageAttachmentViewModel
)
}
}
...
...
@@ -117,19 +122,22 @@ class ChatRoomAdapter(
fun
updateItem
(
message
:
BaseViewModel
<
*
>)
{
var
index
=
dataSet
.
indexOfLast
{
it
.
messageId
==
message
.
messageId
}
val
indexOf
Firs
t
=
dataSet
.
indexOfFirst
{
it
.
messageId
==
message
.
messageId
}
val
indexOf
Nex
t
=
dataSet
.
indexOfFirst
{
it
.
messageId
==
message
.
messageId
}
Timber
.
d
(
"index: $index"
)
if
(
index
>
-
1
)
{
dataSet
[
index
]
=
message
dataSet
.
forEachIndexed
{
index
,
viewModel
->
if
(
viewModel
.
messageId
==
message
.
messageId
)
{
if
(
viewModel
.
nextDownStreamMessage
==
null
)
{
viewModel
.
reactions
=
message
.
reactions
}
notifyItemChanged
(
index
)
while
(
dataSet
[
index
].
nextDownStreamMessage
!=
null
)
{
dataSet
[
index
].
nextDownStreamMessage
!!
.
reactions
=
message
.
reactions
notifyItemChanged
(--
index
)
}
}
// Delete message only if current is a system message update, i.e.: Message Removed
if
(
message
.
message
.
isSystemMessage
()
&&
indexOf
First
>
-
1
&&
indexOfFirs
t
!=
index
)
{
dataSet
.
removeAt
(
indexOf
Firs
t
)
notifyItemRemoved
(
indexOf
Firs
t
)
if
(
message
.
message
.
isSystemMessage
()
&&
indexOf
Next
>
-
1
&&
indexOfNex
t
!=
index
)
{
dataSet
.
removeAt
(
indexOf
Nex
t
)
notifyItemRemoved
(
indexOf
Nex
t
)
}
}
}
...
...
app/src/main/java/chat/rocket/android/chatroom/adapter/CommandSuggestionsAdapter.kt
View file @
8685b5dd
...
...
@@ -12,7 +12,7 @@ import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import
chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
class
CommandSuggestionsAdapter
:
SuggestionsAdapter
<
CommandSuggestionsViewHolder
>(
token
=
"/"
,
constraint
=
CONSTRAINT_BOUND_TO_START
,
threshold
=
UNLIMITED_RESULT_COUNT
)
{
constraint
=
CONSTRAINT_BOUND_TO_START
,
threshold
=
RESULT_COUNT_UNLIMITED
)
{
override
fun
onCreateViewHolder
(
parent
:
ViewGroup
,
viewType
:
Int
):
CommandSuggestionsViewHolder
{
val
view
=
LayoutInflater
.
from
(
parent
.
context
).
inflate
(
R
.
layout
.
suggestion_command_item
,
parent
,
...
...
app/src/main/java/chat/rocket/android/chatroom/adapter/MessageAttachmentViewHolder.kt
0 → 100644
View file @
8685b5dd
package
chat.rocket.android.chatroom.adapter
import
android.text.method.LinkMovementMethod
import
android.view.View
import
chat.rocket.android.chatroom.viewmodel.MessageAttachmentViewModel
import
chat.rocket.android.widget.emoji.EmojiReactionListener
import
kotlinx.android.synthetic.main.item_message.view.*
class
MessageAttachmentViewHolder
(
itemView
:
View
,
listener
:
ActionsListener
,
reactionListener
:
EmojiReactionListener
?
=
null
)
:
BaseViewHolder
<
MessageAttachmentViewModel
>(
itemView
,
listener
,
reactionListener
)
{
init
{
with
(
itemView
)
{
text_content
.
movementMethod
=
LinkMovementMethod
()
setupActionMenu
(
text_content
)
}
}
override
fun
bindViews
(
data
:
MessageAttachmentViewModel
)
{
with
(
itemView
)
{
text_message_time
.
text
=
data
.
time
text_sender
.
text
=
data
.
senderName
text_content
.
text
=
data
.
content
}
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatroom/adapter/PeopleSuggestionsAdapter.kt
View file @
8685b5dd
package
chat.rocket.android.chatroom.adapter
import
DrawableHelper
import
android.content.Context
import
android.view.LayoutInflater
import
android.view.View
import
android.view.ViewGroup
...
...
@@ -13,10 +14,32 @@ import chat.rocket.android.util.extensions.setVisible
import
chat.rocket.android.widget.autocompletion.model.SuggestionModel
import
chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import
chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import
chat.rocket.common.model.UserStatus
import
com.facebook.drawee.view.SimpleDraweeView
class
PeopleSuggestionsAdapter
:
SuggestionsAdapter
<
PeopleSuggestionViewHolder
>(
"@"
)
{
class
PeopleSuggestionsAdapter
(
context
:
Context
)
:
SuggestionsAdapter
<
PeopleSuggestionViewHolder
>(
"@"
)
{
init
{
val
allDescription
=
context
.
getString
(
R
.
string
.
suggest_all_description
)
val
hereDescription
=
context
.
getString
(
R
.
string
.
suggest_here_description
)
val
pinnedList
=
listOf
(
PeopleSuggestionViewModel
(
imageUri
=
null
,
text
=
"all"
,
username
=
"all"
,
name
=
allDescription
,
status
=
null
,
pinned
=
false
,
searchList
=
listOf
(
"all"
)),
PeopleSuggestionViewModel
(
imageUri
=
null
,
text
=
"here"
,
username
=
"here"
,
name
=
hereDescription
,
status
=
null
,
pinned
=
false
,
searchList
=
listOf
(
"here"
))
)
setPinnedSuggestions
(
pinnedList
)
}
override
fun
onCreateViewHolder
(
parent
:
ViewGroup
,
viewType
:
Int
):
PeopleSuggestionViewHolder
{
val
view
=
LayoutInflater
.
from
(
parent
.
context
).
inflate
(
R
.
layout
.
suggestion_member_item
,
parent
,
false
)
...
...
@@ -34,15 +57,19 @@ class PeopleSuggestionsAdapter : SuggestionsAdapter<PeopleSuggestionViewHolder>(
val
statusView
=
itemView
.
findViewById
<
ImageView
>(
R
.
id
.
image_status
)
username
.
text
=
item
.
username
name
.
text
=
item
.
name
if
(
item
.
imageUri
.
isEmpty
()
)
{
if
(
item
.
imageUri
?.
isEmpty
()
!=
false
)
{
avatar
.
setVisible
(
false
)
}
else
{
avatar
.
setVisible
(
true
)
avatar
.
setImageURI
(
item
.
imageUri
)
}
val
status
=
item
.
status
?:
UserStatus
.
Offline
()
val
status
=
item
.
status
if
(
status
!=
null
)
{
val
statusDrawable
=
DrawableHelper
.
getUserStatusDrawable
(
status
,
itemView
.
context
)
statusView
.
setImageDrawable
(
statusDrawable
)
}
else
{
statusView
.
setVisible
(
false
)
}
setOnClickListener
{
itemClickListener
?.
onClick
(
item
)
}
...
...
app/src/main/java/chat/rocket/android/chatroom/presentation/ChatRoomPresenter.kt
View file @
8685b5dd
...
...
@@ -291,7 +291,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
view
.
showReplyingAction
(
username
=
user
,
replyMarkdown
=
"[ ]($serverUrl/$room/$roomName?msg=$id) $mention "
,
quotedMessage
=
m
.
message
quotedMessage
=
m
apper
.
map
(
message
).
last
().
preview
?.
message
?:
""
)
}
}
...
...
@@ -499,7 +499,6 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
//TODO: cache the commands
val
commands
=
client
.
commands
(
0
,
100
).
result
view
.
populateCommandSuggestions
(
commands
.
map
{
println
(
"${it.command} - ${it.description}"
)
CommandSuggestionViewModel
(
it
.
command
,
it
.
description
?:
""
,
listOf
(
it
.
command
))
})
}
catch
(
ex
:
RocketChatException
)
{
...
...
app/src/main/java/chat/rocket/android/chatroom/ui/ActionSnackbar.kt
View file @
8685b5dd
package
chat.rocket.android.chatroom.ui
import
android.graphics.drawable.Drawable
import
android.support.design.widget.BaseTransientBottomBar
import
android.support.v4.view.ViewCompat
import
android.text.Spannable
import
android.text.SpannableStringBuilder
import
android.view.LayoutInflater
import
android.view.View
import
android.view.ViewGroup
...
...
@@ -27,7 +27,6 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
actionSnackbar
.
cancelView
=
view
.
findViewById
(
R
.
id
.
image_view_action_cancel_quote
)
as
ImageView
actionSnackbar
.
duration
=
BaseTransientBottomBar
.
LENGTH_INDEFINITE
val
spannable
=
Markwon
.
markdown
(
context
,
content
).
trim
()
actionSnackbar
.
marginDrawable
=
context
.
getDrawable
(
R
.
drawable
.
quote
)
actionSnackbar
.
messageTextView
.
content
=
spannable
return
actionSnackbar
}
...
...
@@ -37,19 +36,16 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
lateinit
var
cancelView
:
View
private
lateinit
var
messageTextView
:
TextView
private
lateinit
var
titleTextView
:
TextView
private
lateinit
var
marginDrawable
:
Drawable
var
text
:
String
=
""
set
(
value
)
{
val
spannable
=
parser
.
renderMarkdown
(
value
)
as
Spannable
spannable
.
setSpan
(
MessageParser
.
QuoteMarginSpan
(
marginDrawable
,
10
),
0
,
spannable
.
length
,
0
)
val
spannable
=
SpannableStringBuilder
.
valueOf
(
value
)
messageTextView
.
content
=
spannable
}
var
title
:
String
=
""
set
(
value
)
{
val
spannable
=
Markwon
.
markdown
(
this
.
context
,
value
)
as
Spannable
spannable
.
setSpan
(
MessageParser
.
QuoteMarginSpan
(
marginDrawable
,
10
),
0
,
spannable
.
length
,
0
)
titleTextView
.
content
=
spannable
}
...
...
app/src/main/java/chat/rocket/android/chatroom/ui/ChatRoomFragment.kt
View file @
8685b5dd
...
...
@@ -36,7 +36,9 @@ import kotlinx.android.synthetic.main.message_attachment_options.*
import
kotlinx.android.synthetic.main.message_composer.*
import
kotlinx.android.synthetic.main.message_list.*
import
timber.log.Timber
import
java.util.concurrent.atomic.AtomicInteger
import
javax.inject.Inject
import
kotlin.math.absoluteValue
fun
newInstance
(
chatRoomId
:
String
,
chatRoomName
:
String
,
...
...
@@ -88,6 +90,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private
val
centerX
by
lazy
{
recycler_view
.
right
}
private
val
centerY
by
lazy
{
recycler_view
.
bottom
}
private
val
handler
=
Handler
()
private
var
verticalScrollOffset
=
AtomicInteger
(
0
)
override
fun
onCreate
(
savedInstanceState
:
Bundle
?)
{
super
.
onCreate
(
savedInstanceState
)
...
...
@@ -207,13 +210,54 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
})
}
recycler_view
.
addOnLayoutChangeListener
{
_
,
_
,
_
,
_
,
bottom
,
_
,
_
,
_
,
oldBottom
->
val
y
=
oldBottom
-
bottom
if
(
y
.
absoluteValue
>
0
)
{
// if y is positive the keyboard is up else it's down
recycler_view
.
post
{
if
(
y
>
0
||
verticalScrollOffset
.
get
().
absoluteValue
>=
y
.
absoluteValue
)
{
recycler_view
.
scrollBy
(
0
,
y
)
}
else
{
recycler_view
.
scrollBy
(
0
,
verticalScrollOffset
.
get
())
}
}
}
}
recycler_view
.
addOnScrollListener
(
object
:
RecyclerView
.
OnScrollListener
()
{
var
state
=
AtomicInteger
(
RecyclerView
.
SCROLL_STATE_IDLE
)
override
fun
onScrollStateChanged
(
recyclerView
:
RecyclerView
,
newState
:
Int
)
{
state
.
compareAndSet
(
RecyclerView
.
SCROLL_STATE_IDLE
,
newState
)
when
(
newState
)
{
RecyclerView
.
SCROLL_STATE_IDLE
->
{
if
(!
state
.
compareAndSet
(
RecyclerView
.
SCROLL_STATE_SETTLING
,
newState
))
{
state
.
compareAndSet
(
RecyclerView
.
SCROLL_STATE_DRAGGING
,
newState
)
}
}
RecyclerView
.
SCROLL_STATE_DRAGGING
->
{
state
.
compareAndSet
(
RecyclerView
.
SCROLL_STATE_IDLE
,
newState
)
}
RecyclerView
.
SCROLL_STATE_SETTLING
->
{
state
.
compareAndSet
(
RecyclerView
.
SCROLL_STATE_DRAGGING
,
newState
)
}
}
}
override
fun
onScrolled
(
recyclerView
:
RecyclerView
,
dx
:
Int
,
dy
:
Int
)
{
if
(
state
.
get
()
!=
RecyclerView
.
SCROLL_STATE_IDLE
)
{
verticalScrollOffset
.
getAndAdd
(
dy
)
}
}
})
}
val
oldMessagesCount
=
adapter
.
itemCount
adapter
.
appendData
(
dataSet
)
recycler_view
.
scrollToPosition
(
92
)
if
(
oldMessagesCount
==
0
&&
dataSet
.
isNotEmpty
())
{
recycler_view
.
scrollToPosition
(
0
)
verticalScrollOffset
.
set
(
0
)
}
presenter
.
loadActiveMembers
(
chatRoomId
,
chatRoomType
,
filterSelfOut
=
true
)
}
...
...
@@ -241,6 +285,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override
fun
showNewMessage
(
message
:
List
<
BaseViewModel
<*
>>)
{
adapter
.
prependData
(
message
)
recycler_view
.
scrollToPosition
(
0
)
verticalScrollOffset
.
set
(
0
)
}
override
fun
disableSendMessageButton
()
{
...
...
@@ -281,6 +326,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if
(!
recycler_view
.
isAtBottom
())
{
if
(
adapter
.
itemCount
>
0
)
{
recycler_view
.
scrollToPosition
(
0
)
verticalScrollOffset
.
set
(
0
)
}
}
}
...
...
@@ -430,6 +476,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private
fun
setupFab
()
{
button_fab
.
setOnClickListener
{
recycler_view
.
scrollToPosition
(
0
)
verticalScrollOffset
.
set
(
0
)
button_fab
.
hide
()
}
}
...
...
@@ -453,11 +500,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
emojiKeyboardPopup
.
listener
=
this
text_message
.
listener
=
object
:
ComposerEditText
.
ComposerEditTextListener
{
override
fun
onKeyboardOpened
()
{
if
(
recycler_view
.
isAtBottom
())
{
if
(
adapter
.
itemCount
>
0
)
{
recycler_view
.
scrollToPosition
(
0
)
}
}
}
override
fun
onKeyboardClosed
()
{
...
...
@@ -511,7 +553,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private
fun
setupSuggestionsView
()
{
suggestions_view
.
anchorTo
(
text_message
)
.
setMaximumHeight
(
resources
.
getDimensionPixelSize
(
R
.
dimen
.
suggestions_box_max_height
))
.
addTokenAdapter
(
PeopleSuggestionsAdapter
())
.
addTokenAdapter
(
PeopleSuggestionsAdapter
(
context
!!
))
.
addTokenAdapter
(
CommandSuggestionsAdapter
())
.
addTokenAdapter
(
RoomSuggestionsAdapter
())
.
addSuggestionProviderAction
(
"@"
)
{
query
->
...
...
app/src/main/java/chat/rocket/android/chatroom/viewmodel/AudioAttachmentViewModel.kt
View file @
8685b5dd
...
...
@@ -12,8 +12,9 @@ data class AudioAttachmentViewModel(
override
val
attachmentTitle
:
CharSequence
,
override
val
id
:
Long
,
override
var
reactions
:
List
<
ReactionViewModel
>,
override
var
nextDownStreamMessage
:
BaseViewModel
<*>?
=
null
)
:
BaseFileAttachmentViewModel
<
AudioAttachment
>
{
override
var
nextDownStreamMessage
:
BaseViewModel
<*>?
=
null
,
override
var
preview
:
Message
?
=
null
)
:
BaseFileAttachmentViewModel
<
AudioAttachment
>
{
override
val
viewType
:
Int
get
()
=
BaseViewModel
.
ViewType
.
AUDIO_ATTACHMENT
.
viewType
override
val
layoutId
:
Int
...
...
app/src/main/java/chat/rocket/android/chatroom/viewmodel/BaseViewModel.kt
View file @
8685b5dd
...
...
@@ -11,6 +11,7 @@ interface BaseViewModel<out T> {
val
layoutId
:
Int
var
reactions
:
List
<
ReactionViewModel
>
var
nextDownStreamMessage
:
BaseViewModel
<*>?
var
preview
:
Message
?
enum
class
ViewType
(
val
viewType
:
Int
)
{
MESSAGE
(
0
),
...
...
app/src/main/java/chat/rocket/android/chatroom/viewmodel/ImageAttachmentViewModel.kt
View file @
8685b5dd
...
...
@@ -12,7 +12,8 @@ data class ImageAttachmentViewModel(
override
val
attachmentTitle
:
CharSequence
,
override
val
id
:
Long
,
override
var
reactions
:
List
<
ReactionViewModel
>,
override
var
nextDownStreamMessage
:
BaseViewModel
<*>?
=
null
override
var
nextDownStreamMessage
:
BaseViewModel
<*>?
=
null
,
override
var
preview
:
Message
?
=
null
)
:
BaseFileAttachmentViewModel
<
ImageAttachment
>
{
override
val
viewType
:
Int
get
()
=
BaseViewModel
.
ViewType
.
IMAGE_ATTACHMENT
.
viewType
...
...
app/src/main/java/chat/rocket/android/chatroom/viewmodel/MessageAttachmentViewModel.kt
0 → 100644
View file @
8685b5dd
package
chat.rocket.android.chatroom.viewmodel
import
chat.rocket.android.R
import
chat.rocket.core.model.Message
data class
MessageAttachmentViewModel
(
override
val
message
:
Message
,
override
val
rawData
:
Message
,
override
val
messageId
:
String
,
var
senderName
:
String
,
val
time
:
CharSequence
,
val
content
:
CharSequence
,
val
isPinned
:
Boolean
,
override
var
reactions
:
List
<
ReactionViewModel
>,
override
var
nextDownStreamMessage
:
BaseViewModel
<*>?
=
null
,
var
messageLink
:
String
?
=
null
,
override
var
preview
:
Message
?
=
null
)
:
BaseViewModel
<
Message
>
{
override
val
viewType
:
Int
get
()
=
BaseViewModel
.
ViewType
.
MESSAGE_ATTACHMENT
.
viewType
override
val
layoutId
:
Int
get
()
=
R
.
layout
.
item_message_attachment
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatroom/viewmodel/MessageViewModel.kt
View file @
8685b5dd
...
...
@@ -14,6 +14,7 @@ data class MessageViewModel(
override
val
isPinned
:
Boolean
,
override
var
reactions
:
List
<
ReactionViewModel
>,
override
var
nextDownStreamMessage
:
BaseViewModel
<*>?
=
null
,
override
var
preview
:
Message
?
=
null
,
var
isFirstUnread
:
Boolean
)
:
BaseMessageViewModel
<
Message
>
{
override
val
viewType
:
Int
...
...
app/src/main/java/chat/rocket/android/chatroom/viewmodel/UrlPreviewViewModel.kt
View file @
8685b5dd
...
...
@@ -13,7 +13,8 @@ data class UrlPreviewViewModel(
val
description
:
CharSequence
?,
val
thumbUrl
:
String
?,
override
var
reactions
:
List
<
ReactionViewModel
>,
override
var
nextDownStreamMessage
:
BaseViewModel
<*>?
=
null
override
var
nextDownStreamMessage
:
BaseViewModel
<*>?
=
null
,
override
var
preview
:
Message
?
=
null
)
:
BaseViewModel
<
Url
>
{
override
val
viewType
:
Int
get
()
=
BaseViewModel
.
ViewType
.
URL_PREVIEW
.
viewType
...
...
app/src/main/java/chat/rocket/android/chatroom/viewmodel/VideoAttachmentViewModel.kt
View file @
8685b5dd
...
...
@@ -12,7 +12,8 @@ data class VideoAttachmentViewModel(
override
val
attachmentTitle
:
CharSequence
,
override
val
id
:
Long
,
override
var
reactions
:
List
<
ReactionViewModel
>,
override
var
nextDownStreamMessage
:
BaseViewModel
<*>?
=
null
override
var
nextDownStreamMessage
:
BaseViewModel
<*>?
=
null
,
override
var
preview
:
Message
?
=
null
)
:
BaseFileAttachmentViewModel
<
VideoAttachment
>
{
override
val
viewType
:
Int
get
()
=
BaseViewModel
.
ViewType
.
VIDEO_ATTACHMENT
.
viewType
...
...
app/src/main/java/chat/rocket/android/chatroom/viewmodel/ViewModelMapper.kt
View file @
8685b5dd
...
...
@@ -4,9 +4,7 @@ import DateTimeHelper
import
android.content.Context
import
android.graphics.Color
import
android.graphics.Typeface
import
android.text.SpannableString
import
android.text.SpannableStringBuilder
import
android.text.style.AbsoluteSizeSpan
import
android.text.style.ForegroundColorSpan
import
android.text.style.StyleSpan
import
chat.rocket.android.R
...
...
@@ -22,10 +20,8 @@ import chat.rocket.core.model.attachment.*
import
chat.rocket.core.model.isSystemMessage
import
chat.rocket.core.model.url.Url
import
kotlinx.coroutines.experimental.CommonPool
import
kotlinx.coroutines.experimental.launch
import
kotlinx.coroutines.experimental.withContext
import
okhttp3.HttpUrl
import
timber.log.Timber
import
java.security.InvalidParameterException
import
javax.inject.Inject
...
...
@@ -72,9 +68,17 @@ class ViewModelMapper @Inject constructor(private val context: Context,
}
mapMessage
(
message
).
let
{
if
(
list
.
size
>
0
)
{
it
.
preview
=
list
[
0
].
preview
}
list
.
add
(
it
)
}
for
(
i
in
list
.
size
-
1
downTo
0
)
{
val
next
=
if
(
i
-
1
<
0
)
null
else
list
[
i
-
1
]
list
[
i
].
nextDownStreamMessage
=
next
}
return
@withContext
list
}
...
...
@@ -87,27 +91,47 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val
description
=
url
.
meta
?.
description
return
UrlPreviewViewModel
(
message
,
url
,
message
.
id
,
title
,
hostname
,
description
,
thumb
,
getReactions
(
message
))
getReactions
(
message
)
,
preview
=
message
.
copy
(
message
=
url
.
url
)
)
}
private
fun
mapAttachment
(
message
:
Message
,
attachment
:
Attachment
):
BaseViewModel
<
*
>?
{
private
suspend
fun
mapAttachment
(
message
:
Message
,
attachment
:
Attachment
):
BaseViewModel
<
*
>?
{
return
when
(
attachment
)
{
is
FileAttachment
->
mapFileAttachment
(
message
,
attachment
)
is
MessageAttachment
->
mapMessageAttachment
(
message
,
attachment
)
else
->
null
}
}
private
suspend
fun
mapMessageAttachment
(
message
:
Message
,
attachment
:
MessageAttachment
):
MessageAttachmentViewModel
{
val
attachmentAuthor
=
attachment
.
author
!!
val
time
=
getTime
(
attachment
.
timestamp
!!
)
val
attachmentText
=
when
(
attachment
.
attachments
.
orEmpty
().
firstOrNull
())
{
is
ImageAttachment
->
context
.
getString
(
R
.
string
.
msg_preview_photo
)
is
VideoAttachment
->
context
.
getString
(
R
.
string
.
msg_preview_video
)
is
AudioAttachment
->
context
.
getString
(
R
.
string
.
msg_preview_audio
)
else
->
attachment
.
text
?:
""
}
val
content
=
stripMessageQuotes
(
message
)
return
MessageAttachmentViewModel
(
message
=
content
,
rawData
=
message
,
messageId
=
message
.
id
,
time
=
time
,
senderName
=
attachmentAuthor
,
content
=
attachmentText
,
isPinned
=
message
.
pinned
,
reactions
=
getReactions
(
message
),
preview
=
message
.
copy
(
message
=
content
.
message
))
}
private
fun
mapFileAttachment
(
message
:
Message
,
attachment
:
FileAttachment
):
BaseViewModel
<
*
>?
{
val
attachmentUrl
=
attachmentUrl
(
attachment
)
val
attachmentTitle
=
attachmentTitle
(
attachment
)
val
id
=
attachmentId
(
message
,
attachment
)
return
when
(
attachment
)
{
is
ImageAttachment
->
ImageAttachmentViewModel
(
message
,
attachment
,
message
.
id
,
attachmentUrl
,
attachmentTitle
,
id
,
getReactions
(
message
))
attachmentUrl
,
attachmentTitle
,
id
,
getReactions
(
message
),
preview
=
message
.
copy
(
message
=
context
.
getString
(
R
.
string
.
msg_preview_photo
)))
is
VideoAttachment
->
VideoAttachmentViewModel
(
message
,
attachment
,
message
.
id
,
attachmentUrl
,
attachmentTitle
,
id
,
getReactions
(
message
))
attachmentUrl
,
attachmentTitle
,
id
,
getReactions
(
message
),
preview
=
message
.
copy
(
message
=
context
.
getString
(
R
.
string
.
msg_preview_video
)))
is
AudioAttachment
->
AudioAttachmentViewModel
(
message
,
attachment
,
message
.
id
,
attachmentUrl
,
attachmentTitle
,
id
,
getReactions
(
message
))
attachmentUrl
,
attachmentTitle
,
id
,
getReactions
(
message
),
preview
=
message
.
copy
(
message
=
context
.
getString
(
R
.
string
.
msg_preview_audio
)))
else
->
null
}
}
...
...
@@ -151,32 +175,20 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val
sender
=
getSenderName
(
message
)
val
time
=
getTime
(
message
.
timestamp
)
val
avatar
=
getUserAvatar
(
message
)
val
preview
=
mapMessagePreview
(
message
)
val
baseUrl
=
settings
.
baseUrl
()
var
quote
:
Message
?
=
null
val
urls
=
ArrayList
<
Url
>()
message
.
urls
?.
let
{
if
(
it
.
isEmpty
())
return
@let
for
(
url
in
it
)
{
urls
.
add
(
url
)
baseUrl
?.
let
{
val
quoteUrl
=
HttpUrl
.
parse
(
url
.
url
)
val
serverUrl
=
HttpUrl
.
parse
(
baseUrl
)
if
(
quoteUrl
!=
null
&&
serverUrl
!=
null
)
{
quote
=
makeQuote
(
quoteUrl
,
serverUrl
)
?.
let
{
getMessageWithoutQuoteMarkdown
(
it
)
}
}
}
}
}
val
content
=
getContent
(
context
,
getMessageWithoutQuoteMarkdown
(
message
),
quote
)
MessageViewModel
(
message
=
getMessageWithoutQuoteMarkdown
(
message
),
rawData
=
message
,
val
content
=
getContent
(
stripMessageQuotes
(
message
))
MessageViewModel
(
message
=
stripMessageQuotes
(
message
),
rawData
=
message
,
messageId
=
message
.
id
,
avatar
=
avatar
!!
,
time
=
time
,
senderName
=
sender
,
content
=
content
,
isPinned
=
message
.
pinned
,
reactions
=
getReactions
(
message
),
isFirstUnread
=
false
)
isFirstUnread
=
false
,
preview
=
preview
)
}
private
suspend
fun
mapMessagePreview
(
message
:
Message
):
Message
{
return
when
(
message
.
isSystemMessage
())
{
false
->
stripMessageQuotes
(
message
)
true
->
message
.
copy
(
message
=
getSystemMessage
(
message
).
toString
())
}
}
private
fun
getReactions
(
message
:
Message
):
List
<
ReactionViewModel
>
{
...
...
@@ -198,7 +210,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
return
reactions
?:
emptyList
()
}
private
fun
getMessageWithoutQuoteMarkdown
(
message
:
Message
):
Message
{
private
suspend
fun
stripMessageQuotes
(
message
:
Message
):
Message
{
val
baseUrl
=
settings
.
baseUrl
()
return
message
.
copy
(
message
=
message
.
message
.
replace
(
"\\[\\s\\]\\($baseUrl.*\\)"
.
toRegex
(),
""
).
trim
()
...
...
@@ -229,33 +241,14 @@ class ViewModelMapper @Inject constructor(private val context: Context,
private
fun
getTime
(
timestamp
:
Long
)
=
DateTimeHelper
.
getTime
(
DateTimeHelper
.
getLocalDateTime
(
timestamp
))
private
fun
makeQuote
(
quoteUrl
:
HttpUrl
,
serverUrl
:
HttpUrl
):
Message
?
{
if
(
quoteUrl
.
host
()
==
serverUrl
.
host
())
{
val
msgIdToQuote
=
quoteUrl
.
queryParameter
(
"msg"
)
Timber
.
d
(
"Will quote message Id: $msgIdToQuote"
)
return
if
(
msgIdToQuote
!=
null
)
messagesRepository
.
getById
(
msgIdToQuote
)
else
null
}
return
null
}
private
suspend
fun
getContent
(
context
:
Context
,
message
:
Message
,
quote
:
Message
?):
CharSequence
{
private
suspend
fun
getContent
(
message
:
Message
):
CharSequence
{
return
when
(
message
.
isSystemMessage
())
{
true
->
getSystemMessage
(
message
,
context
)
false
->
getNormalMessage
(
message
,
quot
e
)
true
->
getSystemMessage
(
message
)
false
->
parser
.
renderMarkdown
(
message
,
currentUsernam
e
)
}
}
private
suspend
fun
getNormalMessage
(
message
:
Message
,
quote
:
Message
?):
CharSequence
{
var
quoteViewModel
:
MessageViewModel
?
=
null
if
(
quote
!=
null
)
{
val
quoteMessage
:
Message
=
quote
quoteViewModel
=
mapMessage
(
quoteMessage
)
}
return
parser
.
renderMarkdown
(
message
.
message
,
quoteViewModel
,
currentUsername
)
}
private
fun
getSystemMessage
(
message
:
Message
,
context
:
Context
):
CharSequence
{
private
fun
getSystemMessage
(
message
:
Message
):
CharSequence
{
val
content
=
when
(
message
.
type
)
{
//TODO: Add implementation for Welcome type.
is
MessageType
.
MessageRemoved
->
context
.
getString
(
R
.
string
.
message_removed
)
...
...
@@ -264,68 +257,17 @@ class ViewModelMapper @Inject constructor(private val context: Context,
is
MessageType
.
UserAdded
->
context
.
getString
(
R
.
string
.
message_user_added_by
,
message
.
message
,
message
.
sender
?.
username
)
is
MessageType
.
RoomNameChanged
->
context
.
getString
(
R
.
string
.
message_room_name_changed
,
message
.
message
,
message
.
sender
?.
username
)
is
MessageType
.
UserRemoved
->
context
.
getString
(
R
.
string
.
message_user_removed_by
,
message
.
message
,
message
.
sender
?.
username
)
is
MessageType
.
MessagePinned
->
{
val
attachment
=
message
.
attachments
?.
get
(
0
)
val
pinnedSystemMessage
=
context
.
getString
(
R
.
string
.
message_pinned
)
if
(
attachment
!=
null
&&
attachment
is
MessageAttachment
)
{
return
SpannableStringBuilder
(
pinnedSystemMessage
)
.
apply
{
setSpan
(
StyleSpan
(
Typeface
.
ITALIC
),
0
,
length
,
0
)
setSpan
(
ForegroundColorSpan
(
Color
.
GRAY
),
0
,
length
,
0
)
}
.
append
(
quoteMessage
(
attachment
.
author
!!
,
attachment
.
text
!!
,
attachment
.
timestamp
!!
))
}
return
pinnedSystemMessage
}
is
MessageType
.
MessagePinned
->
context
.
getString
(
R
.
string
.
message_pinned
)
else
->
{
throw
InvalidParameterException
(
"Invalid message type: ${message.type}"
)
}
}
//isSystemMessage = true
val
spannableMsg
=
SpannableStringBuilder
(
content
)
spannableMsg
.
setSpan
(
StyleSpan
(
Typeface
.
ITALIC
),
0
,
spannableMsg
.
length
,
0
)
spannableMsg
.
setSpan
(
ForegroundColorSpan
(
Color
.
GRAY
),
0
,
spannableMsg
.
length
,
0
)
/*if (attachmentType == null) {
val username = message.sender?.username
val message = message.message
val usernameTextStartIndex = if (username != null) content.indexOf(username) else -1
val usernameTextEndIndex = if (username != null) usernameTextStartIndex + username.length else -1
val messageTextStartIndex = if (message.isNotEmpty()) content.indexOf(message) else -1
val messageTextEndIndex = messageTextStartIndex + message.length
if (usernameTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), usernameTextStartIndex, usernameTextEndIndex,
0)
}
if (messageTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), messageTextStartIndex, messageTextEndIndex,
0)
}
} else if (attachmentType == AttachmentType.Message) {
spannableMsg.append(quoteMessage(attachmentMessageAuthor!!, attachmentMessageText!!, attachmentTimestamp!!))
}*/
return
spannableMsg
}
private
fun
quoteMessage
(
author
:
String
,
text
:
String
,
timestamp
:
Long
):
CharSequence
{
return
SpannableStringBuilder
().
apply
{
val
header
=
"\n$author ${getTime(timestamp)}\n"
append
(
SpannableString
(
header
).
apply
{
setSpan
(
StyleSpan
(
Typeface
.
BOLD
),
1
,
author
.
length
+
1
,
0
)
setSpan
(
MessageParser
.
QuoteMarginSpan
(
context
.
getDrawable
(
R
.
drawable
.
quote
),
10
),
1
,
length
,
0
)
setSpan
(
AbsoluteSizeSpan
(
context
.
resources
.
getDimensionPixelSize
(
R
.
dimen
.
message_time_text_size
)),
author
.
length
+
1
,
length
,
0
)
})
append
(
SpannableString
(
parser
.
renderMarkdown
(
text
)).
apply
{
setSpan
(
MessageParser
.
QuoteMarginSpan
(
context
.
getDrawable
(
R
.
drawable
.
quote
),
10
),
0
,
length
,
0
)
})
}
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/chatroom/viewmodel/suggestion/PeopleSuggestionViewModel.kt
View file @
8685b5dd
...
...
@@ -3,7 +3,7 @@ package chat.rocket.android.chatroom.viewmodel.suggestion
import
chat.rocket.android.widget.autocompletion.model.SuggestionModel
import
chat.rocket.common.model.UserStatus
class
PeopleSuggestionViewModel
(
val
imageUri
:
String
,
class
PeopleSuggestionViewModel
(
val
imageUri
:
String
?
,
text
:
String
,
val
username
:
String
,
val
name
:
String
,
...
...
app/src/main/java/chat/rocket/android/chatrooms/presentation/ChatRoomsPresenter.kt
View file @
8685b5dd
package
chat.rocket.android.chatrooms.presentation
import
chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import
chat.rocket.android.core.lifecycle.CancelStrategy
import
chat.rocket.android.main.presentation.MainNavigator
import
chat.rocket.android.server.domain.*
...
...
@@ -9,7 +10,10 @@ import chat.rocket.android.server.infraestructure.chatRooms
import
chat.rocket.android.server.infraestructure.state
import
chat.rocket.android.util.extensions.launchUI
import
chat.rocket.common.RocketChatException
import
chat.rocket.common.model.*
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.core.internal.model.Subscription
import
chat.rocket.core.internal.realtime.State
import
chat.rocket.core.internal.realtime.StreamMessage
...
...
@@ -30,6 +34,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private
val
getChatRoomsInteractor
:
GetChatRoomsInteractor
,
private
val
saveChatRoomsInteractor
:
SaveChatRoomsInteractor
,
private
val
refreshSettingsInteractor
:
RefreshSettingsInteractor
,
private
val
viewModelMapper
:
ViewModelMapper
,
settingsRepository
:
SettingsRepository
,
factory
:
ConnectionManagerFactory
)
{
private
val
manager
:
ConnectionManager
=
factory
.
create
(
serverInteractor
.
get
()
!!
)
...
...
@@ -89,9 +94,9 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
val
chatRoomsCombined
=
mutableListOf
<
ChatRoom
>()
chatRoomsCombined
.
addAll
(
usersToChatRooms
(
users
))
chatRoomsCombined
.
addAll
(
roomsToChatRooms
(
rooms
))
view
.
updateChatRooms
(
chatRoomsCombined
)
view
.
updateChatRooms
(
getChatRoomsWithPreviews
(
chatRoomsCombined
.
toList
())
)
}
else
{
view
.
updateChatRooms
(
roomList
)
view
.
updateChatRooms
(
getChatRoomsWithPreviews
(
roomList
)
)
}
}
catch
(
ex
:
RocketChatException
)
{
Timber
.
e
(
ex
)
...
...
@@ -156,7 +161,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
val
sortedRooms
=
sortRooms
(
chatRooms
)
Timber
.
d
(
"Loaded rooms: ${sortedRooms.size}"
)
saveChatRoomsInteractor
.
save
(
currentServer
,
sortedRooms
)
return
sortedRooms
return
getChatRoomsWithPreviews
(
sortedRooms
)
}
private
fun
sortRooms
(
chatRooms
:
List
<
ChatRoom
>):
List
<
ChatRoom
>
{
...
...
@@ -167,7 +172,17 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private
fun
updateRooms
()
{
Timber
.
d
(
"Updating Rooms"
)
launch
(
strategy
.
jobs
)
{
view
.
updateChatRooms
(
getChatRoomsInteractor
.
get
(
currentServer
))
view
.
updateChatRooms
(
getChatRoomsWithPreviews
(
getChatRoomsInteractor
.
get
(
currentServer
)))
}
}
private
suspend
fun
getChatRoomsWithPreviews
(
chatRooms
:
List
<
ChatRoom
>):
List
<
ChatRoom
>
{
return
chatRooms
.
map
{
if
(
it
.
lastMessage
!=
null
)
{
it
.
copy
(
lastMessage
=
viewModelMapper
.
map
(
it
.
lastMessage
!!
).
last
().
preview
)
}
else
{
it
}
}
}
...
...
@@ -304,7 +319,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
// Update a ChatRoom with a Subscription information
private
fun
updateSubscription
(
subscription
:
Subscription
)
{
Timber
.
d
(
"Updating subscrition: ${subscription.id} - ${subscription.name}"
)
Timber
.
d
(
"Updating subscri
p
tion: ${subscription.id} - ${subscription.name}"
)
val
chatRooms
=
getChatRoomsInteractor
.
get
(
currentServer
).
toMutableList
()
val
chatRoom
=
chatRooms
.
find
{
chatRoom
->
chatRoom
.
id
==
subscription
.
roomId
}
chatRoom
?.
apply
{
...
...
app/src/main/java/chat/rocket/android/chatrooms/ui/ChatRoomsAdapter.kt
View file @
8685b5dd
...
...
@@ -3,14 +3,18 @@ package chat.rocket.android.chatrooms.ui
import
DateTimeHelper
import
DrawableHelper
import
android.content.Context
import
android.graphics.
drawable.Drawable
import
android.graphics.
Color
import
android.support.v4.content.ContextCompat
import
android.support.v7.widget.RecyclerView
import
android.text.SpannableStringBuilder
import
android.text.style.ForegroundColorSpan
import
android.view.View
import
android.view.ViewGroup
import
android.widget.TextView
import
chat.rocket.android.R
import
chat.rocket.android.helper.UrlHelper
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.useRealName
import
chat.rocket.android.util.extensions.content
...
...
@@ -26,6 +30,7 @@ import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class
ChatRoomsAdapter
(
private
val
context
:
Context
,
private
val
settings
:
PublicSettings
,
private
val
localRepository
:
LocalRepository
,
private
val
listener
:
(
ChatRoom
)
->
Unit
)
:
RecyclerView
.
Adapter
<
ChatRoomsAdapter
.
ViewHolder
>()
{
var
dataSet
:
MutableList
<
ChatRoom
>
=
ArrayList
()
...
...
@@ -116,21 +121,30 @@ class ChatRoomsAdapter(private val context: Context,
val
lastMessageSender
=
lastMessage
?.
sender
if
(
lastMessage
!=
null
&&
lastMessageSender
!=
null
)
{
val
message
=
lastMessage
.
message
val
senderUsername
=
lastMessageSender
.
username
val
senderUsername
=
if
(
settings
.
useRealName
())
{
lastMessageSender
.
name
?:
lastMessageSender
.
username
}
else
{
lastMessageSender
.
username
}
when
(
senderUsername
)
{
chatRoom
.
name
->
{
textView
.
content
=
message
}
// TODO Change to MySelf
// chatRoom.user?.username -> {
// holder.lastMessage.textContent = context.getString(R.string.msg_you) + ": $message"
// }
else
->
{
textView
.
content
=
"@$senderUsername: $message"
val
user
=
if
(
localRepository
.
checkIfMyself
(
lastMessageSender
.
username
!!
))
{
"${context.getString(R.string.msg_you)}: "
}
else
{
"$senderUsername: "
}
val
spannable
=
SpannableStringBuilder
(
user
)
val
len
=
spannable
.
length
spannable
.
setSpan
(
ForegroundColorSpan
(
Color
.
BLACK
),
0
,
len
-
1
,
0
)
spannable
.
append
(
message
)
textView
.
content
=
spannable
}
}
}
else
{
textView
.
content
=
""
textView
.
content
=
context
.
getText
(
R
.
string
.
msg_no_messages_yet
)
}
}
...
...
app/src/main/java/chat/rocket/android/chatrooms/ui/ChatRoomsFragment.kt
View file @
8685b5dd
...
...
@@ -12,6 +12,7 @@ import android.view.*
import
chat.rocket.android.R
import
chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import
chat.rocket.android.chatrooms.presentation.ChatRoomsView
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.*
...
...
@@ -31,6 +32,7 @@ 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
var
searchView
:
SearchView
?
=
null
private
val
handler
=
Handler
()
...
...
@@ -108,7 +110,11 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override
fun
showLoading
()
=
view_loading
.
setVisible
(
true
)
override
fun
hideLoading
()
=
view_loading
.
setVisible
(
false
)
override
fun
hideLoading
()
{
if
(
view_loading
!=
null
)
{
view_loading
.
setVisible
(
false
)
}
}
override
fun
showMessage
(
resId
:
Int
)
{
showToast
(
resId
)
...
...
@@ -156,8 +162,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
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
recycler_view
.
adapter
=
ChatRoomsAdapter
(
this
,
settingsRepository
.
get
(
serverInteractor
.
get
()
!!
))
{
chatRoom
->
recycler_view
.
adapter
=
ChatRoomsAdapter
(
this
,
settingsRepository
.
get
(
serverInteractor
.
get
()
!!
),
localRepository
)
{
chatRoom
->
presenter
.
loadChatRoom
(
chatRoom
)
}
}
...
...
app/src/main/java/chat/rocket/android/helper/MessageParser.kt
View file @
8685b5dd
...
...
@@ -4,158 +4,113 @@ import android.app.Application
import
android.content.ActivityNotFoundException
import
android.content.Context
import
android.content.Intent
import
android.graphics.*
import
android.graphics.drawable.Drawable
import
android.graphics.Canvas
import
android.graphics.Paint
import
android.graphics.RectF
import
android.net.Uri
import
android.support.customtabs.CustomTabsIntent
import
android.provider.Browser
import
android.support.v4.content.ContextCompat
import
android.support.v4.content.res.ResourcesCompat
import
android.text.Layout
import
android.text.Spannable
import
android.text.Spanned
import
android.text.
TextUtils
import
android.text.style.
*
import
android.text.
style.ClickableSpan
import
android.text.style.
ReplacementSpan
import
android.util.Patterns
import
android.view.View
import
chat.rocket.android.R
import
chat.rocket.android.chatroom.viewmodel.MessageViewModel
import
chat.rocket.android.widget.emoji.EmojiParser
import
chat.rocket.android.widget.emoji.EmojiRepository
import
chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import
chat.rocket.common.model.SimpleUser
import
chat.rocket.core.model.Message
import
org.commonmark.node.AbstractVisitor
import
org.commonmark.node.
BlockQuote
import
org.commonmark.node.
Document
import
org.commonmark.node.Text
import
ru.noties.markwon.Markwon
import
ru.noties.markwon.SpannableBuilder
import
ru.noties.markwon.SpannableConfiguration
import
ru.noties.markwon.renderer.SpannableMarkdownVisitor
import
timber.log.Timber
import
java.util.regex.Pattern
import
javax.inject.Inject
class
MessageParser
@Inject
constructor
(
val
context
:
Application
,
private
val
configuration
:
SpannableConfiguration
)
{
private
val
parser
=
Markwon
.
createParser
()
private
val
regexUsername
=
Pattern
.
compile
(
"([^\\S]|^)+(@[\\w.\\-]+)"
,
Pattern
.
MULTILINE
or
Pattern
.
CASE_INSENSITIVE
)
private
val
selfReferList
=
listOf
(
"@all"
,
"@here"
)
/**
* Render a markdown text message to Spannable.
*
* @param text The text message containing markdown syntax.
* @param quote An optional message to be quoted either by a quote or reply action.
* @param urls A list of urls to convert to markdown link syntax.
* @param message The [Message] object we're interested on rendering.
* @param selfUsername This user username.
*
* @return A Spannable with the parsed markdown.
*/
fun
renderMarkdown
(
text
:
String
,
quote
:
MessageViewModel
?
=
null
,
selfUsername
:
String
?
=
null
):
CharSequence
{
fun
renderMarkdown
(
message
:
Message
,
selfUsername
:
String
?
=
null
):
CharSequence
{
val
text
=
message
.
message
val
builder
=
SpannableBuilder
()
val
content
=
EmojiRepository
.
shortnameToUnicode
(
text
,
true
)
val
parentNode
=
parser
.
parse
(
toLenientMarkdown
(
content
))
parentNode
.
accept
(
QuoteMessageBodyVisitor
(
context
,
configuration
,
builder
))
quote
?.
apply
{
var
quoteNode
=
parser
.
parse
(
"> $senderName $time"
)
parentNode
.
appendChild
(
quoteNode
)
quoteNode
.
accept
(
QuoteMessageSenderVisitor
(
context
,
configuration
,
builder
,
senderName
.
length
))
quoteNode
=
parser
.
parse
(
"> ${toLenientMarkdown(quote.rawData.message)}"
)
quoteNode
.
accept
(
EmojiVisitor
(
builder
))
quoteNode
.
accept
(
QuoteMessageBodyVisitor
(
context
,
configuration
,
builder
))
}
parentNode
.
accept
(
SpannableMarkdownVisitor
(
configuration
,
builder
))
parentNode
.
accept
(
LinkVisitor
(
builder
))
parentNode
.
accept
(
EmojiVisitor
(
builder
))
val
result
=
builder
.
text
()
applySpans
(
result
,
selfUsername
)
return
result
}
private
fun
applySpans
(
text
:
CharSequence
,
currentUser
:
String
?)
{
if
(
text
!
is
Spannable
)
return
applyMentionSpans
(
text
,
currentUser
)
}
private
fun
applyMentionSpans
(
text
:
CharSequence
,
currentUser
:
String
?)
{
val
matcher
=
regexUsername
.
matcher
(
text
)
val
result
=
text
as
Spannable
while
(
matcher
.
find
())
{
val
user
=
matcher
.
group
(
2
)
val
start
=
matcher
.
start
(
2
)
//TODO: should check if username actually exists prior to applying.
with
(
context
)
{
val
referSelf
=
when
(
user
)
{
in
selfReferList
->
true
"@$currentUser"
->
true
else
->
false
}
val
mentionTextColor
:
Int
val
mentionBgColor
:
Int
if
(
referSelf
)
{
mentionTextColor
=
ResourcesCompat
.
getColor
(
resources
,
R
.
color
.
white
,
theme
)
mentionBgColor
=
ResourcesCompat
.
getColor
(
context
.
resources
,
R
.
color
.
colorAccent
,
theme
)
}
else
{
mentionTextColor
=
ResourcesCompat
.
getColor
(
resources
,
R
.
color
.
colorAccent
,
theme
)
mentionBgColor
=
ResourcesCompat
.
getColor
(
resources
,
android
.
R
.
color
.
transparent
,
theme
)
parentNode
.
accept
(
EmojiVisitor
(
configuration
,
builder
))
message
.
mentions
?.
let
{
parentNode
.
accept
(
MentionVisitor
(
context
,
builder
,
it
,
selfUsername
))
}
val
padding
=
resources
.
getDimensionPixelSize
(
R
.
dimen
.
padding_mention
).
toFloat
()
val
radius
=
resources
.
getDimensionPixelSize
(
R
.
dimen
.
radius_mention
).
toFloat
()
val
usernameSpan
=
MentionSpan
(
mentionBgColor
,
mentionTextColor
,
radius
,
padding
,
referSelf
)
result
.
setSpan
(
usernameSpan
,
start
,
start
+
user
.
length
,
0
)
}
}
return
builder
.
text
()
}
/**
* Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs.
*/
// Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs.
private
fun
toLenientMarkdown
(
text
:
String
):
String
{
return
text
.
trim
().
replace
(
"\\*(.+)\\*"
.
toRegex
())
{
"**${it.groupValues[1].trim()}**"
}
.
replace
(
"\\~(.+)\\~"
.
toRegex
())
{
"~~${it.groupValues[1].trim()}~~"
}
.
replace
(
"\\_(.+)\\_"
.
toRegex
())
{
"_${it.groupValues[1].trim()}_"
}
}
class
QuoteMessageSenderVisitor
(
private
val
context
:
Context
,
configuration
:
SpannableConfiguration
,
class
MentionVisitor
(
context
:
Context
,
private
val
builder
:
SpannableBuilder
,
private
val
senderNameLength
:
Int
)
:
SpannableMarkdownVisitor
(
configuration
,
builder
)
{
override
fun
visit
(
blockQuote
:
BlockQuote
)
{
// mark current length
val
length
=
builder
.
length
()
// pass to super to apply markdown
super
.
visit
(
blockQuote
)
val
res
=
context
.
resources
val
timeOffsetStart
=
length
+
senderNameLength
+
1
builder
.
setSpan
(
QuoteMarginSpan
(
context
.
getDrawable
(
R
.
drawable
.
quote
),
10
),
length
,
builder
.
length
())
builder
.
setSpan
(
StyleSpan
(
Typeface
.
BOLD
),
length
,
length
+
senderNameLength
)
builder
.
setSpan
(
ForegroundColorSpan
(
Color
.
BLACK
),
length
,
builder
.
length
())
// set time spans
builder
.
setSpan
(
AbsoluteSizeSpan
(
res
.
getDimensionPixelSize
(
R
.
dimen
.
message_time_text_size
)),
timeOffsetStart
,
builder
.
length
())
builder
.
setSpan
(
ForegroundColorSpan
(
ContextCompat
.
getColor
(
context
,
R
.
color
.
darkGray
)),
timeOffsetStart
,
builder
.
length
())
}
}
class
EmojiVisitor
(
private
val
builder
:
SpannableBuilder
)
:
AbstractVisitor
()
{
override
fun
visit
(
text
:
Text
)
{
val
spannable
=
EmojiParser
.
parse
(
text
.
literal
)
private
val
mentions
:
List
<
SimpleUser
>,
private
val
currentUser
:
String
?)
:
AbstractVisitor
()
{
private
val
othersTextColor
=
ResourcesCompat
.
getColor
(
context
.
resources
,
R
.
color
.
colorAccent
,
context
.
theme
)
private
val
othersBackgroundColor
=
ResourcesCompat
.
getColor
(
context
.
resources
,
android
.
R
.
color
.
transparent
,
context
.
theme
)
private
val
myselfTextColor
=
ResourcesCompat
.
getColor
(
context
.
resources
,
R
.
color
.
white
,
context
.
theme
)
private
val
myselfBackgroundColor
=
ResourcesCompat
.
getColor
(
context
.
resources
,
R
.
color
.
colorAccent
,
context
.
theme
)
private
val
mentionPadding
=
context
.
resources
.
getDimensionPixelSize
(
R
.
dimen
.
padding_mention
).
toFloat
()
private
val
mentionRadius
=
context
.
resources
.
getDimensionPixelSize
(
R
.
dimen
.
radius_mention
).
toFloat
()
override
fun
visit
(
t
:
Text
)
{
val
text
=
t
.
literal
val
mentionsList
=
mentions
.
map
{
it
.
username
}.
toMutableList
()
mentionsList
.
add
(
"all"
)
mentionsList
.
add
(
"here"
)
mentionsList
.
toList
().
forEach
{
if
(
it
!=
null
)
{
val
mentionMe
=
it
==
currentUser
||
it
==
"all"
||
it
==
"here"
var
offset
=
text
.
indexOf
(
"@$it"
,
0
,
true
)
while
(
offset
>
-
1
)
{
val
textColor
=
if
(
mentionMe
)
myselfTextColor
else
othersTextColor
val
backgroundColor
=
if
(
mentionMe
)
myselfBackgroundColor
else
othersBackgroundColor
val
usernameSpan
=
MentionSpan
(
backgroundColor
,
textColor
,
mentionRadius
,
mentionPadding
,
mentionMe
)
// Add 1 to end offset to include the @.
val
end
=
offset
+
it
.
length
+
1
builder
.
setSpan
(
usernameSpan
,
offset
,
end
,
0
)
offset
=
text
.
indexOf
(
"@$it"
,
end
,
true
)
}
}
}
}
}
class
EmojiVisitor
(
configuration
:
SpannableConfiguration
,
private
val
builder
:
SpannableBuilder
)
:
SpannableMarkdownVisitor
(
configuration
,
builder
)
{
override
fun
visit
(
document
:
Document
)
{
val
spannable
=
EmojiParser
.
parse
(
builder
.
text
())
if
(
spannable
is
Spanned
)
{
val
spans
=
spannable
.
getSpans
(
0
,
spannable
.
length
,
EmojiTypefaceSpan
::
class
.
java
)
spans
.
forEach
{
builder
.
setSpan
(
it
,
spannable
.
getSpanStart
(
it
),
spannable
.
getSpanEnd
(
it
),
0
)
}
}
visitChildren
(
text
)
}
}
...
...
@@ -195,60 +150,6 @@ class MessageParser @Inject constructor(val context: Application, private val co
}
}
class
QuoteMessageBodyVisitor
(
private
val
context
:
Context
,
configuration
:
SpannableConfiguration
,
private
val
builder
:
SpannableBuilder
)
:
SpannableMarkdownVisitor
(
configuration
,
builder
)
{
override
fun
visit
(
blockQuote
:
BlockQuote
)
{
// mark current length
val
length
=
builder
.
length
()
// pass to super to apply markdown
super
.
visit
(
blockQuote
)
val
padding
=
context
.
resources
.
getDimensionPixelSize
(
R
.
dimen
.
padding_quote
)
builder
.
setSpan
(
QuoteMarginSpan
(
context
.
getDrawable
(
R
.
drawable
.
quote
),
padding
),
length
,
builder
.
length
())
}
}
class
QuoteMarginSpan
(
quoteDrawable
:
Drawable
,
private
var
pad
:
Int
)
:
LeadingMarginSpan
,
LineHeightSpan
{
private
val
drawable
:
Drawable
=
quoteDrawable
override
fun
getLeadingMargin
(
first
:
Boolean
):
Int
{
return
drawable
.
intrinsicWidth
+
pad
}
override
fun
drawLeadingMargin
(
c
:
Canvas
,
p
:
Paint
,
x
:
Int
,
dir
:
Int
,
top
:
Int
,
baseline
:
Int
,
bottom
:
Int
,
text
:
CharSequence
,
start
:
Int
,
end
:
Int
,
first
:
Boolean
,
layout
:
Layout
)
{
val
st
=
(
text
as
Spanned
).
getSpanStart
(
this
)
val
ix
=
x
val
itop
=
layout
.
getLineTop
(
layout
.
getLineForOffset
(
st
))
val
dw
=
drawable
.
intrinsicWidth
val
dh
=
drawable
.
intrinsicHeight
// XXX What to do about Paint?
drawable
.
setBounds
(
ix
,
itop
,
ix
+
dw
,
itop
+
layout
.
height
)
drawable
.
draw
(
c
)
}
override
fun
chooseHeight
(
text
:
CharSequence
,
start
:
Int
,
end
:
Int
,
spanstartv
:
Int
,
v
:
Int
,
fm
:
Paint
.
FontMetricsInt
)
{
if
(
end
==
(
text
as
Spanned
).
getSpanEnd
(
this
))
{
val
ht
=
drawable
.
intrinsicHeight
var
need
=
ht
-
(
v
+
fm
.
descent
-
fm
.
ascent
-
spanstartv
)
if
(
need
>
0
)
fm
.
descent
+=
need
need
=
ht
-
(
v
+
fm
.
bottom
-
fm
.
top
-
spanstartv
)
if
(
need
>
0
)
fm
.
bottom
+=
need
}
}
}
class
MentionSpan
(
private
val
backgroundColor
:
Int
,
private
val
textColor
:
Int
,
private
val
radius
:
Float
,
...
...
@@ -274,13 +175,11 @@ class MessageParser @Inject constructor(val context: Application, private val co
bottom
:
Int
,
paint
:
Paint
)
{
val
length
=
paint
.
measureText
(
text
.
subSequence
(
start
,
end
).
toString
())
val
rect
=
RectF
(
x
,
top
.
toFloat
(),
x
+
length
+
padding
*
2
,
bottom
.
toFloat
())
paint
.
setColor
(
backgroundColor
)
val
rect
=
RectF
(
x
,
top
.
toFloat
(),
x
+
length
+
padding
*
2
,
bottom
.
toFloat
())
paint
.
color
=
backgroundColor
canvas
.
drawRoundRect
(
rect
,
radius
,
radius
,
paint
)
paint
.
setColor
(
textColor
)
paint
.
color
=
textColor
canvas
.
drawText
(
text
,
start
,
end
,
x
+
padding
,
y
.
toFloat
(),
paint
)
}
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/infrastructure/LocalRepository.kt
View file @
8685b5dd
...
...
@@ -23,3 +23,5 @@ interface LocalRepository {
const
val
CURRENT_USERNAME_KEY
=
"username_"
}
}
fun
LocalRepository
.
checkIfMyself
(
username
:
String
)
=
get
(
LocalRepository
.
CURRENT_USERNAME_KEY
)
==
username
\ No newline at end of file
app/src/main/java/chat/rocket/android/profile/ui/ProfileFragment.kt
View file @
8685b5dd
...
...
@@ -56,8 +56,8 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_email
.
textContent
=
email
text_avatar_url
.
textContent
=
""
currentName
=
user
name
currentUsername
=
name
currentName
=
name
currentUsername
=
user
name
currentEmail
=
email
currentAvatar
=
avatarUrl
...
...
@@ -76,7 +76,9 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
}
override
fun
hideLoading
()
{
if
(
view_loading
!=
null
)
{
view_loading
.
setVisible
(
false
)
}
enableUserInput
(
true
)
}
...
...
app/src/main/java/chat/rocket/android/widget/autocompletion/strategy/CompletionStrategy.kt
View file @
8685b5dd
...
...
@@ -6,5 +6,6 @@ interface CompletionStrategy {
fun
getItem
(
prefix
:
String
,
position
:
Int
):
SuggestionModel
fun
autocompleteItems
(
prefix
:
String
):
List
<
SuggestionModel
>
fun
addAll
(
list
:
List
<
SuggestionModel
>)
fun
addPinned
(
list
:
List
<
SuggestionModel
>)
fun
size
():
Int
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/widget/autocompletion/strategy/regex/StringMatchingCompletionStrategy.kt
View file @
8685b5dd
...
...
@@ -2,14 +2,19 @@ package chat.rocket.android.widget.autocompletion.strategy.regex
import
chat.rocket.android.widget.autocompletion.model.SuggestionModel
import
chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import
chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import
chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
.Companion.RESULT_COUNT_UNLIMITED
import
java.util.concurrent.CopyOnWriteArrayList
internal
class
StringMatchingCompletionStrategy
(
private
val
threshold
:
Int
=
-
1
)
:
CompletionStrategy
{
internal
class
StringMatchingCompletionStrategy
(
private
val
threshold
:
Int
=
RESULT_COUNT_UNLIMITED
)
:
CompletionStrategy
{
private
val
list
=
CopyOnWriteArrayList
<
SuggestionModel
>()
private
val
pinnedList
=
mutableListOf
<
SuggestionModel
>()
init
{
check
(
threshold
>=
RESULT_COUNT_UNLIMITED
)
}
override
fun
autocompleteItems
(
prefix
:
String
):
List
<
SuggestionModel
>
{
val
r
esult
=
list
.
filter
{
val
partialR
esult
=
list
.
filter
{
it
.
searchList
.
forEach
{
word
->
if
(
word
.
contains
(
prefix
,
ignoreCase
=
true
))
{
return
@filter
true
...
...
@@ -17,13 +22,23 @@ internal class StringMatchingCompletionStrategy(private val threshold: Int = -1)
}
false
}.
sortedByDescending
{
it
.
pinned
}
return
if
(
threshold
==
SuggestionsAdapter
.
UNLIMITED_RESULT_COUNT
)
result
else
result
.
take
(
threshold
)
return
if
(
threshold
==
RESULT_COUNT_UNLIMITED
)
partialResult
.
toList
()
else
{
val
result
=
partialResult
.
take
(
threshold
).
toMutableList
()
result
.
addAll
(
pinnedList
)
result
.
toList
()
}
}
override
fun
addAll
(
list
:
List
<
SuggestionModel
>)
{
this
.
list
.
addAllAbsent
(
list
)
}
override
fun
addPinned
(
list
:
List
<
SuggestionModel
>)
{
this
.
pinnedList
.
addAll
(
list
)
}
override
fun
getItem
(
prefix
:
String
,
position
:
Int
):
SuggestionModel
{
return
list
[
position
]
}
...
...
app/src/main/java/chat/rocket/android/widget/autocompletion/strategy/trie/TrieCompletionStrategy.kt
View file @
8685b5dd
...
...
@@ -27,5 +27,9 @@ class TrieCompletionStrategy : CompletionStrategy {
}
}
override
fun
addPinned
(
list
:
List
<
SuggestionModel
>)
{
}
override
fun
size
()
=
items
.
size
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/widget/autocompletion/ui/SuggestionsAdapter.kt
View file @
8685b5dd
...
...
@@ -13,7 +13,7 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
threshold
:
Int
=
MAX_RESULT_COUNT
)
:
RecyclerView
.
Adapter
<
VH
>()
{
companion
object
{
// Any number of results.
const
val
UNLIMITED_RESULT_COUNT
=
-
1
const
val
RESULT_COUNT_UNLIMITED
=
-
1
// Trigger suggestions only if on the line start.
const
val
CONSTRAINT_BOUND_TO_START
=
0
// Trigger suggestions from anywhere.
...
...
@@ -21,12 +21,14 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
// Maximum number of results to display by default.
private
const
val
MAX_RESULT_COUNT
=
5
}
private
var
itemType
:
Type
?
=
null
private
var
itemClickListener
:
ItemClickListener
?
=
null
// Called to gather results when no results have previously matched.
private
var
providerExternal
:
((
query
:
String
)
->
Unit
)?
=
null
private
var
pinnedSuggestions
:
List
<
SuggestionModel
>?
=
null
// Maximum number of results/suggestions to display.
private
var
resultsThreshold
:
Int
=
if
(
threshold
>
0
)
threshold
else
UNLIMITED_RESULT_COUNT
private
var
resultsThreshold
:
Int
=
if
(
threshold
>
0
)
threshold
else
RESULT_COUNT_UNLIMITED
// The strategy used for suggesting completions.
private
val
strategy
:
CompletionStrategy
=
StringMatchingCompletionStrategy
(
resultsThreshold
)
// Current input term to look up for suggestions.
...
...
@@ -53,6 +55,15 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
return
strategy
.
autocompleteItems
(
currentTerm
)[
position
]
}
/**
* Set suggestions that should always appear when prompted.
*
* @param suggestions The list of suggestions that will be pinned.
*/
fun
setPinnedSuggestions
(
suggestions
:
List
<
SuggestionModel
>)
{
this
.
strategy
.
addPinned
(
suggestions
)
}
fun
autocomplete
(
newTerm
:
String
)
{
this
.
currentTerm
=
newTerm
.
toLowerCase
().
trim
()
}
...
...
app/src/main/java/chat/rocket/android/widget/autocompletion/ui/SuggestionsView.kt
View file @
8685b5dd
...
...
@@ -23,23 +23,19 @@ import chat.rocket.android.R
import
chat.rocket.android.widget.autocompletion.model.SuggestionModel
import
chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter.Companion.CONSTRAINT_BOUND_TO_START
import
java.lang.ref.WeakReference
import
java.util.concurrent.CopyOnWriteArrayList
import
java.util.concurrent.atomic.AtomicInteger
/**
* This is a special index that means we're not at an autocompleting state.
*/
// This is a special index that means we're not at an autocompleting state.
private
const
val
NO_STATE_INDEX
=
0
class
SuggestionsView
:
FrameLayout
,
TextWatcher
{
private
val
recyclerView
:
RecyclerView
private
val
registeredTokens
=
CopyOnWriteArrayList
<
String
>()
// Maps tokens to their respective adapters.
private
val
adaptersByToken
=
hashMapOf
<
String
,
SuggestionsAdapter
<
out
BaseSuggestionViewHolder
>>()
private
val
externalProvidersByToken
=
hashMapOf
<
String
,
((
query
:
String
)
->
Unit
)>()
private
val
localProvidersByToken
=
hashMapOf
<
String
,
HashMap
<
String
,
List
<
SuggestionModel
>>>()
private
var
editor
:
WeakReference
<
EditText
>?
=
null
private
var
completion
StartIndex
=
AtomicInteger
(
NO_STATE_INDEX
)
private
var
completion
Offset
=
AtomicInteger
(
NO_STATE_INDEX
)
private
var
maxHeight
:
Int
=
0
companion
object
{
...
...
@@ -66,7 +62,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
// If we have a deletion.
if
(
after
==
0
)
{
val
deleted
=
s
.
subSequence
(
start
,
start
+
count
).
toString
()
if
(
adaptersByToken
.
containsKey
(
deleted
)
&&
completion
StartIndex
.
get
()
>
NO_STATE_INDEX
)
{
if
(
adaptersByToken
.
containsKey
(
deleted
)
&&
completion
Offset
.
get
()
>
NO_STATE_INDEX
)
{
// We have removed the '@', '#' or any other action token so halt completion.
cancelSuggestions
(
true
)
}
...
...
@@ -77,6 +73,11 @@ class SuggestionsView : FrameLayout, TextWatcher {
// If we don't have any adapter bound to any token bail out.
if
(
adaptersByToken
.
isEmpty
())
return
if
(
editor
?.
get
()
!=
null
&&
editor
?.
get
()
?.
selectionStart
?:
0
<=
completionOffset
.
get
())
{
completionOffset
.
set
(
NO_STATE_INDEX
)
collapse
()
}
val
new
=
s
.
subSequence
(
start
,
start
+
count
).
toString
()
if
(
adaptersByToken
.
containsKey
(
new
))
{
val
constraint
=
adapter
(
new
).
constraint
...
...
@@ -84,8 +85,8 @@ class SuggestionsView : FrameLayout, TextWatcher {
return
}
swapAdapter
(
getAdapterForToken
(
new
)
!!
)
completion
StartIndex
.
compareAndSet
(
NO_STATE_INDEX
,
start
+
1
)
editor
?.
let
{
completion
Offset
.
compareAndSet
(
NO_STATE_INDEX
,
start
+
1
)
this
.
editor
?.
let
{
// Disable keyboard suggestions when autocompleting.
val
editText
=
it
.
get
()
if
(
editText
!=
null
)
{
...
...
@@ -97,13 +98,13 @@ class SuggestionsView : FrameLayout, TextWatcher {
if
(
new
.
startsWith
(
" "
))
{
// just halts the completion execution
cancelSuggestions
(
fals
e
)
cancelSuggestions
(
tru
e
)
return
}
val
prefixEndIndex
=
editor
?.
get
()
?.
selectionStart
?:
NO_STATE_INDEX
if
(
prefixEndIndex
==
NO_STATE_INDEX
||
prefixEndIndex
<
completion
StartIndex
.
get
())
return
val
prefix
=
s
.
subSequence
(
completion
StartIndex
.
get
(),
editor
?.
get
()
?.
selectionStart
?:
completionStartIndex
.
get
()).
toString
()
val
prefixEndIndex
=
this
.
editor
?.
get
()
?.
selectionStart
?:
NO_STATE_INDEX
if
(
prefixEndIndex
==
NO_STATE_INDEX
||
prefixEndIndex
<
completion
Offset
.
get
())
return
val
prefix
=
s
.
subSequence
(
completion
Offset
.
get
(),
this
.
editor
?.
get
()
?.
selectionStart
?:
completionOffset
.
get
()).
toString
()
recyclerView
.
adapter
?.
let
{
it
as
SuggestionsAdapter
// we need to look up only after the '@'
...
...
@@ -157,7 +158,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
val
adapter
=
adapter
(
token
)
localProvidersByToken
.
getOrPut
(
token
,
{
hashMapOf
()
})
.
put
(
adapter
.
term
(),
list
)
if
(
completion
StartIndex
.
get
()
>
NO_STATE_INDEX
&&
adapter
.
itemCount
==
0
)
expand
()
if
(
completion
Offset
.
get
()
>
NO_STATE_INDEX
&&
adapter
.
itemCount
==
0
)
expand
()
adapter
.
addItems
(
list
)
}
return
this
...
...
@@ -199,7 +200,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
// Reset completion start index only if we've deleted the token that triggered completion or
// we finished the completion process.
if
(
haltCompletion
)
{
completion
StartIndex
.
set
(
NO_STATE_INDEX
)
completion
Offset
.
set
(
NO_STATE_INDEX
)
}
collapse
()
// Re-enable keyboard suggestions.
...
...
@@ -212,7 +213,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
private
fun
insertSuggestionOnEditor
(
item
:
SuggestionModel
)
{
editor
?.
get
()
?.
let
{
val
suggestionText
=
item
.
text
it
.
text
.
replace
(
completion
StartIndex
.
get
(),
it
.
selectionStart
,
"$suggestionText "
)
it
.
text
.
replace
(
completion
Offset
.
get
(),
it
.
selectionStart
,
"$suggestionText "
)
}
}
...
...
app/src/main/java/chat/rocket/android/widget/emoji/EmojiParser.kt
View file @
8685b5dd
...
...
@@ -14,15 +14,21 @@ class EmojiParser {
*/
fun
parse
(
text
:
CharSequence
):
CharSequence
{
val
unicodedText
=
EmojiRepository
.
shortnameToUnicode
(
text
,
true
)
val
spannableString
=
SpannableString
.
valueOf
(
unicodedText
)
// Look for groups of emojis, set a CustomTypefaceSpan with the emojione font
val
length
=
spannableString
.
length
var
spannable
=
SpannableString
.
valueOf
(
unicodedText
)
val
typeface
=
EmojiRepository
.
cachedTypeface
// Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font.
val
length
=
spannable
.
length
var
inEmoji
=
false
var
emojiStart
=
0
var
offset
=
0
while
(
offset
<
length
)
{
val
codepoint
=
unicodedText
.
codePointAt
(
offset
)
val
count
=
Character
.
charCount
(
codepoint
)
// Skip control characters.
if
(
codepoint
==
0
x2028
)
{
offset
+=
count
continue
}
if
(
codepoint
>=
0
x200
)
{
if
(!
inEmoji
)
{
emojiStart
=
offset
...
...
@@ -30,18 +36,25 @@ class EmojiParser {
inEmoji
=
true
}
else
{
if
(
inEmoji
)
{
spannable
String
.
setSpan
(
EmojiTypefaceSpan
(
"sans-serif"
,
EmojiRepository
.
cachedT
ypeface
),
spannable
.
setSpan
(
EmojiTypefaceSpan
(
"sans-serif"
,
t
ypeface
),
emojiStart
,
offset
,
Spanned
.
SPAN_EXCLUSIVE_EXCLUSIVE
)
}
inEmoji
=
false
}
offset
+=
count
if
(
offset
>=
length
&&
inEmoji
)
{
spannable
String
.
setSpan
(
EmojiTypefaceSpan
(
"sans-serif"
,
EmojiRepository
.
cachedT
ypeface
),
spannable
.
setSpan
(
EmojiTypefaceSpan
(
"sans-serif"
,
t
ypeface
),
emojiStart
,
offset
,
Spanned
.
SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
return
spannableString
return
spannable
}
private
fun
calculateSurrogatePairs
(
scalar
:
Int
):
Pair
<
Int
,
Int
>
{
val
temp
:
Int
=
(
scalar
-
0
x10000
)
/
0
x400
val
s1
:
Int
=
Math
.
floor
(
temp
.
toDouble
()).
toInt
()
+
0
xD800
val
s2
:
Int
=
((
scalar
-
0
x10000
)
%
0
x400
)
+
0
xDC00
return
Pair
(
s1
,
s2
)
}
}
}
\ No newline at end of file
app/src/main/java/chat/rocket/android/widget/emoji/EmojiRepository.kt
View file @
8685b5dd
...
...
@@ -54,6 +54,10 @@ object EmojiRepository {
*/
fun
getAll
()
=
ALL_EMOJIS
// fun findEmojiByUnicode(unicode: Int) {
// ALL_EMOJIS.find { it.unicode == }
// }
/**
* Get all emojis for a given category.
*
...
...
@@ -119,10 +123,7 @@ object EmojiRepository {
var
result
:
String
=
input
.
toString
()
while
(
matcher
.
find
())
{
val
unicode
=
shortNameToUnicode
.
get
(
":${matcher.group(1)}:"
)
if
(
unicode
==
null
)
{
continue
}
val
unicode
=
shortNameToUnicode
.
get
(
":${matcher.group(1)}:"
)
?:
continue
if
(
supported
)
{
result
=
result
.
replace
(
":"
+
matcher
.
group
(
1
)
+
":"
,
unicode
)
...
...
@@ -159,9 +160,7 @@ object EmojiRepository {
private
fun
buildStringListFromJsonArray
(
array
:
JSONArray
):
List
<
String
>
{
val
list
=
ArrayList
<
String
>(
array
.
length
())
for
(
i
in
0
..
array
.
length
()
-
1
)
{
list
.
add
(
array
.
getString
(
i
))
}
(
0
until
array
.
length
()).
mapTo
(
list
)
{
array
.
getString
(
it
)
}
return
list
}
...
...
app/src/main/java/chat/rocket/android/widget/emoji/EmojiTypefaceSpan.kt
View file @
8685b5dd
...
...
@@ -17,22 +17,22 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac
private
fun
applyCustomTypeFace
(
paint
:
Paint
,
tf
:
Typeface
)
{
val
oldStyle
:
Int
val
old
=
paint
.
getTypeface
()
val
old
=
paint
.
typeface
if
(
old
==
null
)
{
oldStyle
=
0
}
else
{
oldStyle
=
old
.
getStyle
()
oldStyle
=
old
.
style
}
val
fake
=
oldStyle
and
tf
.
style
.
inv
()
if
(
fake
and
Typeface
.
BOLD
!=
0
)
{
paint
.
setFakeBoldText
(
true
)
paint
.
isFakeBoldText
=
true
}
if
(
fake
and
Typeface
.
ITALIC
!=
0
)
{
paint
.
setTextSkewX
(-
0.25f
)
paint
.
textSkewX
=
-
0.25f
}
paint
.
setTypeface
(
tf
)
paint
.
typeface
=
tf
}
}
\ No newline at end of file
app/src/main/res/drawable/quote.xml
→
app/src/main/res/drawable/quote
_vertical_bar
.xml
View file @
8685b5dd
...
...
@@ -2,9 +2,11 @@
<shape
xmlns:android=
"http://schemas.android.com/apk/res/android"
android:shape=
"rectangle"
>
<solid
android:color=
"@color/
darkGra
y"
/>
<solid
android:color=
"@color/
colorPrimar
y"
/>
<size
android:width=
"4dp"
android:height=
"4dp"
/>
<corners
android:radius=
"8dp"
/>
</shape>
\ No newline at end of file
app/src/main/res/layout/item_message_attachment.xml
0 → 100644
View file @
8685b5dd
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:app=
"http://schemas.android.com/apk/res-auto"
xmlns:tools=
"http://schemas.android.com/tools"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:background=
"?android:attr/selectableItemBackground"
android:clickable=
"true"
android:focusable=
"true"
android:paddingBottom=
"@dimen/message_item_top_and_bottom_padding"
android:paddingEnd=
"@dimen/screen_edge_left_and_right_padding"
android:paddingStart=
"@dimen/screen_edge_left_and_right_padding"
android:paddingTop=
"@dimen/message_item_top_and_bottom_padding"
>
<View
android:id=
"@+id/quote_bar"
android:layout_width=
"4dp"
android:layout_height=
"0dp"
android:layout_marginStart=
"56dp"
android:background=
"@drawable/quote_vertical_bar"
app:layout_constraintBottom_toTopOf=
"@+id/recycler_view_reactions"
app:layout_constraintStart_toStartOf=
"parent"
app:layout_constraintTop_toTopOf=
"parent"
/>
<LinearLayout
android:id=
"@+id/top_container"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:layout_marginStart=
"8dp"
android:orientation=
"horizontal"
app:layout_constraintLeft_toRightOf=
"@+id/quote_bar"
app:layout_constraintTop_toBottomOf=
"@id/new_messages_notif"
>
<TextView
android:id=
"@+id/text_sender"
style=
"@style/Sender.Name.TextView"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:textColor=
"@color/colorPrimary"
tools:text=
"Ronald Perkins"
/>
<TextView
android:id=
"@+id/text_message_time"
style=
"@style/Timestamp.TextView"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_marginStart=
"10dp"
tools:text=
"11:45 PM"
/>
</LinearLayout>
<TextView
android:id=
"@+id/text_content"
style=
"@style/Message.Quote.TextView"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:ellipsize=
"end"
android:singleLine=
"true"
app:layout_constraintEnd_toEndOf=
"parent"
app:layout_constraintStart_toStartOf=
"@+id/top_container"
app:layout_constraintTop_toBottomOf=
"@+id/top_container"
tools:text=
"This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!"
/>
<include
layout=
"@layout/layout_reactions"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
app:layout_constraintStart_toStartOf=
"@+id/quote_bar"
app:layout_constraintTop_toBottomOf=
"@+id/text_content"
/>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
app/src/main/res/layout/message_action_bar.xml
View file @
8685b5dd
...
...
@@ -6,6 +6,17 @@
android:layout_height=
"wrap_content"
android:background=
"@color/colorPrimary"
>
<View
android:id=
"@+id/quote_bar"
android:layout_width=
"4dp"
android:layout_height=
"0dp"
android:background=
"@drawable/quote_vertical_bar"
android:layout_marginTop=
"4dp"
android:layout_marginBottom=
"4dp"
app:layout_constraintBottom_toBottomOf=
"parent"
app:layout_constraintStart_toEndOf=
"@+id/image_view_action_cancel_quote"
app:layout_constraintTop_toTopOf=
"parent"
/>
<TextView
android:id=
"@+id/text_view_action_text"
android:layout_width=
"0dp"
...
...
app/src/main/res/layout/suggestion_member_item.xml
View file @
8685b5dd
...
...
@@ -6,9 +6,9 @@
android:layout_height=
"wrap_content"
android:layout_marginBottom=
"2dp"
android:layout_marginEnd=
"2dp"
android:layout_marginLeft=
"
4
dp"
android:layout_marginLeft=
"
8
dp"
android:layout_marginRight=
"2dp"
android:layout_marginStart=
"
4
dp"
android:layout_marginStart=
"
8
dp"
android:layout_marginTop=
"2dp"
android:background=
"@color/suggestion_background_color"
>
...
...
@@ -22,7 +22,8 @@
android:id=
"@+id/image_avatar"
android:layout_width=
"24dp"
android:layout_height=
"24dp"
android:layout_margin=
"4dp"
android:layout_marginTop=
"4dp"
android:layout_marginBottom=
"4dp"
app:roundedCornerRadius=
"3dp"
tools:src=
"@tools:sample/avatars"
/>
...
...
app/src/main/res/values-pt-rBR/strings.xml
View file @
8685b5dd
...
...
@@ -70,6 +70,10 @@
<string
name=
"msg_new_password"
>
Informe a nova senha
</string>
<string
name=
"msg_confirm_password"
>
Confirme a nova senha
</string>
<string
name=
"msg_unread_messages"
>
Mensagens não lidas
</string>
<string
name=
"msg_preview_video"
>
Vídeo
</string>
<string
name=
"msg_preview_audio"
>
Audio
</string>
<string
name=
"msg_preview_photo"
>
Foto
</string>
<string
name=
"msg_no_messages_yet"
>
Nenhuma mensagem ainda
</string>
<!-- System messages -->
<string
name=
"message_room_name_changed"
>
Nome da sala alterado para: %1$s por %2$s
</string>
...
...
@@ -116,6 +120,10 @@
<string
name=
"status_disconnecting"
>
desconectando
</string>
<string
name=
"status_waiting"
>
conectando em %d segundos
</string>
<!--Suggestions-->
<string
name=
"suggest_all_description"
>
Notifica todos nesta sala
</string>
<string
name=
"suggest_here_description"
>
Notifica usuários ativos nesta sala
</string>
<!-- Slash Commands -->
<string
name=
"Slash_Gimme_Description"
>
Exibir ༼ つ ◕_◕ ༽つ antes de sua mensagem
</string>
<string
name=
"Slash_LennyFace_Description"
>
Exibir ( ͡° ͜ʖ ͡°) depois de sua mensagem
</string>
...
...
app/src/main/res/values/strings.xml
View file @
8685b5dd
...
...
@@ -71,6 +71,10 @@
<string
name=
"msg_new_password"
>
Enter New Password
</string>
<string
name=
"msg_confirm_password"
>
Confirm New Password
</string>
<string
name=
"msg_unread_messages"
>
Unread messages
</string>
<string
name=
"msg_preview_video"
>
Video
</string>
<string
name=
"msg_preview_audio"
>
Audio
</string>
<string
name=
"msg_preview_photo"
>
Photo
</string>
<string
name=
"msg_no_messages_yet"
>
No messages yet
</string>
<!-- System messages -->
<string
name=
"message_room_name_changed"
>
Room name changed to: %1$s by %2$s
</string>
...
...
@@ -117,6 +121,10 @@
<string
name=
"status_disconnecting"
>
disconnecting
</string>
<string
name=
"status_waiting"
>
connecting in %d seconds
</string>
<!--Suggestions-->
<string
name=
"suggest_all_description"
>
Notify all in this room
</string>
<string
name=
"suggest_here_description"
>
Notify active users in this room
</string>
<!-- Slash Commands -->
<string
name=
"Slash_Gimme_Description"
>
Displays ༼ つ ◕_◕ ༽つ before your message
</string>
<string
name=
"Slash_LennyFace_Description"
>
Displays ( ͡° ͜ʖ ͡°) after your message
</string>
...
...
app/src/main/res/values/styles.xml
View file @
8685b5dd
...
...
@@ -88,6 +88,10 @@
<item
name=
"android:textColor"
>
@color/colorPrimaryText
</item>
</style>
<style
name=
"Message.Quote.TextView"
parent=
"Message.TextView"
>
<item
name=
"android:textColor"
>
@color/colorPrimaryText
</item>
</style>
<style
name=
"Timestamp.TextView"
parent=
"TextAppearance.AppCompat.Caption"
>
<item
name=
"android:textSize"
>
10sp
</item>
</style>
...
...
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