Browse Source

add data classes for polls (WIP)

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 2 years ago
parent
commit
f2025332ab

+ 241 - 0
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 <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.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<ChatMessage>(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<String?, String?> = 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<Drawable>(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
+    }
+}

+ 175 - 0
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 <mario@lovelyhq.com>
+ * Copyright (C) 2021 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.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<ChatMessage>(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
+    }
+}

+ 18 - 1
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<PollOverall> getPoll(@Header("Authorization") String authorization,
+                                    @Url String url);
+
+    @POST
+    Observable<PollOverall> createPoll(@Header("Authorization") String authorization,
+                                       @Url String url);
+
+    @POST
+    Observable<PollOverall> votePoll(@Header("Authorization") String authorization,
+                                     @Url String url);
+
+    @DELETE
+    Observable<PollOverall> closePoll(@Header("Authorization") String authorization,
+                                      @Url String url);
 }

+ 15 - 0
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

+ 31 - 1
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
     }
 

+ 65 - 0
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 <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.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<String>? = null,
+
+    @JsonField(name = ["votes"])
+    var votes: ArrayList<Int>? = 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<Int>? = 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)
+}

+ 35 - 0
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 <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.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)
+}

+ 35 - 0
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 <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.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)
+}

+ 35 - 17
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 <t@timkrueger.me>
+ * Copyright (C) 2021-2022 Marcel Hibbe <dev@mhibbe.de>
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
  * 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;
+    }
+
 }

+ 10 - 0
app/src/main/res/drawable/ic_baseline_bar_chart_24.xml

@@ -0,0 +1,10 @@
+<vector android:height="24dp"
+    android:tint="#000000"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M5,9.2h3L8,19L5,19zM10.6,5h2.8v14h-2.8zM16.2,13L19,13v6h-2.8z" />
+</vector>

+ 109 - 0
app/src/main/res/layout/item_custom_incoming_poll_message.xml

@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2021 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/>.
+  -->
+
+<RelativeLayout 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:layout_marginLeft="16dp"
+    android:layout_marginTop="2dp"
+    android:layout_marginRight="16dp"
+    android:layout_marginBottom="2dp">
+
+    <com.facebook.drawee.view.SimpleDraweeView
+        android:id="@id/messageUserAvatar"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_alignParentTop="true"
+        android:layout_marginEnd="8dp"
+        app:roundAsCircle="true" />
+
+    <com.google.android.flexbox.FlexboxLayout
+        android:id="@id/bubble"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginEnd="@dimen/message_incoming_bubble_margin_right"
+        android:layout_toEndOf="@id/messageUserAvatar"
+        android:orientation="vertical"
+        app:alignContent="stretch"
+        app:alignItems="stretch"
+        app:flexWrap="wrap"
+        app:justifyContent="flex_end">
+
+        <include
+            android:id="@+id/message_quote"
+            layout="@layout/item_message_quote"
+            android:visibility="gone" />
+
+        <androidx.emoji.widget.EmojiTextView
+            android:id="@+id/messageAuthor"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="4dp"
+            android:textAlignment="viewStart"
+            android:textColor="@color/textColorMaxContrast"
+            android:textSize="12sp" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:gravity="center_vertical">
+
+            <ImageView
+                android:id="@+id/message_poll_icon"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@null"
+                android:src="@drawable/ic_baseline_bar_chart_24"
+                app:tint="@color/high_emphasis_menu_icon"></ImageView>
+
+            <androidx.emoji.widget.EmojiTextView
+                android:id="@+id/message_poll_title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:textAlignment="viewStart"
+                android:textStyle="bold"
+                tools:text="This is the poll title?" />
+
+        </LinearLayout>
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/double_margin_between_elements"
+            android:text="@string/message_poll_tap_to_vote"></TextView>
+
+        <TextView
+            android:id="@id/messageTime"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageText"
+            android:layout_marginStart="8dp"
+            app:layout_alignSelf="center"
+            tools:text="12:38" />
+
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
+
+    </com.google.android.flexbox.FlexboxLayout>
+</RelativeLayout>

+ 78 - 0
app/src/main/res/layout/item_custom_outcoming_poll_message.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2021 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/>.
+  -->
+
+<RelativeLayout 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:layout_marginLeft="16dp"
+    android:layout_marginTop="2dp"
+    android:layout_marginRight="16dp"
+    android:layout_marginBottom="2dp">
+
+    <com.google.android.flexbox.FlexboxLayout
+        android:id="@id/bubble"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_alignParentEnd="true"
+        android:layout_marginStart="@dimen/message_outcoming_bubble_margin_left"
+        app:alignContent="stretch"
+        app:alignItems="stretch"
+        app:flexWrap="wrap"
+        app:justifyContent="flex_end">
+
+        <include
+            android:id="@+id/message_quote"
+            layout="@layout/item_message_quote"
+            android:visibility="gone" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:gravity="center_vertical">
+
+        </LinearLayout>
+
+        <TextView
+            android:id="@id/messageTime"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageText"
+            android:layout_marginStart="8dp"
+            app:layout_alignSelf="center"
+            tools:text="10:35" />
+
+        <ImageView
+            android:id="@+id/checkMark"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/messageTime"
+            android:layout_marginStart="8dp"
+            app:layout_alignSelf="center"
+            android:contentDescription="@null" />
+
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
+
+    </com.google.android.flexbox.FlexboxLayout>
+</RelativeLayout>

+ 7 - 0
app/src/main/res/values/strings.xml

@@ -309,6 +309,7 @@
     <string name="nc_sent_an_audio" formatted="true">%1$s sent an audio.</string>
     <string name="nc_sent_a_video" formatted="true">%1$s sent a video.</string>
     <string name="nc_sent_an_image" formatted="true">%1$s sent an image.</string>
+    <string name="nc_sent_poll" formatted="true">%1$s sent a poll.</string>
     <string name="nc_sent_location" formatted="true">%1$s sent a location.</string>
     <string name="nc_sent_voice" formatted="true">%1$s sent a voice message.</string>
     <string name="nc_sent_a_link_you">You sent a link.</string>
@@ -317,6 +318,7 @@
     <string name="nc_sent_an_audio_you">You sent an audio.</string>
     <string name="nc_sent_a_video_you">You sent a video.</string>
     <string name="nc_sent_an_image_you">You sent an image.</string>
+    <string name="nc_sent_poll_you">You sent a poll.</string>
     <string name="nc_sent_location_you">You sent a location.</string>
     <string name="nc_sent_voice_you">You sent a voice message.</string>
     <string name="nc_formatted_message" translatable="false">%1$s: %2$s</string>
@@ -435,6 +437,10 @@
     <string name="play_pause_voice_message">Play/pause voice message</string>
     <string name="nc_voice_message_missing_audio_permission">Permission for audio recording is required</string>
 
+    <!-- polls -->
+    <string name="message_poll_tap_to_vote">Tap to vote</string>
+    <string name="message_poll_tap_see_results">Tap to see results</string>
+
     <!-- Phonebook Integration -->
     <string name="nc_settings_phone_book_integration_key" translatable="false">phone_book_integration</string>
     <string name="nc_settings_phone_book_integration_desc">Match contacts based on phone number to integrate Talk shortcut into system contacts app</string>
@@ -534,4 +540,5 @@
     <string name="call_without_notification">Call without notification</string>
     <string name="set_avatar_from_camera">Set avatar from camera</string>
 
+
 </resources>