Merge pull request #1895 from nextcloud/feature/1772/reactions

Reactions to chat messages
This commit is contained in:
Marcel Hibbe 2022-04-09 23:21:33 +02:00 committed by GitHub
commit d66a6a9578
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1844 additions and 285 deletions

View File

@ -56,16 +56,7 @@
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" static="false" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
@ -212,8 +203,7 @@
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>

View File

@ -141,6 +141,15 @@ If you set your `user.name` and `user.email` git configs, you can sign your comm
You can also use git [aliases](https://git-scm.com/book/tr/v2/Git-Basics-Git-Aliases) like `git config --global alias.ci 'commit -s'`.
Now you can commit with `git ci` and the commit will be signed.
### Git hooks
We provide git hooks to make development process easier for both the developer and the reviewers.
To install them, just run:
```bash
./gradlew installGitHooks
```
## Contribution process
Contribute your code targeting/based-on the branch ```master```.

View File

@ -334,6 +334,14 @@ dependencies {
gplayImplementation "com.google.firebase:firebase-messaging:23.0.0"
}
task installGitHooks(type: Copy, group: "development") {
description = "Install git hooks"
from("../scripts/hooks") {
include '*'
}
into '../.git/hooks'
}
detekt {
reports {
xml {

View File

@ -0,0 +1,28 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters
import com.nextcloud.talk.models.json.reactions.ReactionVoter
data class ReactionItem(
val reactionVoter: ReactionVoter,
val reaction: String?
)

View File

@ -0,0 +1,25 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters
interface ReactionItemClickListener {
fun onClick(reactionItem: ReactionItem)
}

View File

@ -0,0 +1,47 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.talk.databinding.ReactionItemBinding
import com.nextcloud.talk.models.database.UserEntity
class ReactionsAdapter(
private val clickListener: ReactionItemClickListener,
private val userEntity: UserEntity?
) : RecyclerView.Adapter<ReactionsViewHolder>() {
internal var list: MutableList<ReactionItem> = ArrayList<ReactionItem>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReactionsViewHolder {
val itemBinding = ReactionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ReactionsViewHolder(itemBinding, userEntity?.baseUrl)
}
override fun onBindViewHolder(holder: ReactionsViewHolder, position: Int) {
holder.bind(list[position], clickListener)
}
override fun getItemCount(): Int {
return list.size
}
}

View File

@ -0,0 +1,88 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.adapters
import android.text.TextUtils
import androidx.recyclerview.widget.RecyclerView
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.interfaces.DraweeController
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ReactionItemBinding
import com.nextcloud.talk.models.json.reactions.ReactionVoter
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
class ReactionsViewHolder(
private val binding: ReactionItemBinding,
private val baseUrl: String?
) : RecyclerView.ViewHolder(binding.root) {
fun bind(reactionItem: ReactionItem, clickListener: ReactionItemClickListener) {
binding.root.setOnClickListener { clickListener.onClick(reactionItem) }
binding.reaction.text = reactionItem.reaction
binding.name.text = reactionItem.reactionVoter.actorDisplayName
if (baseUrl != null && baseUrl.isNotEmpty()) {
loadAvatar(reactionItem)
}
}
private fun loadAvatar(reactionItem: ReactionItem) {
if (reactionItem.reactionVoter.actorType == ReactionVoter.ReactionActorType.GUESTS) {
var displayName = sharedApplication?.resources?.getString(R.string.nc_guest)
if (!TextUtils.isEmpty(reactionItem.reactionVoter.actorDisplayName)) {
displayName = reactionItem.reactionVoter.actorDisplayName!!
}
val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
.setOldController(binding.avatar.controller)
.setAutoPlayAnimations(true)
.setImageRequest(
DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForGuestAvatar(
baseUrl,
displayName,
false
),
null
)
)
.build()
binding.avatar.controller = draweeController
} else if (reactionItem.reactionVoter.actorType == ReactionVoter.ReactionActorType.USERS) {
val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
.setOldController(binding.avatar.controller)
.setAutoPlayAnimations(true)
.setImageRequest(
DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatar(
baseUrl,
reactionItem.reactionVoter.actorId,
false
),
null
)
)
.build()
binding.avatar.controller = draweeController
}
}
}

View File

@ -78,6 +78,8 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
@Inject
var appPreferences: AppPreferences? = null
lateinit var reactionsInterface: ReactionsInterface
@SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) {
super.onBind(message)
@ -93,13 +95,21 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
val textSize = context?.resources!!.getDimension(R.dimen.chat_text_size)
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
binding.messageText.text = message.text
binding.messageText.isEnabled = false
// parent message handling
setParentMessageDataOnMessageItem(message)
// geo-location
setLocationDataOnMessageItem(message)
Reaction().showReactions(message, binding.reactions, context!!, true)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
reactionsInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
reactionsInterface.onLongClickReactions(message)
true
}
}
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
@ -270,6 +280,10 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
return locationGeoLink.replace("geo:", "geo:0,0?q=")
}
fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
this.reactionsInterface = reactionsInterface
}
companion object {
private const val TAG = "LocInMessageView"
}

View File

@ -27,6 +27,7 @@ import android.widget.ProgressBar;
import com.facebook.drawee.view.SimpleDraweeView;
import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
import androidx.emoji.widget.EmojiTextView;
@ -77,4 +78,7 @@ public class IncomingPreviewMessageViewHolder extends MagicPreviewMessageViewHol
public ProgressBar getPreviewContactProgressBar() {
return binding.contactProgressBar;
}
@Override
public ReactionsInsideMessageBinding getReactionsBinding(){ return binding.reactions; }
}

View File

@ -74,6 +74,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
lateinit var message: ChatMessage
lateinit var voiceMessageInterface: VoiceMessageInterface
lateinit var reactionsInterface: ReactionsInterface
@SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) {
@ -140,6 +141,15 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
}
}
})
Reaction().showReactions(message, binding.reactions, context!!, true)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
reactionsInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
reactionsInterface.onLongClickReactions(message)
true
}
}
private fun updateDownloadState(message: ChatMessage) {
@ -302,10 +312,14 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
}
}
fun assignAdapter(voiceMessageInterface: VoiceMessageInterface) {
fun assignVoiceMessageInterface(voiceMessageInterface: VoiceMessageInterface) {
this.voiceMessageInterface = voiceMessageInterface
}
fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
this.reactionsInterface = reactionsInterface
}
companion object {
private const val TAG = "VoiceInMessageView"
private const val SEEKBAR_START: Int = 0

View File

@ -71,6 +71,8 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
@Inject
var appPreferences: AppPreferences? = null
lateinit var reactionsInterface: ReactionsInterface
override fun onBind(message: ChatMessage) {
super.onBind(message)
sharedApplication!!.componentApplication.inject(this)
@ -119,6 +121,15 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
}
itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)
Reaction().showReactions(message, binding.reactions, context!!, true)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
reactionsInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
reactionsInterface.onLongClickReactions(message)
true
}
}
private fun processAuthor(message: ChatMessage) {
@ -260,6 +271,10 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
return messageStringInternal
}
fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
this.reactionsInterface = reactionsInterface
}
companion object {
const val TEXT_SIZE_MULTIPLIER = 2.5
}

View File

@ -61,6 +61,8 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
@Inject
var context: Context? = null
lateinit var reactionsInterface: ReactionsInterface
override fun onBind(message: ChatMessage) {
super.onBind(message)
sharedApplication!!.componentApplication.inject(this)
@ -118,6 +120,15 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
binding.checkMark.setContentDescription(readStatusContentDescriptionString)
itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)
Reaction().showReactions(message, binding.reactions, context!!, true)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
reactionsInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
reactionsInterface.onLongClickReactions(message)
true
}
}
private fun processParentMessage(message: ChatMessage) {
@ -204,6 +215,10 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
return messageString1
}
fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
this.reactionsInterface = reactionsInterface
}
companion object {
const val TEXT_SIZE_MULTIPLIER = 2.5
}

View File

