diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt new file mode 100644 index 000000000..8ada42208 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt @@ -0,0 +1,241 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2021 Marcel Hibbe + * + * 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 . + */ + +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.os.Build +import android.text.TextUtils +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.ViewCompat +import autodagger.AutoInjector +import coil.load +import com.amulyakhare.textdrawable.TextDrawable +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.databinding.ItemCustomIncomingPollMessageBinding +import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class IncomingPollMessageViewHolder(incomingView: View, payload: Any) : MessageHolders +.IncomingTextMessageViewHolder(incomingView, payload) { + + private val binding: ItemCustomIncomingPollMessageBinding = + ItemCustomIncomingPollMessageBinding.bind(itemView) + + @JvmField + @Inject + var context: Context? = null + + @JvmField + @Inject + var appPreferences: AppPreferences? = null + + @Inject + @JvmField + var ncApi: NcApi? = null + + lateinit var message: ChatMessage + + lateinit var reactionsInterface: ReactionsInterface + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + this.message = message + sharedApplication!!.componentApplication.inject(this) + + setAvatarAndAuthorOnMessageItem(message) + + colorizeMessageBubble(message) + + itemView.isSelected = false + binding.messageTime.setTextColor(ResourcesCompat.getColor(context?.resources!!, R.color.warm_grey_four, null)) + + // parent message handling + setParentMessageDataOnMessageItem(message) + + setPollPreview(message) + + Reaction().showReactions(message, binding.reactions, binding.messageTime.context, false) + binding.reactions.reactionsEmojiWrapper.setOnClickListener { + reactionsInterface.onClickReactions(message) + } + binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? -> + reactionsInterface.onLongClickReactions(message) + true + } + } + + private fun setPollPreview(message: ChatMessage) { + var pollId: String? + var pollName: String? = "" + + if (message.messageParameters != null && message.messageParameters!!.size > 0) { + for (key in message.messageParameters!!.keys) { + val individualHashMap: Map = message.messageParameters!![key]!! + if (individualHashMap["type"] == "talk-poll") { + pollId = individualHashMap["id"] + pollName = individualHashMap["name"] + } + } + } + + binding.messagePollTitle.text = pollName + + // TODO: how to get room token here? + // val credentials = ApiUtils.getCredentials(message.activeUser?.username, message.activeUser?.token) + // ncApi!!.getPoll( + // credentials, + // ApiUtils.getUrlForPoll( + // message.activeUser?.baseUrl, + // ??????? + // ) + // ) + } + + private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) { + val author: String = message.actorDisplayName!! + if (!TextUtils.isEmpty(author)) { + binding.messageAuthor.text = author + binding.messageUserAvatar.setOnClickListener { + (payload as? ProfileBottomSheet)?.showFor(message.actorId!!, itemView.context) + } + } else { + binding.messageAuthor.setText(R.string.nc_nick_guest) + } + + if (!message.isGrouped && !message.isOneToOneConversation) { + setAvatarOnMessage(message) + } else { + if (message.isOneToOneConversation) { + binding.messageUserAvatar.visibility = View.GONE + } else { + binding.messageUserAvatar.visibility = View.INVISIBLE + } + binding.messageAuthor.visibility = View.GONE + } + } + + private fun setAvatarOnMessage(message: ChatMessage) { + binding.messageUserAvatar.visibility = View.VISIBLE + if (message.actorType == "guests") { + // do nothing, avatar is set + } else if (message.actorType == "bots" && message.actorId == "changelog") { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val layers = arrayOfNulls(2) + layers[0] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_background) + layers[1] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_foreground) + val layerDrawable = LayerDrawable(layers) + binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable)) + } else { + binding.messageUserAvatar.setImageResource(R.mipmap.ic_launcher) + } + } else if (message.actorType == "bots") { + val drawable = TextDrawable.builder() + .beginConfig() + .bold() + .endConfig() + .buildRound( + ">", + ResourcesCompat.getColor(context!!.resources, R.color.black, null) + ) + binding.messageUserAvatar.visibility = View.VISIBLE + binding.messageUserAvatar.setImageDrawable(drawable) + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + val resources = itemView.resources + + var bubbleResource = R.drawable.shape_incoming_message + + if (message.isGrouped) { + bubbleResource = R.drawable.shape_grouped_incoming_message + } + + val bgBubbleColor = if (message.isDeleted) { + ResourcesCompat.getColor(resources, R.color.bg_message_list_incoming_bubble_deleted, null) + } else { + ResourcesCompat.getColor(resources, R.color.bg_message_list_incoming_bubble, null) + } + val bubbleDrawable = DisplayUtils.getMessageSelector( + bgBubbleColor, + ResourcesCompat.getColor(resources, R.color.transparent, null), + bgBubbleColor, bubbleResource + ) + ViewCompat.setBackground(bubble, bubbleDrawable) + } + + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (!message.isDeleted && message.parentMessage != null) { + val parentChatMessage = message.parentMessage + parentChatMessage!!.activeUser = message.activeUser + parentChatMessage!!.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token) + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context!!.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = parentChatMessage.text + + binding.messageQuote.quotedMessageAuthor + .setTextColor(ContextCompat.getColor(context!!, R.color.textColorMaxContrast)) + + if (parentChatMessage.actorId?.equals(message.activeUser!!.userId) == true) { + binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.colorPrimary) + } else { + binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) + } + + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + fun assignReactionInterface(reactionsInterface: ReactionsInterface) { + this.reactionsInterface = reactionsInterface + } + + companion object { + private val TAG = NextcloudTalkApplication::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt new file mode 100644 index 000000000..fdbb4f2a4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt @@ -0,0 +1,175 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2017-2018 Mario Danic + * Copyright (C) 2021 Marcel Hibbe + * + * 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 . + */ + +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.PorterDuff +import android.os.Handler +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.ViewCompat +import autodagger.AutoInjector +import coil.load +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.databinding.ItemCustomOutcomingPollMessageBinding +import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class OutcomingPollMessageViewHolder(outcomingView: View) : MessageHolders +.OutcomingTextMessageViewHolder(outcomingView) { + + private val binding: ItemCustomOutcomingPollMessageBinding = + ItemCustomOutcomingPollMessageBinding.bind(itemView) + + @JvmField + @Inject + var context: Context? = null + + @JvmField + @Inject + var appPreferences: AppPreferences? = null + + lateinit var message: ChatMessage + + lateinit var handler: Handler + + lateinit var reactionsInterface: ReactionsInterface + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + this.message = message + sharedApplication!!.componentApplication.inject(this) + + colorizeMessageBubble(message) + + itemView.isSelected = false + binding.messageTime.setTextColor(context!!.resources.getColor(R.color.white60)) + + // parent message handling + setParentMessageDataOnMessageItem(message) + + val readStatusDrawableInt = when (message.readStatus) { + ReadStatus.READ -> R.drawable.ic_check_all + ReadStatus.SENT -> R.drawable.ic_check + else -> null + } + + val readStatusContentDescriptionString = when (message.readStatus) { + ReadStatus.READ -> context?.resources?.getString(R.string.nc_message_read) + ReadStatus.SENT -> context?.resources?.getString(R.string.nc_message_sent) + else -> null + } + + readStatusDrawableInt?.let { drawableInt -> + AppCompatResources.getDrawable(context!!, drawableInt)?.let { + it.setColorFilter(context?.resources!!.getColor(R.color.white60), PorterDuff.Mode.SRC_ATOP) + binding.checkMark.setImageDrawable(it) + } + } + + binding.checkMark.setContentDescription(readStatusContentDescriptionString) + + Reaction().showReactions(message, binding.reactions, binding.messageTime.context, true) + binding.reactions.reactionsEmojiWrapper.setOnClickListener { + reactionsInterface.onClickReactions(message) + } + binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? -> + reactionsInterface.onLongClickReactions(message) + true + } + } + + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + if (!message.isDeleted && message.parentMessage != null) { + val parentChatMessage = message.parentMessage + parentChatMessage!!.activeUser = message.activeUser + parentChatMessage.imageUrl?.let { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token) + ) + } + } ?: run { + binding.messageQuote.quotedMessageImage.visibility = View.GONE + } + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName + ?: context!!.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = parentChatMessage.text + binding.messageQuote.quotedMessage.setTextColor( + context!!.resources.getColor(R.color.nc_outcoming_text_default) + ) + binding.messageQuote.quotedMessageAuthor.setTextColor(context!!.resources.getColor(R.color.nc_grey)) + + binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white) + + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + val resources = sharedApplication!!.resources + val bgBubbleColor = if (message.isDeleted) { + resources.getColor(R.color.bg_message_list_outcoming_bubble_deleted) + } else { + resources.getColor(R.color.bg_message_list_outcoming_bubble) + } + if (message.isGrouped) { + val bubbleDrawable = DisplayUtils.getMessageSelector( + bgBubbleColor, + resources.getColor(R.color.transparent), + bgBubbleColor, + R.drawable.shape_grouped_outcoming_message + ) + ViewCompat.setBackground(bubble, bubbleDrawable) + } else { + val bubbleDrawable = DisplayUtils.getMessageSelector( + bgBubbleColor, + resources.getColor(R.color.transparent), + bgBubbleColor, + R.drawable.shape_outcoming_message + ) + ViewCompat.setBackground(bubble, bubbleDrawable) + } + } + + fun assignReactionInterface(reactionsInterface: ReactionsInterface) { + this.reactionsInterface = reactionsInterface + } + + companion object { + private val TAG = NextcloudTalkApplication::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 6f2b65e3d..e6f0b8d97 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -1,5 +1,5 @@ /* - * Nextcloud Talk application + * Nextcloud Talk application * * @author Mario Danic * @author Marcel Hibbe @@ -47,6 +47,7 @@ import com.nextcloud.talk.models.json.statuses.StatusesOverall; import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchOverall; import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall; import com.nextcloud.talk.models.json.userprofile.UserProfileOverall; +import com.nextcloud.talk.polls.repositories.model.PollOverall; import java.util.List; import java.util.Map; @@ -526,4 +527,20 @@ public interface NcApi { @Query("from") String fromUrl, @Query("limit") Integer limit, @Query("cursor") Integer cursor); + + @GET + Observable getPoll(@Header("Authorization") String authorization, + @Url String url); + + @POST + Observable createPoll(@Header("Authorization") String authorization, + @Url String url); + + @POST + Observable votePoll(@Header("Authorization") String authorization, + @Url String url); + + @DELETE + Observable closePoll(@Header("Authorization") String authorization, + @Url String url); } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index 15c506670..dbe202e39 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -104,6 +104,7 @@ import com.nextcloud.talk.activities.CallActivity import com.nextcloud.talk.activities.MainActivity import com.nextcloud.talk.activities.TakePhotoActivity import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder +import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder import com.nextcloud.talk.adapters.messages.IncomingVoiceMessageViewHolder import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder @@ -111,6 +112,7 @@ import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder +import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder import com.nextcloud.talk.adapters.messages.PreviewMessageInterface @@ -544,6 +546,17 @@ class ChatController(args: Bundle) : this ) + messageHolders.registerContentType( + CONTENT_TYPE_POLL, + IncomingPollMessageViewHolder::class.java, + profileBottomSheet, + R.layout.item_custom_incoming_poll_message, + OutcomingPollMessageViewHolder::class.java, + null, + R.layout.item_custom_outcoming_poll_message, + this + ) + var senderId = "" if (!conversationUser?.userId.equals("?")) { senderId = "users/" + conversationUser?.userId @@ -3012,6 +3025,7 @@ class ChatController(args: Bundle) : return when (type) { CONTENT_TYPE_LOCATION -> message.hasGeoLocation() CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage + CONTENT_TYPE_POLL -> message.isPoll() CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage) CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1" else -> false @@ -3127,6 +3141,7 @@ class ChatController(args: Bundle) : private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2 private const val CONTENT_TYPE_LOCATION: Byte = 3 private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 4 + private const val CONTENT_TYPE_POLL: Byte = 5 private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200 private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100 private const val LOBBY_TIMER_DELAY: Long = 5000 diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt index 215c3b3f0..faa0f1e50 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.kt @@ -124,6 +124,8 @@ data class ChatMessage( var voiceMessageDownloadProgress: Int = 0, ) : Parcelable, MessageContentType, MessageContentType.Image { + + // TODO: messageTypesToIgnore is weird. must be deleted by refactoring! @JsonIgnore var messageTypesToIgnore = Arrays.asList( MessageType.REGULAR_TEXT_MESSAGE, @@ -132,7 +134,8 @@ data class ChatMessage( MessageType.SINGLE_LINK_AUDIO_MESSAGE, MessageType.SINGLE_LINK_MESSAGE, MessageType.SINGLE_NC_GEOLOCATION_MESSAGE, - MessageType.VOICE_MESSAGE + MessageType.VOICE_MESSAGE, + MessageType.POLL_MESSAGE ) fun hasFileAttachment(): Boolean { @@ -165,6 +168,21 @@ data class ChatMessage( return false } + fun isPoll(): Boolean { + if (messageParameters != null && messageParameters!!.size > 0) { + for ((_, individualHashMap) in messageParameters!!) { + if (MessageDigest.isEqual( + individualHashMap["type"]!!.toByteArray(), + "talk-poll".toByteArray() + ) + ) { + return true + } + } + } + return false + } + override fun getImageUrl(): String? { if (messageParameters != null && messageParameters!!.size > 0) { for ((_, individualHashMap) in messageParameters!!) { @@ -207,6 +225,8 @@ data class ChatMessage( MessageType.SINGLE_NC_ATTACHMENT_MESSAGE } else if (hasGeoLocation()) { MessageType.SINGLE_NC_GEOLOCATION_MESSAGE + } else if (isPoll()) { + MessageType.POLL_MESSAGE } else { MessageType.REGULAR_TEXT_MESSAGE } @@ -334,6 +354,15 @@ data class ChatMessage( getNullsafeActorDisplayName() ) } + } else if (MessageType.POLL_MESSAGE == getCalculateMessageType()) { + return if (actorId == activeUser!!.userId) { + sharedApplication!!.getString(R.string.nc_sent_poll_you) + } else { + String.format( + sharedApplication!!.resources.getString(R.string.nc_sent_an_image), + getNullsafeActorDisplayName() + ) + } } } return "" @@ -410,6 +439,7 @@ data class ChatMessage( SINGLE_LINK_AUDIO_MESSAGE, SINGLE_NC_ATTACHMENT_MESSAGE, SINGLE_NC_GEOLOCATION_MESSAGE, + POLL_MESSAGE, VOICE_MESSAGE } diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/model/Poll.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/Poll.kt new file mode 100644 index 000000000..a61bc2630 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/Poll.kt @@ -0,0 +1,65 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2022 Marcel Hibbe + * + * 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 . + */ +package com.nextcloud.talk.polls.repositories.model + +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 Poll( + @JsonField(name = ["id"]) + var id: Int = 0, + + @JsonField(name = ["question"]) + var question: String? = null, + + @JsonField(name = ["options"]) + var options: ArrayList? = null, + + @JsonField(name = ["votes"]) + var votes: ArrayList? = null, + + @JsonField(name = ["actorType"]) + var actorType: String? = null, + + @JsonField(name = ["actorId"]) + var actorId: String? = null, + + @JsonField(name = ["actorDisplayName"]) + var actorDisplayName: String? = null, + + @JsonField(name = ["status"]) + var status: Int = 0, + + @JsonField(name = ["resultMode"]) + var resultMode: Int = 0, + + @JsonField(name = ["maxVotes"]) + var maxVotes: Int = 0, + + @JsonField(name = ["votedSelf"]) + var votedSelf: ArrayList? = null, +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(0, null, null, null, null, null, null, 0, 0, 0, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOCS.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOCS.kt new file mode 100644 index 000000000..d2bd27b06 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOCS.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2022 Marcel Hibbe + * + * 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 . + */ +package com.nextcloud.talk.polls.repositories.model + +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 PollOCS( + @JsonField(name = ["data"]) + var data: Poll? +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOverall.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOverall.kt new file mode 100644 index 000000000..d5b8fb331 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollOverall.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2022 Marcel Hibbe + * + * 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 . + */ +package com.nextcloud.talk.polls.repositories.model + +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 PollOverall( + @JsonField(name = ["ocs"]) + var ocs: PollOCS? = null +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null) +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 5c999ed0e..d38cbcb12 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -2,8 +2,10 @@ * Nextcloud Talk application * * @author Mario Danic + * @author Marcel Hibbe * @author Tim Krüger * Copyright (C) 2021 Tim Krüger + * Copyright (C) 2021-2022 Marcel Hibbe * Copyright (C) 2017-2018 Mario Danic * * This program is free software: you can redistribute it and/or modify @@ -61,8 +63,8 @@ public class ApiUtils { } /** - * @deprecated This is only supported on API v1-3, in API v4+ please use - * {@link ApiUtils#getUrlForAttendees(int, String, String)} instead. + * @deprecated This is only supported on API v1-3, in API v4+ please use {@link ApiUtils#getUrlForAttendees(int, + * String, String)} instead. */ @Deprecated public static String getUrlForRemovingParticipantFromConversation(String baseUrl, String roomToken, boolean isGuest) { @@ -95,13 +97,13 @@ public class ApiUtils { public static String getUrlForFilePreviewWithRemotePath(String baseUrl, String remotePath, int px) { return baseUrl + "/index.php/core/preview.png?file=" - + Uri.encode(remotePath, "UTF-8") - + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1"; + + Uri.encode(remotePath, "UTF-8") + + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1"; } public static String getUrlForFilePreviewWithFileId(String baseUrl, String fileId, int px) { return baseUrl + "/index.php/core/preview?fileId=" - + fileId + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1"; + + fileId + "&x=" + px + "&y=" + px + "&a=1&mode=cover&forceIcon=1"; } public static String getSharingUrl(String baseUrl) { @@ -151,8 +153,8 @@ public class ApiUtils { if (user.hasSpreedFeatureCapability("conversation-v2")) { return version; } - if (version == APIv1 && - user.hasSpreedFeatureCapability("mention-flag") && + if (version == APIv1 && + user.hasSpreedFeatureCapability( "mention-flag") && !user.hasSpreedFeatureCapability("conversation-v4")) { return version; } @@ -238,7 +240,7 @@ public class ApiUtils { } public static String getUrlForParticipants(int version, String baseUrl, String token) { - if (token == null || token.isEmpty()){ + if (token == null || token.isEmpty()) { Log.e(TAG, "token was null or empty"); } return getUrlForRoom(version, baseUrl, token) + "/participants"; @@ -287,6 +289,7 @@ public class ApiUtils { public static String getUrlForCall(int version, String baseUrl, String token) { return getUrlForApi(version, baseUrl) + "/call/" + token; } + public static String getUrlForChat(int version, String baseUrl, String token) { return getUrlForApi(version, baseUrl) + "/chat/" + token; } @@ -294,10 +297,11 @@ public class ApiUtils { public static String getUrlForMentionSuggestions(int version, String baseUrl, String token) { return getUrlForChat(version, baseUrl, token) + "/mentions"; } + public static String getUrlForChatMessage(int version, String baseUrl, String token, String messageId) { return getUrlForChat(version, baseUrl, token) + "/" + messageId; } - + public static String getUrlForChatSharedItems(int version, String baseUrl, String token) { return getUrlForChat(version, baseUrl, token) + "/share"; } @@ -366,11 +370,11 @@ public class ApiUtils { } public static RetrofitBucket getRetrofitBucketForAddParticipantWithSource( - int version, - String baseUrl, - String token, - String source, - String id + int version, + String baseUrl, + String token, + String source, + String id ) { RetrofitBucket retrofitBucket = getRetrofitBucketForAddParticipant(version, baseUrl, token, id); retrofitBucket.getQueryMap().put("source", source); @@ -417,7 +421,7 @@ public class ApiUtils { public static String getUrlPushProxy() { return NextcloudTalkApplication.Companion.getSharedApplication(). - getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices"; + getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices"; } public static String getUrlForNotificationWithId(String baseUrl, String notificationId) { @@ -448,8 +452,10 @@ public class ApiUtils { return getUrlForChat(version, baseUrl, roomToken) + "/share"; } - public static String getUrlForHoverCard(String baseUrl, String userId) { return baseUrl + ocsApiVersion + - "/hovercard/v1/" + userId; } + public static String getUrlForHoverCard(String baseUrl, String userId) { + return baseUrl + ocsApiVersion + + "/hovercard/v1/" + userId; + } public static String getUrlForSetChatReadMarker(int version, String baseUrl, String roomToken) { return getUrlForChat(version, baseUrl, roomToken) + "/read"; @@ -497,4 +503,16 @@ public class ApiUtils { public static String getUrlForUnifiedSearch(@NotNull String baseUrl, @NotNull String providerId) { return baseUrl + ocsApiVersion + "/search/providers/" + providerId + "/search"; } + + public static String getUrlForPoll(String baseUrl, + String roomToken, + String pollId) { + return getUrlForPoll(baseUrl, roomToken) + "/" + pollId; + } + + public static String getUrlForPoll(String baseUrl, + String roomToken) { + return baseUrl + ocsApiVersion + spreedApiVersion + "/poll/" + roomToken; + } + } diff --git a/app/src/main/res/drawable/ic_baseline_bar_chart_24.xml b/app/src/main/res/drawable/ic_baseline_bar_chart_24.xml new file mode 100644 index 000000000..b84f89296 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_bar_chart_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/item_custom_incoming_poll_message.xml b/app/src/main/res/layout/item_custom_incoming_poll_message.xml new file mode 100644 index 000000000..7f198bd15 --- /dev/null +++ b/app/src/main/res/layout/item_custom_incoming_poll_message.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_outcoming_poll_message.xml b/app/src/main/res/layout/item_custom_outcoming_poll_message.xml new file mode 100644 index 000000000..0a52ee34b --- /dev/null +++ b/app/src/main/res/layout/item_custom_outcoming_poll_message.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f6acc81cd..5ef3f0bd7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -309,6 +309,7 @@ %1$s sent an audio. %1$s sent a video. %1$s sent an image. + %1$s sent a poll. %1$s sent a location. %1$s sent a voice message. You sent a link. @@ -317,6 +318,7 @@ You sent an audio. You sent a video. You sent an image. + You sent a poll. You sent a location. You sent a voice message. %1$s: %2$s @@ -435,6 +437,10 @@ Play/pause voice message Permission for audio recording is required + + Tap to vote + Tap to see results + phone_book_integration Match contacts based on phone number to integrate Talk shortcut into system contacts app @@ -534,4 +540,5 @@ Call without notification Set avatar from camera +