@ -52,6 +52,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.components.filebrowser.models.BrowserFile;
import com.nextcloud.talk.components.filebrowser.models.DavResponse;
import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker;
import com.nextcloud.talk.models.database.CapabilitiesUtil;
import com.nextcloud.talk.models.database.UserEntity;
@ -111,8 +112,13 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
ProgressBar progressBar;
ReactionsInsideMessageBinding reactionsBinding;
View clickView;
ReactionsInterface reactionsInterface;
PreviewMessageInterface previewMessageInterface;
public MagicPreviewMessageViewHolder(View itemView, Object payload) {
super(itemView, payload);
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
@ -185,25 +191,30 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
fetchFileInformation("/" + message.getSelectedIndividualHashMap().get(KEY_PATH), message.activeUser);
}
String accountString =
if(message.activeUser != null && message.activeUser.getUsername() != null && message.activeUser.getBaseUrl() != null){
String accountString =
message.activeUser.getUsername() + "@" +
message.activeUser.getBaseUrl()
.replace("https://", "")
.replace("http://", "");
message.activeUser.getBaseUrl()
.replace("https://", "")
.replace("http://", "");
clickView.setOnClickListener(v -> {
String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
if (isSupportedForInternalViewer(mimetype) || canBeHandledByExternalApp(mimetype, fileName)) {
openOrDownloadFile(message);
} else {
openFileInFilesApp(message, accountString);
}
});
clickView.setOnClickListener(v -> {
String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
if (isSupportedForInternalViewer(mimetype) || canBeHandledByExternalApp(mimetype, fileName)) {
openOrDownloadFile(message);
} else {
openFileInFilesApp(message, accountString);
}
});
clickView.setOnLongClickListener(l -> {
onMessageViewLongClick(message, accountString);
return true;
});
} else {
Log.e(TAG, "failed to set click listener because activeUser, username or baseUrl were null");
}
clickView.setOnLongClickListener(l -> {
onMessageViewLongClick(message, accountString);
return true;
});
// check if download worker is already running
String fileId = message.getSelectedIndividualHashMap().get(KEY_ID);
@ -246,8 +257,17 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
}
itemView.setTag(REPLYABLE_VIEW_TAG, message.isReplyable());
}
reactionsBinding = getReactionsBinding();
new Reaction().showReactions(message, reactionsBinding, context, false);
reactionsBinding.reactionsEmojiWrapper.setOnClickListener(l -> {
reactionsInterface.onClickReactions(message);
});
reactionsBinding.reactionsEmojiWrapper.setOnLongClickListener(l -> {
reactionsInterface.onLongClickReactions(message);
return true;
});
}
private Drawable getDrawableFromContactDetails(Context context, String base64) {
Drawable drawable = null;
@ -283,6 +303,8 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
public abstract ProgressBar getPreviewContactProgressBar();
public abstract ReactionsInsideMessageBinding getReactionsBinding();
private void openOrDownloadFile(ChatMessage message) {
String filename = message.getSelectedIndividualHashMap().get(KEY_NAME);
String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
@ -410,6 +432,7 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
private void onMessageViewLongClick(ChatMessage message, String accountString) {
if (isSupportedForInternalViewer(message.getSelectedIndividualHashMap().get(KEY_MIMETYPE))) {
previewMessageInterface.onPreviewMessageLongClick(message);
return;
}
@ -591,4 +614,12 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
}
});
}
public void assignReactionInterface(ReactionsInterface reactionsInterface) {
this.reactionsInterface = reactionsInterface;
}
public void assignPreviewMessageInterface(PreviewMessageInterface previewMessageInterface) {
this.previewMessageInterface = previewMessageInterface;
}
}

View File

@ -68,6 +68,8 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
@Inject
var context: Context? = null
lateinit var reactionsInterface: ReactionsInterface
@SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) {
super.onBind(message)
@ -84,7 +86,6 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
binding.messageTime.layoutParams = layoutParams
binding.messageText.text = message.text
binding.messageText.isEnabled = false
// parent message handling
setParentMessageDataOnMessageItem(message)
@ -112,6 +113,15 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
// geo-location
setLocationDataOnMessageItem(message)
Reaction().showReactions(message, binding.reactions, context!!, true)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
reactionsInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
reactionsInterface.onLongClickReactions(message)
true
}
}
@SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
@ -245,6 +255,10 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
return locationGeoLink.replace("geo:", "geo:0,0?q=")
}
fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
this.reactionsInterface = reactionsInterface
}
companion object {
private const val TAG = "LocOutMessageView"
}

View File

@ -27,6 +27,7 @@ import android.widget.ProgressBar;
import com.facebook.drawee.view.SimpleDraweeView;
import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
import androidx.emoji.widget.EmojiTextView;
@ -77,4 +78,7 @@ public class OutcomingPreviewMessageViewHolder extends MagicPreviewMessageViewHo
public ProgressBar getPreviewContactProgressBar() {
return binding.contactProgressBar;
}
@Override
public ReactionsInsideMessageBinding getReactionsBinding() { return binding.reactions; }
}

View File

@ -69,6 +69,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
lateinit var handler: Handler
lateinit var voiceMessageInterface: VoiceMessageInterface
lateinit var reactionsInterface: ReactionsInterface
@SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) {
@ -129,6 +130,15 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
}
binding.checkMark.setContentDescription(readStatusContentDescriptionString)
Reaction().showReactions(message, binding.reactions, context!!, true)
binding.reactions.reactionsEmojiWrapper.setOnClickListener {
reactionsInterface.onClickReactions(message)
}
binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
reactionsInterface.onLongClickReactions(message)
true
}
}
private fun handleResetVoiceMessageState(message: ChatMessage) {
@ -275,10 +285,14 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
}
}
fun assignAdapter(voiceMessageInterface: VoiceMessageInterface) {
fun assignVoiceMessageInterface(voiceMessageInterface: VoiceMessageInterface) {
this.voiceMessageInterface = voiceMessageInterface
}
fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
this.reactionsInterface = reactionsInterface
}
companion object {
private const val TAG = "VoiceOutMessageView"
private const val SEEKBAR_START: Int = 0

View File

@ -0,0 +1,7 @@
package com.nextcloud.talk.adapters.messages
import com.nextcloud.talk.models.json.chat.ChatMessage
interface PreviewMessageInterface {
fun onPreviewMessageLongClick(chatMessage: ChatMessage)
}

View File

@ -0,0 +1,93 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Parts related to account import were either copied from or inspired by the great work done by David Luhmer at:
* https://github.com/nextcloud/ownCloud-Account-Importer
*/
package com.nextcloud.talk.adapters.messages
import android.content.Context
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.nextcloud.talk.R
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.utils.DisplayUtils
import com.vanniktech.emoji.EmojiTextView
class Reaction {
fun showReactions(
message: ChatMessage,
binding: ReactionsInsideMessageBinding,
context: Context,
useLightColorForText: Boolean
) {
binding.reactionsEmojiWrapper.removeAllViews()
if (message.reactions != null && message.reactions.isNotEmpty()) {
var remainingEmojisToDisplay = MAX_EMOJIS_TO_DISPLAY
val showInfoAboutMoreEmojis = message.reactions.size > MAX_EMOJIS_TO_DISPLAY
for ((emoji, amount) in message.reactions) {
val reactionEmoji = EmojiTextView(context)
reactionEmoji.text = emoji
binding.reactionsEmojiWrapper.addView(reactionEmoji)
val reactionAmount = TextView(context)
if (amount > 1) {
if (useLightColorForText) {
reactionAmount.setTextColor(ContextCompat.getColor(context, R.color.nc_grey))
}
reactionAmount.text = amount.toString()
}
val params = RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
params.setMargins(
DisplayUtils.convertDpToPixel(EMOJI_START_MARGIN, context).toInt(),
0,
DisplayUtils.convertDpToPixel(EMOJI_END_MARGIN, context).toInt(),
0
)
reactionAmount.layoutParams = params
binding.reactionsEmojiWrapper.addView(reactionAmount)
remainingEmojisToDisplay--
if (remainingEmojisToDisplay == 0 && showInfoAboutMoreEmojis) {
val infoAboutMoreEmojis = TextView(context)
infoAboutMoreEmojis.text = EMOJI_MORE
binding.reactionsEmojiWrapper.addView(infoAboutMoreEmojis)
break
}
}
}
}
companion object {
const val MAX_EMOJIS_TO_DISPLAY = 4
const val EMOJI_START_MARGIN: Float = 2F
const val EMOJI_END_MARGIN: Float = 8F
const val EMOJI_MORE = ""
}
}

View File

@ -0,0 +1,8 @@
package com.nextcloud.talk.adapters.messages
import com.nextcloud.talk.models.json.chat.ChatMessage
interface ReactionsInterface {
fun onClickReactions(chatMessage: ChatMessage)
fun onLongClickReactions(chatMessage: ChatMessage)
}

View File

@ -49,10 +49,26 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
public void onBindViewHolder(ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
if (holder instanceof IncomingVoiceMessageViewHolder) {
((IncomingVoiceMessageViewHolder) holder).assignAdapter(chatController);
if (holder instanceof MagicIncomingTextMessageViewHolder) {
((MagicIncomingTextMessageViewHolder) holder).assignReactionInterface(chatController);
} else if (holder instanceof MagicOutcomingTextMessageViewHolder) {
((MagicOutcomingTextMessageViewHolder) holder).assignReactionInterface(chatController);
} else if (holder instanceof IncomingLocationMessageViewHolder) {
((IncomingLocationMessageViewHolder) holder).assignReactionInterface(chatController);
} else if (holder instanceof OutcomingLocationMessageViewHolder) {
((OutcomingLocationMessageViewHolder) holder).assignReactionInterface(chatController);
} else if (holder instanceof IncomingVoiceMessageViewHolder) {
((IncomingVoiceMessageViewHolder) holder).assignVoiceMessageInterface(chatController);
((IncomingVoiceMessageViewHolder) holder).assignReactionInterface(chatController);
} else if (holder instanceof OutcomingVoiceMessageViewHolder) {
((OutcomingVoiceMessageViewHolder) holder).assignAdapter(chatController);
((OutcomingVoiceMessageViewHolder) holder).assignVoiceMessageInterface(chatController);
((OutcomingVoiceMessageViewHolder) holder).assignReactionInterface(chatController);
} else if (holder instanceof MagicPreviewMessageViewHolder) {
((MagicPreviewMessageViewHolder) holder).assignPreviewMessageInterface(chatController);
((MagicPreviewMessageViewHolder) holder).assignReactionInterface(chatController);
}
}
}

View File

@ -37,6 +37,7 @@ import com.nextcloud.talk.models.json.notifications.NotificationOverall;
import com.nextcloud.talk.models.json.participants.AddParticipantOverall;
import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
import com.nextcloud.talk.models.json.push.PushRegistrationOverall;
import com.nextcloud.talk.models.json.reactions.ReactionsOverall;
import com.nextcloud.talk.models.json.search.ContactsByNumberOverall;
import com.nextcloud.talk.models.json.signaling.SignalingOverall;
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
@ -62,6 +63,7 @@ import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.PUT;
@ -488,4 +490,17 @@ public interface NcApi {
@GET
Observable<StatusesOverall> getUserStatuses(@Header("Authorization") String authorization, @Url String url);
@POST
Observable<GenericOverall> sendReaction(@Header("Authorization") String authorization, @Url String url,
@Query("reaction") String reaction);
@DELETE
Observable<GenericOverall> deleteReaction(@Header("Authorization") String authorization, @Url String url,
@Query("reaction") String reaction);
@GET
Observable<ReactionsOverall> getReactions(@Header("Authorization") String authorization,
@Url String url,
@Query("reaction") String reaction);
}

View File

@ -110,6 +110,8 @@ import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
import com.nextcloud.talk.adapters.messages.PreviewMessageInterface
import com.nextcloud.talk.adapters.messages.ReactionsInterface
import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
import com.nextcloud.talk.api.NcApi
@ -139,6 +141,7 @@ import com.nextcloud.talk.presenters.MentionAutocompletePresenter
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
import com.nextcloud.talk.ui.dialog.AttachmentDialog
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
import com.nextcloud.talk.utils.ApiUtils
@ -203,7 +206,9 @@ class ChatController(args: Bundle) :
MessagesListAdapter.Formatter<Date>,
MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
ContentChecker<ChatMessage>,
VoiceMessageInterface {
VoiceMessageInterface,
ReactionsInterface,
PreviewMessageInterface {
private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
@ -2087,7 +2092,7 @@ class ChatController(args: Bundle) :
if (response.code() == HTTP_CODE_OK) {
val chatOverall = response.body() as ChatOverall?
val chatMessageList = setDeletionFlagsAndRemoveInfomessages(chatOverall?.ocs!!.data)
val chatMessageList = handleSystemMessages(chatOverall?.ocs!!.data)
if (chatMessageList.isNotEmpty() &&
ChatMessage.SystemMessageType.CLEARED_CHAT == chatMessageList[0].systemMessageType
@ -2336,14 +2341,16 @@ class ChatController(args: Bundle) :
}
}
private fun setDeletionFlagsAndRemoveInfomessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
private fun handleSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
val chatMessageIterator = chatMessageMap.iterator()
while (chatMessageIterator.hasNext()) {
val currentMessage = chatMessageIterator.next()
// setDeletionFlagsAndRemoveInfomessages
if (isInfoMessageAboutDeletion(currentMessage)) {
if (!chatMessageMap.containsKey(currentMessage.value.parentMessage.id)) {
// if chatMessageMap doesnt't contain message to delete (this happens when lookingIntoFuture),
// if chatMessageMap doesn't contain message to delete (this happens when lookingIntoFuture),
// the message to delete has to be modified directly inside the adapter
setMessageAsDeleted(currentMessage.value.parentMessage)
} else {
@ -2351,6 +2358,15 @@ class ChatController(args: Bundle) :
}
chatMessageIterator.remove()
}
// delete reactions system messages
else if (isReactionsMessage(currentMessage)) {
if (!chatMessageMap.containsKey(currentMessage.value.parentMessage.id)) {
updateAdapterForReaction(currentMessage.value.parentMessage)
}
chatMessageIterator.remove()
}
}
return chatMessageMap.values.toList()
}
@ -2360,6 +2376,12 @@ class ChatController(args: Bundle) :
.SystemMessageType.MESSAGE_DELETED
}
private fun isReactionsMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION ||
currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED ||
currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED
}
private fun startACall(isVoiceOnlyCall: Boolean) {
if (currentConversation?.canStartCall == false && currentConversation?.hasCall == false) {
Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show()
@ -2398,21 +2420,50 @@ class ChatController(args: Bundle) :
}
}
override fun onClickReactions(chatMessage: ChatMessage) {
activity?.let {
ShowReactionsDialog(
activity!!,
currentConversation,
chatMessage,
conversationUser,
ncApi!!
).show()
}
}
override fun onLongClickReactions(chatMessage: ChatMessage) {
openMessageActionsDialog(chatMessage)
}
override fun onMessageViewLongClick(view: View?, message: IMessage?) {
if (hasVisibleItems(message as ChatMessage)) {
openMessageActionsDialog(message)
}
override fun onPreviewMessageLongClick(chatMessage: ChatMessage) {
openMessageActionsDialog(chatMessage)
}
private fun openMessageActionsDialog(iMessage: IMessage?) {
val message = iMessage as ChatMessage
if (hasVisibleItems(message) && !isSystemMessage(message)) {
activity?.let {
MessageActionsDialog(
activity!!,
this,
message,
conversationUser?.userId,
conversationUser,
currentConversation,
isShowMessageDeletionButton(message)
isShowMessageDeletionButton(message),
ncApi!!
).show()
}
}
}
private fun isSystemMessage(message: ChatMessage): Boolean {
return ChatMessage.MessageType.SYSTEM_MESSAGE == message.getMessageType()
}
fun deleteMessage(message: IMessage?) {
var apiVersion = 1
// FIXME Fix API checking with guests?
@ -2680,6 +2731,29 @@ class ChatController(args: Bundle) :
adapter?.update(messageTemp)
}
private fun updateAdapterForReaction(message: IMessage?) {
val messageTemp = message as ChatMessage
messageTemp.isOneToOneConversation =
currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
messageTemp.activeUser = conversationUser
adapter?.update(messageTemp)
}
fun updateAdapterAfterSendReaction(message: ChatMessage, emoji: String) {
if (message.reactions == null) {
message.reactions = LinkedHashMap()
}
var amount = message.reactions[emoji]
if (amount == null) {
amount = 0
}
message.reactions[emoji] = amount + 1
adapter?.update(message)
}
private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
if (conversationUser == null) return false

View File

@ -277,6 +277,7 @@ class WebViewLoginController(args: Bundle? = null) : NewBaseController(
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
try {
val sslCertificate = error.certificate

View File

@ -22,6 +22,7 @@
package com.nextcloud.talk.models.json.chat;
import android.text.TextUtils;
import android.util.Log;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonIgnore;
@ -40,6 +41,7 @@ import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -50,6 +52,8 @@ import kotlin.text.Charsets;
@Parcel
@JsonObject
public class ChatMessage implements MessageContentType, MessageContentType.Image {
private static String TAG = "ChatMessage";
@JsonIgnore
public boolean isGrouped;
@JsonIgnore
@ -90,6 +94,8 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
public Enum<ReadStatus> readStatus = ReadStatus.NONE;
@JsonField(name = "messageType")
public String messageType;
@JsonField(name = "reactions")
public LinkedHashMap<String, Integer> reactions;
public boolean isDownloadingVoiceMessage;
public boolean resetVoiceMessage;
@ -100,21 +106,21 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
@JsonIgnore
List<MessageType> messageTypesToIgnore = Arrays.asList(
MessageType.REGULAR_TEXT_MESSAGE,
MessageType.SYSTEM_MESSAGE,
MessageType.SINGLE_LINK_VIDEO_MESSAGE,
MessageType.SINGLE_LINK_AUDIO_MESSAGE,
MessageType.SINGLE_LINK_MESSAGE,
MessageType.SINGLE_NC_GEOLOCATION_MESSAGE,
MessageType.VOICE_MESSAGE);
MessageType.REGULAR_TEXT_MESSAGE,
MessageType.SYSTEM_MESSAGE,
MessageType.SINGLE_LINK_VIDEO_MESSAGE,
MessageType.SINGLE_LINK_AUDIO_MESSAGE,
MessageType.SINGLE_LINK_MESSAGE,
MessageType.SINGLE_NC_GEOLOCATION_MESSAGE,
MessageType.VOICE_MESSAGE);
public boolean hasFileAttachment() {
if (messageParameters != null && messageParameters.size() > 0) {
for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
Map<String, String> individualHashMap = entry.getValue();
if(MessageDigest.isEqual(
Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
("file").getBytes(Charsets.UTF_8))) {
if (MessageDigest.isEqual(
Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
("file").getBytes(Charsets.UTF_8))) {
return true;
}
}
@ -127,9 +133,9 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
Map<String, String> individualHashMap = entry.getValue();
if(MessageDigest.isEqual(
Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
("geo-location").getBytes(Charsets.UTF_8))) {
if (MessageDigest.isEqual(
Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
("geo-location").getBytes(Charsets.UTF_8))) {
return true;
}
}
@ -144,13 +150,20 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
if (messageParameters != null && messageParameters.size() > 0) {
for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
Map<String, String> individualHashMap = entry.getValue();
if(MessageDigest.isEqual(
Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
("file").getBytes(Charsets.UTF_8))) {
if (MessageDigest.isEqual(
Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
("file").getBytes(Charsets.UTF_8))) {
selectedIndividualHashMap = individualHashMap;
if(!isVoiceMessage()){
return (ApiUtils.getUrlForFilePreviewWithFileId(getActiveUser().getBaseUrl(),
individualHashMap.get("id"), NextcloudTalkApplication.Companion.getSharedApplication().getResources().getDimensionPixelSize(R.dimen.maximum_file_preview_size)));
if (!isVoiceMessage()) {
if (getActiveUser() != null && getActiveUser().getBaseUrl() != null) {
return (ApiUtils.getUrlForFilePreviewWithFileId(
getActiveUser().getBaseUrl(),
individualHashMap.get("id"),
NextcloudTalkApplication.Companion.getSharedApplication().getResources().getDimensionPixelSize(R.dimen.maximum_file_preview_size)));
} else {
Log.e(TAG, "getActiveUser() or getActiveUser().getBaseUrl() were null when trying to " +
"getImageUrl()");
}
}
}
}
@ -168,7 +181,7 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
return MessageType.SYSTEM_MESSAGE;
}
if (isVoiceMessage()){
if (isVoiceMessage()) {
return MessageType.VOICE_MESSAGE;
}
@ -207,20 +220,20 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
return getText();
} else {
if (MessageType.SINGLE_LINK_GIPHY_MESSAGE == getMessageType()
|| MessageType.SINGLE_LINK_TENOR_MESSAGE == getMessageType()
|| MessageType.SINGLE_LINK_GIF_MESSAGE == getMessageType()) {
|| MessageType.SINGLE_LINK_TENOR_MESSAGE == getMessageType()
|| MessageType.SINGLE_LINK_GIF_MESSAGE == getMessageType()) {
if (getActorId().equals(getActiveUser().getUserId())) {
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_gif_you));
} else {
return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_gif),
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
}
} else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == getMessageType()) {
if (getActorId().equals(getActiveUser().getUserId())) {
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_attachment_you));
} else {
return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_attachment),
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
}
} else if (MessageType.SINGLE_NC_GEOLOCATION_MESSAGE == getMessageType()) {
if (getActorId().equals(getActiveUser().getUserId())) {
@ -248,21 +261,21 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_audio_you));
} else {
return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_audio),
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
}
} else if (MessageType.SINGLE_LINK_VIDEO_MESSAGE == getMessageType()) {
if (getActorId().equals(getActiveUser().getUserId())) {
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_video_you));
} else {
return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_video),
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
}
} else if (MessageType.SINGLE_LINK_IMAGE_MESSAGE == getMessageType()) {
if (getActorId().equals(getActiveUser().getUserId())) {
return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_image_you));
} else {
return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_image),
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
!TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
}
}
}
@ -289,14 +302,16 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
@Override
public String getAvatar() {
if (getActorType().equals("users")) {
if (getActiveUser() == null) {
return null;
} else if (getActorType().equals("users")) {
return ApiUtils.getUrlForAvatar(getActiveUser().getBaseUrl(), actorId, true);
} else if (getActorType().equals("bridged")) {
return ApiUtils.getUrlForAvatar(getActiveUser().getBaseUrl(), "bridge-bot",
true);
} else {
String apiId =
NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest);
NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest);
if (!TextUtils.isEmpty(getActorDisplayName())) {
apiId = getActorDisplayName();
@ -592,7 +607,7 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
return "ChatMessage(isGrouped=" + this.isGrouped() + ", isOneToOneConversation=" + this.isOneToOneConversation() + ", activeUser=" + this.getActiveUser() + ", selectedIndividualHashMap=" + this.getSelectedIndividualHashMap() + ", isDeleted=" + this.isDeleted() + ", jsonMessageId=" + this.getJsonMessageId() + ", token=" + this.getToken() + ", actorType=" + this.getActorType() + ", actorId=" + this.getActorId() + ", actorDisplayName=" + this.getActorDisplayName() + ", timestamp=" + this.getTimestamp() + ", message=" + this.getMessage() + ", messageParameters=" + this.getMessageParameters() + ", systemMessageType=" + this.getSystemMessageType() + ", replyable=" + this.isReplyable() + ", parentMessage=" + this.getParentMessage() + ", readStatus=" + this.getReadStatus() + ", messageTypesToIgnore=" + this.getMessageTypesToIgnore() + ")";
}
public boolean isVoiceMessage(){
public boolean isVoiceMessage() {
return "voice-message".equals(messageType);
}
@ -657,6 +672,9 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
MATTERBRIDGE_CONFIG_REMOVED,
MATTERBRIDGE_CONFIG_ENABLED,
MATTERBRIDGE_CONFIG_DISABLED,
CLEARED_CHAT
CLEARED_CHAT,
REACTION,
REACTION_DELETED,
REACTION_REVOKED
}
}

View File

@ -0,0 +1,50 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.converters
import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter
import com.nextcloud.talk.models.json.reactions.ReactionVoter.ReactionActorType.DUMMY
import com.nextcloud.talk.models.json.reactions.ReactionVoter.ReactionActorType.GUESTS
import com.nextcloud.talk.models.json.reactions.ReactionVoter.ReactionActorType.USERS
import com.nextcloud.talk.models.json.reactions.ReactionVoter
class EnumReactionActorTypeConverter : StringBasedTypeConverter<ReactionVoter.ReactionActorType>() {
override fun getFromString(string: String): ReactionVoter.ReactionActorType {
return when (string) {
"guests" -> GUESTS
"users" -> USERS
else -> DUMMY
}
}
override fun convertToString(`object`: ReactionVoter.ReactionActorType?): String {
if (`object` == null) {
return ""
}
return when (`object`) {
GUESTS -> "guests"
USERS -> "users"
else -> ""
}
}
}

View File

@ -65,6 +65,9 @@ import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MODERAT
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.OBJECT_SHARED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_REMOVED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_SET
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_DELETED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_REVOKED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.READ_ONLY
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.READ_ONLY_OFF
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_ADDED
@ -161,6 +164,9 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst
"matterbridge_config_enabled" -> return MATTERBRIDGE_CONFIG_ENABLED
"matterbridge_config_disabled" -> return MATTERBRIDGE_CONFIG_DISABLED
"history_cleared" -> return CLEARED_CHAT
"reaction" -> return REACTION
"reaction_deleted" -> return REACTION_DELETED
"reaction_revoked" -> return REACTION_REVOKED
else -> return DUMMY
}
}
@ -214,6 +220,9 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst
MATTERBRIDGE_CONFIG_ENABLED -> return "matterbridge_config_enabled"
MATTERBRIDGE_CONFIG_DISABLED -> return "matterbridge_config_disabled"
CLEARED_CHAT -> return "clear_history"
REACTION -> return "reaction"
REACTION_DELETED -> return "reaction_deleted"
REACTION_REVOKED -> return "reaction_revoked"
else -> return ""
}
}

View File

@ -0,0 +1,47 @@
/*
*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.reactions
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import com.nextcloud.talk.models.json.converters.EnumReactionActorTypeConverter
import kotlinx.android.parcel.Parcelize
@Parcelize
@JsonObject
data class ReactionVoter(
@JsonField(name = ["actorType"], typeConverter = EnumReactionActorTypeConverter::class)
var actorType: ReactionActorType?,
@JsonField(name = ["actorId"])
var actorId: String?,
@JsonField(name = ["actorDisplayName"])
var actorDisplayName: String?,
@JsonField(name = ["timestamp"])
var timestamp: Long = 0
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null, null, 0)
enum class ReactionActorType {
DUMMY, GUESTS, USERS
}
}

View File

@ -0,0 +1,37 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.reactions
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import com.nextcloud.talk.models.json.generic.GenericOCS
import kotlinx.android.parcel.Parcelize
import java.util.HashMap
@Parcelize
@JsonObject
data class ReactionsOCS(
@JsonField(name = ["data"])
var data: HashMap<String, List<ReactionVoter>>?
) : GenericOCS(), Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(HashMap())
}

View File

@ -0,0 +1,35 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.reactions
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.android.parcel.Parcelize
@Parcelize
@JsonObject
data class ReactionsOverall(
@JsonField(name = ["ocs"])
var ocs: ReactionsOCS?
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null)
}

View File

@ -32,7 +32,7 @@ data class Status(
var userId: String?,
@JsonField(name = ["message"])
var message: String?,
/* TODO: Change to enum */
/* TODO Change to enum */
@JsonField(name = ["messageId"])
var messageId: String?,
@JsonField(name = ["messageIsPredefined"])
@ -41,7 +41,7 @@ data class Status(
var icon: String?,
@JsonField(name = ["clearAt"])
var clearAt: Long = 0,
/* TODO: Change to enum */
/* TODO Change to enum */
@JsonField(name = ["status"])
var status: String = "offline",
@JsonField(name = ["statusIsUserDefined"])

View File

@ -293,7 +293,7 @@ class ConversationsListBottomDialog(
dialogRouter!!.pushController(
// TODO: refresh conversation list after EntryMenuController finished (throw event? / pass controller
// TODO refresh conversation list after EntryMenuController finished (throw event? / pass controller
// into EntryMenuController to execute fetch data... ?!)
// for example if you set a password, the dialog items should be refreshed for the next time you open it
// without to manually have to refresh the conversations list

View File

@ -20,42 +20,60 @@
package com.nextcloud.talk.ui.dialog
import android.app.Activity
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.annotation.NonNull
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.controllers.ChatController
import com.nextcloud.talk.databinding.DialogMessageActionsBinding
import com.nextcloud.talk.models.database.CapabilitiesUtil
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.utils.ApiUtils
import com.vanniktech.emoji.EmojiPopup
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
class MessageActionsDialog(
val activity: Activity,
private val chatController: ChatController,
private val message: ChatMessage,
private val userId: String?,
private val user: UserEntity?,
private val currentConversation: Conversation?,
private val showMessageDeletionButton: Boolean
) : BottomSheetDialog(activity) {
private val showMessageDeletionButton: Boolean,
private val ncApi: NcApi
) : BottomSheetDialog(chatController.activity!!, R.style.BottomSheetDialogThemeNoFloating) {
private lateinit var dialogMessageActionsBinding: DialogMessageActionsBinding
private lateinit var popup: EmojiPopup
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dialogMessageActionsBinding = DialogMessageActionsBinding.inflate(layoutInflater)
setContentView(dialogMessageActionsBinding.root)
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
initEmojiBar()
initMenuItemCopy(!message.isDeleted)
initMenuReplyToMessage(message.replyable)
initMenuReplyPrivately(
message.replyable &&
userId?.isNotEmpty() == true &&
userId != "?" &&
user?.userId?.isNotEmpty() == true &&
user?.userId != "?" &&
message.user.id.startsWith("users/") &&
message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
@ -67,6 +85,69 @@ class MessageActionsDialog(
ChatMessage.MessageType.SYSTEM_MESSAGE != message.getMessageType() &&
BuildConfig.DEBUG
)
initEmojiMore()
}
@SuppressLint("ClickableViewAccessibility")
private fun initEmojiMore() {
dialogMessageActionsBinding.emojiMore.setOnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
popup.toggle()
}
true
}
popup = EmojiPopup.Builder
.fromRootView(dialogMessageActionsBinding.root)
.setOnEmojiPopupShownListener {
dialogMessageActionsBinding.emojiMore.clearFocus()
dialogMessageActionsBinding.messageActions.visibility = View.GONE
}
.setOnEmojiClickListener { _, imageView ->
popup.dismiss()
sendReaction(message, imageView.unicode)
}
.setOnEmojiPopupDismissListener {
dialogMessageActionsBinding.emojiMore.clearFocus()
dialogMessageActionsBinding.messageActions.visibility = View.VISIBLE
val imm: InputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as
InputMethodManager
imm.hideSoftInputFromWindow(dialogMessageActionsBinding.emojiMore.windowToken, 0)
}
.build(dialogMessageActionsBinding.emojiMore)
dialogMessageActionsBinding.emojiMore.disableKeyboardInput(popup)
dialogMessageActionsBinding.emojiMore.forceSingleEmoji()
}
private fun initEmojiBar() {
if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "reactions")) {
dialogMessageActionsBinding.emojiThumbsUp.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiThumbsUp.text.toString())
}
dialogMessageActionsBinding.emojiThumbsDown.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiThumbsDown.text.toString())
}
dialogMessageActionsBinding.emojiLaugh.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiLaugh.text.toString())
}
dialogMessageActionsBinding.emojiHeart.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiHeart.text.toString())
}
dialogMessageActionsBinding.emojiConfused.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiConfused.text.toString())
}
dialogMessageActionsBinding.emojiSad.setOnClickListener {
sendReaction(message, dialogMessageActionsBinding.emojiSad.text.toString())
}
dialogMessageActionsBinding.emojiMore.setOnClickListener {
dismiss()
}
dialogMessageActionsBinding.emojiBar.visibility = View.VISIBLE
} else {
dialogMessageActionsBinding.emojiBar.visibility = View.GONE
}
}
private fun initMenuMarkAsUnread(visible: Boolean) {
@ -150,8 +231,47 @@ class MessageActionsDialog(
}
}
private fun sendReaction(message: ChatMessage, emoji: String) {
val credentials = ApiUtils.getCredentials(user?.username, user?.token)
ncApi.sendReaction(
credentials,
ApiUtils.getUrlForMessageReaction(
user?.baseUrl,
currentConversation!!.token,
message.id
),
emoji
)
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(@NonNull genericOverall: GenericOverall) {
val statusCode = genericOverall.ocs.meta.statusCode
if (statusCode == HTTP_CREATED) {
chatController.updateAdapterAfterSendReaction(message, emoji)
}
}
override fun onError(e: Throwable) {
Log.e(TAG, "error while sending reaction")
}
override fun onComplete() {
dismiss()
}
})
}
companion object {
private const val TAG = "MessageActionsDialog"
private const val ACTOR_LENGTH = 6
private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
private const val HTTP_OK: Int = 200
private const val HTTP_CREATED: Int = 201
}
}

View File

@ -0,0 +1,351 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* @author Mario Danic
* @author Marcel Hibbe
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
* Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Parts related to account import were either copied from or inspired by the great work done by David Luhmer at:
* https://github.com/nextcloud/ownCloud-Account-Importer
*/
package com.nextcloud.talk.ui.dialog
import android.app.Activity
import android.os.Bundle
import android.util.Log
import android.view.ViewGroup
import androidx.annotation.NonNull
import androidx.recyclerview.widget.LinearLayoutManager
import autodagger.AutoInjector
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.ReactionItem
import com.nextcloud.talk.adapters.ReactionItemClickListener
import com.nextcloud.talk.adapters.ReactionsAdapter
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.DialogMessageReactionsBinding
import com.nextcloud.talk.databinding.ItemReactionsTabBinding
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.reactions.ReactionsOverall
import com.nextcloud.talk.utils.ApiUtils
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import java.util.Collections
import java.util.Comparator
@AutoInjector(NextcloudTalkApplication::class)
class ShowReactionsDialog(
activity: Activity,
private val currentConversation: Conversation?,
private val chatMessage: ChatMessage,
private val userEntity: UserEntity?,
private val ncApi: NcApi
) : BottomSheetDialog(activity), ReactionItemClickListener {
private lateinit var binding: DialogMessageReactionsBinding
private var adapter: ReactionsAdapter? = null
private val tagAll: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DialogMessageReactionsBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
adapter = ReactionsAdapter(this, userEntity)
binding.reactionsList.adapter = adapter
binding.reactionsList.layoutManager = LinearLayoutManager(context)
initEmojiReactions()
}
private fun initEmojiReactions() {
adapter?.list?.clear()
if (chatMessage.reactions != null && chatMessage.reactions.isNotEmpty()) {
var reactionsTotal = 0
for ((emoji, amount) in chatMessage.reactions) {
reactionsTotal = reactionsTotal.plus(amount as Int)
val tab: TabLayout.Tab = binding.emojiReactionsTabs.newTab() // Create a new Tab names "First Tab"
val itemBinding = ItemReactionsTabBinding.inflate(layoutInflater)
itemBinding.reactionTab.tag = emoji
itemBinding.reactionIcon.text = emoji
itemBinding.reactionCount.text = amount.toString()
tab.customView = itemBinding.root
binding.emojiReactionsTabs.addTab(tab)
}
val tab: TabLayout.Tab = binding.emojiReactionsTabs.newTab() // Create a new Tab names "First Tab"
val itemBinding = ItemReactionsTabBinding.inflate(layoutInflater)
itemBinding.reactionTab.tag = tagAll
itemBinding.reactionIcon.text = context.getString(R.string.reactions_tab_all)
itemBinding.reactionCount.text = reactionsTotal.toString()
tab.customView = itemBinding.root
binding.emojiReactionsTabs.addTab(tab, 0)
binding.emojiReactionsTabs.getTabAt(0)?.select()
binding.emojiReactionsTabs.addOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
// called when a tab is reselected
updateParticipantsForEmoji(chatMessage, tab.customView?.tag as String?)
}
override fun onTabUnselected(tab: TabLayout.Tab) {
// called when a tab is reselected
}
override fun onTabReselected(tab: TabLayout.Tab) {
// called when a tab is reselected
}
})
updateParticipantsForEmoji(chatMessage, tagAll)
}
adapter?.notifyDataSetChanged()
}
private fun updateParticipantsForEmoji(chatMessage: ChatMessage, emoji: String?) {
adapter?.list?.clear()
val credentials = ApiUtils.getCredentials(userEntity?.username, userEntity?.token)
ncApi.getReactions(
credentials,
ApiUtils.getUrlForMessageReaction(
userEntity?.baseUrl,
currentConversation!!.token,
chatMessage.id
),
emoji
)
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<ReactionsOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(@NonNull reactionsOverall: ReactionsOverall) {
val reactionVoters: ArrayList<ReactionItem> = ArrayList()
if (reactionsOverall.ocs?.data != null) {
val map = reactionsOverall.ocs?.data
for (key in map!!.keys) {
for (reactionVoter in reactionsOverall.ocs?.data!![key]!!) {
reactionVoters.add(ReactionItem(reactionVoter, key))
}
}
Collections.sort(reactionVoters, ReactionComparator(userEntity?.userId))
adapter?.list?.addAll(reactionVoters)
adapter?.notifyDataSetChanged()
} else {
Log.e(TAG, "no voters for this reaction")
}
}
override fun onError(e: Throwable) {
Log.e(TAG, "failed to retrieve list of reaction voters")
}
override fun onComplete() {
// unused atm
}
})
}
override fun onClick(reactionItem: ReactionItem) {
if (reactionItem.reactionVoter.actorId?.equals(userEntity?.userId) == true) {
deleteReaction(chatMessage, reactionItem.reaction!!)
}
dismiss()
}
private fun deleteReaction(message: ChatMessage, emoji: String) {
val credentials = ApiUtils.getCredentials(userEntity?.username, userEntity?.token)
ncApi.deleteReaction(
credentials,
ApiUtils.getUrlForMessageReaction(
userEntity?.baseUrl,
currentConversation!!.token,
message.id
),
emoji
)
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<GenericOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(@NonNull genericOverall: GenericOverall) {
Log.d(TAG, "deleted reaction: $emoji")
}
override fun onError(e: Throwable) {
Log.e(TAG, "error while deleting reaction: $emoji")
}
override fun onComplete() {
dismiss()
}
})
}
companion object {
const val TAG = "ShowReactionsDialog"
}
class ReactionComparator(val activeUser: String?) : Comparator<ReactionItem> {
@Suppress("ReturnCount")
override fun compare(reactionItem1: ReactionItem?, reactionItem2: ReactionItem?): Int {
// sort by emoji, own account, display-name, timestamp, actor-id
if (reactionItem1 == null && reactionItem2 == null) {
return 0
}
if (reactionItem1 == null) {
return -1
}
if (reactionItem2 == null) {
return 1
}
// emoji
val reaction = StringComparator().compare(reactionItem1.reaction, reactionItem2.reaction)
if (reaction != 0) {
return reaction
}
// own account
val ownAccount = compareOwnAccount(
activeUser,
reactionItem1.reactionVoter.actorId,
reactionItem2.reactionVoter.actorId
)
if (ownAccount != 0) {
return ownAccount
}
// display-name
val displayName = StringComparator()
.compare(
reactionItem1.reactionVoter.actorDisplayName,
reactionItem2.reactionVoter.actorDisplayName
)
if (displayName != 0) {
return displayName
}
// timestamp
val timestamp = LongComparator()
.compare(
reactionItem1.reactionVoter.timestamp,
reactionItem2.reactionVoter.timestamp
)
if (timestamp != 0) {
return timestamp
}
// actor-id
val actorId = StringComparator()
.compare(
reactionItem1.reactionVoter.actorId,
reactionItem2.reactionVoter.actorId
)
if (actorId != 0) {
return actorId
}
return 0
}
@Suppress("ReturnCount")
fun compareOwnAccount(activeUser: String?, actorId1: String?, actorId2: String?): Int {
val reactionVote1Active = activeUser == actorId1
val reactionVote2Active = activeUser == actorId2
if (!reactionVote1Active && !reactionVote2Active || reactionVote1Active && reactionVote2Active) {
return 0
}
if (activeUser == null) {
return 0
}
if (reactionVote1Active) {
return 1
}
if (reactionVote2Active) {
return -1
}
return 0
}
internal class StringComparator : Comparator<String?> {
@Suppress("ReturnCount")
override fun compare(obj1: String?, obj2: String?): Int {
if (obj1 === obj2) {
return 0
}
if (obj1 == null) {
return -1
}
return if (obj2 == null) {
1
} else obj1.lowercase().compareTo(obj2.lowercase())
}
}
internal class LongComparator : Comparator<Long?> {
@Suppress("ReturnCount")
override fun compare(obj1: Long?, obj2: Long?): Int {
if (obj1 === obj2) {
return 0
}
if (obj1 == null) {
return -1
}
return if (obj2 == null) {
1
} else obj1.compareTo(obj2)
}
}
}
}

View File

@ -441,4 +441,11 @@ public class ApiUtils {
public static String getUrlForUserStatuses(String baseUrl) {
return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/statuses";
}
public static String getUrlForMessageReaction(String baseUrl,
String roomToken,
String messageId) {
return baseUrl + ocsApiVersion + spreedApiVersion + "/reaction/" + roomToken + "/" + messageId;
}
}

View File

@ -22,7 +22,7 @@ package com.nextcloud.talk.utils
import android.content.Context
// TODO: improve log handling. https://github.com/nextcloud/talk-android/issues/1376
// TODO improve log handling. https://github.com/nextcloud/talk-android/issues/1376
// writing logs to a file is temporarily disabled to avoid huge logfiles.
object LoggingUtils {

View File

@ -243,7 +243,7 @@ public interface AppPreferences {
@KeyByString("phone_book_integration")
void setPhoneBookIntegration(boolean value);
// TODO: Remove in 13.0.0
// TODO Remove in 13.0.0
@KeyByString("link_previews")
@RemoveMethod
void removeLinkPreviews();

View File

@ -151,7 +151,7 @@ public class MagicWebSocketInstance extends WebSocketListener {
public void restartWebSocket() {
reconnecting = true;
// TODO: when improving logging, keep in mind this issue: https://github.com/nextcloud/talk-android/issues/1013
// TODO when improving logging, keep in mind this issue: https://github.com/nextcloud/talk-android/issues/1013
Log.d(TAG, "restartWebSocket: " + connectionUrl);
Request request = new Request.Builder().url(connectionUrl).build();
okHttpClient.newWebSocket(request, this);

View File

@ -18,8 +18,14 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/medium_emphasis_text" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="@color/medium_emphasis_text"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>

View File

@ -0,0 +1,26 @@
<!--
@author Google LLC
Copyright (C) 2021 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="@color/high_emphasis_menu_icon"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z" />
</vector>

View File

@ -23,7 +23,6 @@
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/bg_bottom_sheet"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

View File

@ -23,7 +23,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/bg_bottom_sheet"
android:paddingStart="@dimen/standard_padding"
android:paddingTop="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_half_padding">

View File

@ -25,7 +25,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/bg_bottom_sheet"
android:orientation="vertical"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_padding"
@ -56,7 +55,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_baseline_person_24"
app:tint="@color/colorPrimary" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/shareContactText"
@ -87,7 +86,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_baseline_location_on_24"
app:tint="@color/colorPrimary" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_share_location"
@ -118,7 +117,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_baseline_photo_camera_24"
app:tint="@color/colorPrimary" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_attach_picture_from_cam"
@ -149,7 +148,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/upload"
app:tint="@color/colorPrimary" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_attach_file_from_local"
@ -180,7 +179,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_share_variant"
app:tint="@color/colorPrimary" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_attach_file_from_cloud"

View File

@ -49,11 +49,11 @@
<ImageView
android:id="@+id/audio_output_bluetooth_icon"
android:layout_width="11dp"
android:layout_height="12dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_baseline_bluetooth_audio_24"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon_inverse" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/audio_output_bluetooth_text"
@ -84,7 +84,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_volume_up_white_24dp"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon_inverse" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/audio_output_speaker_text"
@ -115,7 +115,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_baseline_phone_in_talk_24"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon_inverse" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/audio_output_earspeaker_text"
@ -146,7 +146,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_baseline_headset_mic_24"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon_inverse" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/audio_output_wired_headset_text"

View File

@ -23,7 +23,6 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/bg_bottom_sheet"
android:orientation="vertical"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_padding"

View File

@ -25,7 +25,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/bg_bottom_sheet"
android:orientation="vertical"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_padding"
@ -60,7 +59,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_star_border_black_24dp"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
@ -87,7 +86,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_star_black_24dp"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
@ -114,7 +113,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_eye"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
@ -141,7 +140,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_pencil_grey600_24dp"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
@ -168,7 +167,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_link_grey600_24px"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
@ -195,7 +194,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_lock_grey600_24px"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
@ -222,7 +221,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_lock_open_grey600_24dp"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
@ -249,7 +248,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_lock_plus_grey600_24dp"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
@ -276,7 +275,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_delete_grey600_24dp"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
@ -303,7 +302,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_link_grey600_24px"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
@ -330,7 +329,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_group_grey600_24px"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
@ -357,7 +356,7 @@
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_exit_to_app_black_24dp"
app:tint="@color/grey_600" />
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"

View File

@ -23,195 +23,283 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/bg_bottom_sheet"
android:orientation="vertical"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_padding"
android:paddingBottom="@dimen/standard_half_padding">
<LinearLayout
android:id="@+id/menu_copy_message"
android:id="@+id/emojiBar"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:layout_marginStart="@dimen/standard_eighth_margin"
android:layout_marginEnd="@dimen/zero"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
android:orientation="horizontal">
<ImageView
android:id="@+id/menu_icon_copy_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_content_copy"
app:tint="@color/grey_600" />
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/emojiThumbsUp"
android:layout_width="@dimen/activity_row_layout_height"
android:layout_height="@dimen/activity_row_layout_height"
android:layout_weight="1"
android:cursorVisible="false"
android:gravity="center"
android:text="@string/emoji_thumbsUp"
android:textSize="24sp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_copy_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_copy_message"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/emojiThumbsDown"
android:layout_width="@dimen/activity_row_layout_height"
android:layout_height="@dimen/activity_row_layout_height"
android:layout_weight="1"
android:cursorVisible="false"
android:gravity="center"
android:text="@string/emoji_thumbsDown"
android:textSize="24sp" />
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/emojiHeart"
android:layout_width="@dimen/activity_row_layout_height"
android:layout_height="@dimen/activity_row_layout_height"
android:layout_weight="1"
android:cursorVisible="false"
android:gravity="center"
android:text="@string/default_emoji"
android:textSize="24sp" />
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/emojiLaugh"
android:layout_width="@dimen/activity_row_layout_height"
android:layout_height="@dimen/activity_row_layout_height"
android:layout_weight="1"
android:cursorVisible="false"
android:gravity="center"
android:text="@string/emoji_heart"
android:textSize="24sp" />
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/emojiConfused"
android:layout_width="@dimen/activity_row_layout_height"
android:layout_height="@dimen/activity_row_layout_height"
android:layout_weight="1"
android:cursorVisible="false"
android:gravity="center"
android:text="@string/emoji_confused"
android:textSize="24sp" />
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/emojiSad"
android:layout_width="@dimen/activity_row_layout_height"
android:layout_height="@dimen/activity_row_layout_height"
android:layout_weight="1"
android:cursorVisible="false"
android:gravity="center"
android:text="@string/emoji_sad"
android:textSize="24sp" />
<com.vanniktech.emoji.EmojiEditText
android:id="@+id/emojiMore"
android:layout_width="@dimen/activity_row_layout_height"
android:layout_height="@dimen/activity_row_layout_height"
android:layout_weight="1"
android:background="@android:color/transparent"
android:contentDescription="@string/emoji_more"
android:drawableEnd="@drawable/ic_dots_horizontal"
android:paddingStart="@dimen/zero"
android:paddingEnd="@dimen/standard_padding" />
</LinearLayout>
<LinearLayout
android:id="@+id/menu_mark_as_unread"
android:id="@+id/message_actions"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
android:layout_height="wrap_content"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_padding"
android:orientation="vertical">
<ImageView
android:id="@+id/menu_icon_mark_as_unread"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_eye_off"
app:tint="@color/grey_600" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_mark_as_unread"
<LinearLayout
android:id="@+id/menu_reply_to_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_mark_as_unread"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
</LinearLayout>
<ImageView
android:id="@+id/menu_icon_reply_to_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_reply"
app:tint="@color/high_emphasis_menu_icon" />
<LinearLayout
android:id="@+id/menu_forward_message"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_reply_to_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_reply"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
<ImageView
android:id="@+id/menu_icon_forward_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_share_action"
app:tint="@color/grey_600" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_forward_message"
<LinearLayout
android:id="@+id/menu_reply_privately"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_forward_message"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
</LinearLayout>
<ImageView
android:id="@+id/menu_icon_reply_privately"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_reply"
app:tint="@color/high_emphasis_menu_icon" />
<LinearLayout
android:id="@+id/menu_reply_to_message"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_reply_privately"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_reply_privately"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
<ImageView
android:id="@+id/menu_icon_reply_to_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_reply"
app:tint="@color/grey_600" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_reply_to_message"
<LinearLayout
android:id="@+id/menu_forward_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_reply"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
</LinearLayout>
<ImageView
android:id="@+id/menu_icon_forward_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_share_action"
app:tint="@color/high_emphasis_menu_icon" />
<LinearLayout
android:id="@+id/menu_reply_privately"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_forward_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_forward_message"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
<ImageView
android:id="@+id/menu_icon_reply_privately"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_reply"
app:tint="@color/grey_600" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_reply_privately"
<LinearLayout
android:id="@+id/menu_mark_as_unread"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_reply_privately"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
</LinearLayout>
<ImageView
android:id="@+id/menu_icon_mark_as_unread"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_eye_off"
app:tint="@color/high_emphasis_menu_icon" />
<LinearLayout
android:id="@+id/menu_delete_message"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_mark_as_unread"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_mark_as_unread"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
<ImageView
android:id="@+id/menu_icon_delete_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_delete"
app:tint="@color/grey_600" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_delete_message"
<LinearLayout
android:id="@+id/menu_copy_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_delete_message"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/menu_icon_copy_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_content_copy"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_copy_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_copy_message"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/menu_delete_message"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/menu_icon_delete_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:src="@drawable/ic_delete"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_delete_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero"
android:text="@string/nc_delete_message"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Andy Scherzinger
~ @author Marcel Hibbe
~ Copyright (C) 2022 Andy Scherzinger
~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="288dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/reactions_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/reaction_item" />
</LinearLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/emoji_reactions_tabs"
android:layout_width="wrap_content"
android:layout_height="@dimen/min_size_clickable_area"
app:tabGravity="fill"
app:tabMode="scrollable" />
</LinearLayout>

View File

@ -24,7 +24,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/bg_bottom_sheet"
android:orientation="vertical"
android:paddingStart="@dimen/standard_padding"
android:paddingTop="@dimen/standard_half_padding"

View File

@ -76,7 +76,7 @@
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.2"
android:textAlignment="viewStart"
android:textIsSelectable="true"
android:textIsSelectable="false"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true" />
@ -89,5 +89,8 @@
android:layout_marginStart="8dp"
app:layout_alignSelf="center" />
<include
android:id="@+id/reactions"
layout="@layout/reactions_inside_message" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -173,6 +173,10 @@
android:textColor="@color/warm_grey_four"
app:layout_alignSelf="center"
tools:text="12:38" />
<include
android:id="@+id/reactions"
layout="@layout/reactions_inside_message" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -47,8 +47,7 @@
android:orientation="vertical"
app:alignContent="stretch"
app:alignItems="stretch"
app:flexWrap="wrap"
app:justifyContent="flex_end">
app:flexWrap="wrap">
<include
android:id="@+id/message_quote"
@ -87,5 +86,9 @@
android:textIsSelectable="false"
app:layout_alignSelf="center" />
<include
android:id="@+id/reactions"
layout="@layout/reactions_inside_message" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -108,5 +108,9 @@
app:layout_alignSelf="center"
tools:text="12:38"/>
<include
android:id="@+id/reactions"
layout="@layout/reactions_inside_message" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -59,7 +59,7 @@
android:lineSpacingMultiplier="1.2"
android:textAlignment="viewStart"
android:textColorHighlight="@color/nc_grey"
android:textIsSelectable="true"
android:textIsSelectable="false"
tools:text="Talk to you later!" />
<TextView
@ -80,5 +80,8 @@
app:layout_alignSelf="center"
android:contentDescription="@null" />
<include
android:id="@+id/reactions"
layout="@layout/reactions_inside_message" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -163,6 +163,10 @@
android:textColor="@color/warm_grey_four"
app:layout_alignSelf="center"
tools:text="12:34" />
<include
android:id="@+id/reactions"
layout="@layout/reactions_inside_message" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -76,5 +76,9 @@
android:contentDescription="@null"
app:layout_alignSelf="center" />
<include
android:id="@+id/reactions"
layout="@layout/reactions_inside_message" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -104,5 +104,9 @@
app:layout_alignSelf="center"
android:contentDescription="@null" />
<include
android:id="@+id/reactions"
layout="@layout/reactions_inside_message" />
</com.google.android.flexbox.FlexboxLayout>
</RelativeLayout>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Andy Scherzinger
~ Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/reaction_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<androidx.emoji.widget.EmojiTextView
android:id="@+id/reaction_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:textIsSelectable="false"
android:textSize="14sp"
android:textStyle="bold"
tools:text="@string/default_emoji" />
<TextView
android:id="@+id/reaction_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
tools:text="1" />
</LinearLayout>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?><!--
Nextcloud Talk application
Copyright (C) 2022 Andy Scherzinger
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/item_height">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
android:layout_gravity="center_vertical"
android:layout_margin="@dimen/standard_margin"
app:roundAsCircle="true" />
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="middle"
android:gravity="center_vertical|start"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size"
tools:text="Participant Name" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/reaction"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:gravity="center"
android:textSize="24sp"
android:layout_marginEnd="@dimen/standard_half_margin"
tools:text="@string/default_emoji" />
</LinearLayout>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?><!--
Nextcloud Talk application
Copyright (C) 2022 Marcel Hibbe
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/reactions_emoji_wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:layout_alignSelf="flex_start"
app:layout_flexGrow="1"
app:layout_wrapBefore="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="emojis">
</TextView>
</LinearLayout>

View File

@ -38,7 +38,9 @@
<color name="high_emphasis_text">#deffffff</color>
<color name="medium_emphasis_text">#99ffffff</color>
<color name="low_emphasis_text">#61ffffff</color>
<color name="high_emphasis_text_inverse">#de000000</color>
<!-- bottom sheet specific icon default color -->
<color name="high_emphasis_menu_icon">#8Affffff</color>
<color name="bg_default">#121212</color>
<color name="bg_default_semitransparent">#99121212</color>
@ -58,7 +60,7 @@
<!-- Chat window incoming message text & informational -->
<color name="bg_bottom_sheet">#121212</color>
<color name="bg_message_list_incoming_bubble">#1CFFFFFF</color>
<color name="bg_message_list_incoming_bubble">#2A2A2A</color>
<color name="bg_message_list_incoming_bubble_deleted">#14FFFFFF</color>
<color name="textColorMaxContrast">#8c8c8c</color>

View File

@ -39,12 +39,15 @@
<color name="high_emphasis_text">#de000000</color>
<color name="medium_emphasis_text">#99000000</color>
<color name="low_emphasis_text">#61000000</color>
<color name="high_emphasis_text_inverse">#deffffff</color>
<!-- general text colors for dark background -->
<color name="high_emphasis_text_dark_background">#deffffff</color>
<color name="medium_emphasis_text_dark_background">#99ffffff</color>
<!-- bottom sheet specific icon default color -->
<color name="high_emphasis_menu_icon_inverse">#8Affffff</color>
<color name="high_emphasis_menu_icon">#8A000000</color>
<!-- Text color of sent messages -->
<color name="nc_outcoming_text_default">#FFFFFF</color>
<!-- Text color of received messages -->
@ -82,7 +85,7 @@
<color name="bg_message_list_outcoming_bubble">@color/colorPrimary</color>
<color name="bg_message_list_outcoming_bubble_deleted">#800082C9</color>
<color name="bg_bottom_sheet">#46ffffff</color>
<color name="bg_bottom_sheet">#FFFFFF</color>
<color name="bg_call_screen_dialog">#121212</color>
<color name="call_screen_text">#ffffffff</color>

View File

@ -279,6 +279,12 @@
<string name="invisible">Invisible</string>
<string translatable="false" name="divider"></string>
<string translatable="false" name="default_emoji">😃</string>
<string translatable="false" name="emoji_thumbsUp">👍</string>
<string translatable="false" name="emoji_thumbsDown">👎</string>
<string translatable="false" name="emoji_heart">❤️</string>
<string translatable="false" name="emoji_confused">😯</string>
<string translatable="false" name="emoji_sad">😢</string>
<string translatable="false" name="emoji_more">More emojis</string>
<string name="dontClear">Don\'t clear</string>
<string name="today">Today</string>
<string name="thirtyMinutes">30 minutes</string>
@ -507,4 +513,6 @@
<string name="audio_output_dialog_headline">Audio output</string>
<string name="audio_output_wired_headset">Wired headset</string>
<string name="reactions_tab_all">All</string>
</resources>

View File

@ -40,6 +40,7 @@
<item name="android:navigationBarColor">@color/bg_default</item>
<item name="android:seekBarStyle">@style/Nextcloud.Material.Incoming.SeekBar</item>
<item name="seekBarStyle">@style/Nextcloud.Material.Incoming.SeekBar</item>
<item name="bottomSheetDialogTheme">@style/ThemeOverlay.App.BottomSheetDialog</item>
</style>
<style name="ThemeOverlay.AppTheme.PopupMenu" parent="ThemeOverlay.MaterialComponents.Dark">
@ -55,6 +56,14 @@
<item name="elevation">1dp</item>
</style>
<style name="ThemeOverlay.App.BottomSheetDialog" parent="ThemeOverlay.MaterialComponents.DayNight.BottomSheetDialog">
<item name="bottomSheetStyle">@style/Talk.BottomSheetDialog</item>
</style>
<style name="Talk.BottomSheetDialog" parent="Widget.MaterialComponents.BottomSheet.Modal">
<item name="backgroundTint">@color/bg_bottom_sheet</item>
</style>
<style name="TransparentTheme" parent="Theme.MaterialComponents.NoActionBar.Bridge">
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/background_dark</item>
@ -235,7 +244,7 @@
<item name="android:colorControlNormal">#ffffff</item>
</style>
<style name="BottomSheetDialogThemeNoFloating" parent="Theme.Design.Light.BottomSheetDialog">
<style name="BottomSheetDialogThemeNoFloating" parent="ThemeOverlay.MaterialComponents.DayNight.BottomSheetDialog">
<item name="android:windowIsFloating">false</item>
<item name="android:windowSoftInputMode">adjustResize</item>
</style>

View File

@ -1,5 +1,5 @@
build:
maxIssues: 98
maxIssues: 95
weights:
# complexity: 2
# LongParameterList: 1

View File

@ -1,2 +1,2 @@
DO NOT TOUCH; GENERATED BY DRONE
<span class="mdl-layout-title">Lint Report: 1 error and 164 warnings</span>
<span class="mdl-layout-title">Lint Report: 1 error and 156 warnings</span>