diff options
Diffstat (limited to 'app/src')
65 files changed, 4233 insertions, 64 deletions
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt index 43d163878..2e9e723d4 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt @@ -50,7 +50,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding 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.UriUtils @@ -117,7 +116,7 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess if (!TextUtils.isEmpty(author)) { binding.messageAuthor.text = author binding.messageUserAvatar.setOnClickListener { - (payload as? ProfileBottomSheet)?.showFor(message.actorId!!, itemView.context) + (payload as? MessagePayload)?.profileBottomSheet?.showFor(message.actorId!!, itemView.context) } } else { binding.messageAuthor.setText(R.string.nc_nick_guest) 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..177882d44 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPollMessageViewHolder.kt @@ -0,0 +1,248 @@ +/* + * 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.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.activities.MainActivity +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.polls.ui.PollMainDialogFragment +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) + + @Inject + lateinit var context: Context + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var ncApi: NcApi + + 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? = null + var pollName: String? = null + + 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"].toString() + } + } + } + + if (pollId != null && pollName != null) { + binding.messagePollTitle.text = pollName + + val roomToken = (payload as? MessagePayload)!!.currentConversation.token!! + val isOwnerOrModerator = (payload as? MessagePayload)!!.currentConversation.isParticipantOwnerOrModerator + + binding.bubble.setOnClickListener { + val pollVoteDialog = PollMainDialogFragment.newInstance( + message.activeUser!!, + roomToken, + isOwnerOrModerator, + pollId, + pollName + ) + pollVoteDialog.show( + (binding.messagePollIcon.context as MainActivity).supportFragmentManager, + TAG + ) + } + } + } + + private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) { + val author: String = message.actorDisplayName!! + if (!TextUtils.isEmpty(author)) { + binding.messageAuthor.text = author + binding.messageUserAvatar.setOnClickListener { + (payload as? MessagePayload)?.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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt index a7e904da4..e06604763 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt @@ -48,7 +48,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding 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 @@ -210,7 +209,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message if (!TextUtils.isEmpty(author)) { binding.messageAuthor.text = author binding.messageUserAvatar.setOnClickListener { - (payload as? ProfileBottomSheet)?.showFor(message.actorId!!, itemView.context) + (payload as? MessagePayload)?.profileBottomSheet?.showFor(message.actorId!!, itemView.context) } } else { binding.messageAuthor.setText(R.string.nc_nick_guest) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt index d098ba697..95152a920 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt @@ -47,14 +47,12 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding import com.nextcloud.talk.models.json.chat.ChatMessage -import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.TextMatchers import com.nextcloud.talk.utils.preferences.AppPreferences import com.stfalcon.chatkit.messages.MessageHolders -import java.util.HashMap import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -136,7 +134,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message if (!TextUtils.isEmpty(message.actorDisplayName)) { binding.messageAuthor.text = message.actorDisplayName binding.messageUserAvatar.setOnClickListener { - (payload as? ProfileBottomSheet)?.showFor(message.actorId!!, itemView.context) + (payload as? MessagePayload)?.profileBottomSheet?.showFor(message.actorId!!, itemView.context) } } else { binding.messageAuthor.setText(R.string.nc_nick_guest) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java index 0732e3341..48e33915e 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java @@ -49,7 +49,6 @@ import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation; import com.nextcloud.talk.data.user.model.User; import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding; import com.nextcloud.talk.models.json.chat.ChatMessage; -import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet; import com.nextcloud.talk.utils.DisplayUtils; import com.nextcloud.talk.utils.DrawableUtils; import com.nextcloud.talk.utils.FileViewerUtils; @@ -125,8 +124,9 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom } else { userAvatar.setVisibility(View.VISIBLE); userAvatar.setOnClickListener(v -> { - if (payload instanceof ProfileBottomSheet) { - ((ProfileBottomSheet) payload).showFor(message.getActorId(), v.getContext()); + if (payload instanceof MessagePayload) { + ((MessagePayload) payload).getProfileBottomSheet().showFor(message.getActorId(), + v.getContext()); } }); diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MessagePayload.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MessagePayload.kt new file mode 100644 index 000000000..df6d22a8f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MessagePayload.kt @@ -0,0 +1,29 @@ +/* + * 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.adapters.messages + +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet + +data class MessagePayload( + var currentConversation: Conversation, + val profileBottomSheet: ProfileBottomSheet +) 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..8f865cccc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPollMessageViewHolder.kt @@ -0,0 +1,214 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2017-2018 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/>. + */ + +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.PorterDuff +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.activities.MainActivity +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.ItemCustomOutcomingPollMessageBinding +import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.polls.ui.PollMainDialogFragment +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, payload: Any) : MessageHolders +.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView, payload) { + + private val binding: ItemCustomOutcomingPollMessageBinding = + ItemCustomOutcomingPollMessageBinding.bind(itemView) + + @Inject + lateinit var context: Context + + @Inject + lateinit var appPreferences: AppPreferences + + @Inject + lateinit var ncApi: NcApi + + 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) + + 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.contentDescription = readStatusContentDescriptionString + + setPollPreview(message) + + 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 setPollPreview(message: ChatMessage) { + var pollId: String? = null + var pollName: String? = null + + 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"].toString() + } + } + } + + if (pollId != null && pollName != null) { + binding.messagePollTitle.text = pollName + + val roomToken = (payload as? MessagePayload)!!.currentConversation.token!! + val isOwnerOrModerator = (payload as? MessagePayload)!!.currentConversation.isParticipantOwnerOrModerator + + binding.bubble.setOnClickListener { + val pollVoteDialog = PollMainDialogFragment.newInstance( + message.activeUser!!, + roomToken, + isOwnerOrModerator, + pollId, + pollName + ) + pollVoteDialog.show( + (binding.messagePollIcon.context as MainActivity).supportFragmentManager, + TAG + ) + } + } + } + + 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..5a531f5c7 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,27 @@ 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); + + @FormUrlEncoded + @POST + Observable<PollOverall> createPoll(@Header("Authorization") String authorization, + @Url String url, + @Query("question") String question, + @Field("options[]") List<String> options, + @Query("resultMode") Integer resultMode, + @Query("maxVotes") Integer maxVotes); + + @FormUrlEncoded + @POST + Observable<PollOverall> votePoll(@Header("Authorization") String authorization, + @Url String url, + @Field("optionIds[]") List<Integer> optionIds); + + @DELETE + Observable<PollOverall> 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..48a7fc96e 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -104,13 +104,16 @@ 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 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.MessagePayload 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 @@ -139,6 +142,7 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.conversations.RoomsOverall import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.mention.Mention +import com.nextcloud.talk.polls.ui.PollCreateDialogFragment import com.nextcloud.talk.presenters.MentionAutocompletePresenter import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity import com.nextcloud.talk.shareditems.activities.SharedItemsActivity @@ -483,10 +487,12 @@ class ChatController(args: Bundle) : val messageHolders = MessageHolders() val profileBottomSheet = ProfileBottomSheet(ncApi!!, conversationUser!!, router) + val payload = MessagePayload(currentConversation!!, profileBottomSheet) + messageHolders.setIncomingTextConfig( MagicIncomingTextMessageViewHolder::class.java, R.layout.item_custom_incoming_text_message, - profileBottomSheet + payload ) messageHolders.setOutcomingTextConfig( MagicOutcomingTextMessageViewHolder::class.java, @@ -496,7 +502,7 @@ class ChatController(args: Bundle) : messageHolders.setIncomingImageConfig( IncomingPreviewMessageViewHolder::class.java, R.layout.item_custom_incoming_preview_message, - profileBottomSheet + payload ) messageHolders.setOutcomingImageConfig( @@ -525,7 +531,7 @@ class ChatController(args: Bundle) : messageHolders.registerContentType( CONTENT_TYPE_LOCATION, IncomingLocationMessageViewHolder::class.java, - profileBottomSheet, + payload, R.layout.item_custom_incoming_location_message, OutcomingLocationMessageViewHolder::class.java, null, @@ -536,7 +542,7 @@ class ChatController(args: Bundle) : messageHolders.registerContentType( CONTENT_TYPE_VOICE_MESSAGE, IncomingVoiceMessageViewHolder::class.java, - profileBottomSheet, + payload, R.layout.item_custom_incoming_voice_message, OutcomingVoiceMessageViewHolder::class.java, null, @@ -544,6 +550,17 @@ class ChatController(args: Bundle) : this ) + messageHolders.registerContentType( + CONTENT_TYPE_POLL, + IncomingPollMessageViewHolder::class.java, + payload, + R.layout.item_custom_incoming_poll_message, + OutcomingPollMessageViewHolder::class.java, + payload, + R.layout.item_custom_outcoming_poll_message, + this + ) + var senderId = "" if (!conversationUser?.userId.equals("?")) { senderId = "users/" + conversationUser?.userId @@ -2576,6 +2593,11 @@ class ChatController(args: Bundle) : chatMessageIterator.remove() } + + // delete poll system messages + else if (isPollVotedMessage(currentMessage)) { + chatMessageIterator.remove() + } } return chatMessageMap.values.toList() } @@ -2591,6 +2613,10 @@ class ChatController(args: Bundle) : currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED } + private fun isPollVotedMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean { + return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.POLL_VOTED + } + private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) { if (currentConversation?.canStartCall == false && currentConversation?.hasCall == false) { Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show() @@ -3012,6 +3038,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 @@ -3121,12 +3148,23 @@ class ChatController(args: Bundle) : } } + fun createPoll() { + val pollVoteDialog = PollCreateDialogFragment.newInstance( + roomToken!! + ) + pollVoteDialog.show( + (activity as MainActivity?)!!.supportFragmentManager, + TAG + ) + } + companion object { private const val TAG = "ChatController" private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1 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/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index bb8a5f255..47e3747b3 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -29,6 +29,8 @@ import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository import com.nextcloud.talk.data.storage.ArbitraryStoragesRepositoryImpl import com.nextcloud.talk.data.user.UsersRepository import com.nextcloud.talk.data.user.UsersRepositoryImpl +import com.nextcloud.talk.polls.repositories.PollRepository +import com.nextcloud.talk.polls.repositories.PollRepositoryImpl import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepository import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepositoryImpl import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository @@ -36,6 +38,7 @@ import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepositoryImpl import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository import com.nextcloud.talk.shareditems.repositories.SharedItemsRepositoryImpl import com.nextcloud.talk.utils.database.user.CurrentUserProvider +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import dagger.Module import dagger.Provides import okhttp3.OkHttpClient @@ -53,6 +56,11 @@ class RepositoryModule { } @Provides + fun provideDialogPollRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): PollRepository { + return PollRepositoryImpl(ncApi, userProvider) + } + + @Provides fun provideRemoteFileBrowserItemsRepository(okHttpClient: OkHttpClient, userProvider: CurrentUserProvider): RemoteFileBrowserItemsRepository { return RemoteFileBrowserItemsRepositoryImpl(okHttpClient, userProvider) diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt index f2356d0ab..3d8ee7535 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -25,6 +25,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel import com.nextcloud.talk.messagesearch.MessageSearchViewModel +import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel +import com.nextcloud.talk.polls.viewmodels.PollMainViewModel +import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel +import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel import dagger.Binds import dagger.MapKey @@ -63,6 +67,26 @@ abstract class ViewModelModule { @Binds @IntoMap + @ViewModelKey(PollMainViewModel::class) + abstract fun pollViewModel(viewModel: PollMainViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(PollVoteViewModel::class) + abstract fun pollVoteViewModel(viewModel: PollVoteViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(PollResultsViewModel::class) + abstract fun pollResultsViewModel(viewModel: PollResultsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(PollCreateViewModel::class) + abstract fun pollCreateViewModel(viewModel: PollCreateViewModel): ViewModel + + @Binds + @IntoMap @ViewModelKey(RemoteFileBrowserItemsViewModel::class) abstract fun remoteFileBrowserItemsViewModel(viewModel: RemoteFileBrowserItemsViewModel): ViewModel } 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..1d68ce625 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 { + + // 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 } @@ -460,7 +490,9 @@ data class ChatMessage( CLEARED_CHAT, REACTION, REACTION_DELETED, - REACTION_REVOKED + REACTION_REVOKED, + POLL_VOTED, + POLL_CLOSED } companion object { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt index caa75ec2b..20e525110 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt @@ -65,6 +65,8 @@ 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.POLL_CLOSED +import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.POLL_VOTED 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 @@ -167,6 +169,8 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst "reaction" -> REACTION "reaction_deleted" -> REACTION_DELETED "reaction_revoked" -> REACTION_REVOKED + "poll_voted" -> POLL_VOTED + "poll_closed" -> POLL_CLOSED else -> DUMMY } } @@ -224,6 +228,8 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst REACTION -> return "reaction" REACTION_DELETED -> return "reaction_deleted" REACTION_REVOKED -> return "reaction_revoked" + POLL_VOTED -> return "poll_voted" + POLL_CLOSED -> return "poll_closed" else -> return "" } } diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionItem.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionItem.kt new file mode 100644 index 000000000..2f4217c8f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionItem.kt @@ -0,0 +1,25 @@ +/* + * 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.adapters + +class PollCreateOptionItem( + var pollOption: String +) diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt new file mode 100644 index 000000000..6d40969dc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionViewHolder.kt @@ -0,0 +1,82 @@ +/* + * 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.adapters + +import android.annotation.SuppressLint +import android.text.Editable +import android.text.TextWatcher +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.databinding.PollCreateOptionsItemBinding +import com.nextcloud.talk.utils.EmojiTextInputEditText + +class PollCreateOptionViewHolder( + private val binding: PollCreateOptionsItemBinding +) : RecyclerView.ViewHolder(binding.root) { + + lateinit var optionText: EmojiTextInputEditText + private var textListener: TextWatcher? = null + + @SuppressLint("SetTextI18n") + fun bind( + pollCreateOptionItem: PollCreateOptionItem, + itemsListener: PollCreateOptionsItemListener, + position: Int, + focus: Boolean + ) { + + textListener?.let { + binding.pollOptionText.removeTextChangedListener(it) + } + + binding.pollOptionText.setText(pollCreateOptionItem.pollOption) + + if (focus) { + itemsListener.requestFocus(binding.pollOptionText) + } + + binding.pollOptionDelete.setOnClickListener { + itemsListener.onRemoveOptionsItemClick(pollCreateOptionItem, position) + } + + textListener = getTextWatcher(pollCreateOptionItem, itemsListener) + binding.pollOptionText.addTextChangedListener(textListener) + } + + private fun getTextWatcher( + pollCreateOptionItem: PollCreateOptionItem, + itemsListener: PollCreateOptionsItemListener + ) = + object : TextWatcher { + override fun afterTextChanged(s: Editable) { + // unused atm + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + // unused atm + } + + override fun onTextChanged(option: CharSequence, start: Int, before: Int, count: Int) { + pollCreateOptionItem.pollOption = option.toString() + + itemsListener.onOptionsItemTextChanged(pollCreateOptionItem) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt new file mode 100644 index 000000000..39c8f7d7f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsAdapter.kt @@ -0,0 +1,59 @@ +/* + * 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.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.databinding.PollCreateOptionsItemBinding + +class PollCreateOptionsAdapter( + private val clickListener: PollCreateOptionsItemListener +) : RecyclerView.Adapter<PollCreateOptionViewHolder>() { + + internal var list: ArrayList<PollCreateOptionItem> = ArrayList<PollCreateOptionItem>() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollCreateOptionViewHolder { + val itemBinding = PollCreateOptionsItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + + return PollCreateOptionViewHolder(itemBinding) + } + + override fun onBindViewHolder(holder: PollCreateOptionViewHolder, position: Int) { + val currentItem = list[position] + var focus = false + + if (list.size - 1 == position && currentItem.pollOption.isBlank()) { + focus = true + } + + holder.bind(currentItem, clickListener, position, focus) + } + + override fun getItemCount(): Int { + return list.size + } + + fun updateOptionsList(optionsList: ArrayList<PollCreateOptionItem>) { + list = optionsList + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsItemListener.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsItemListener.kt new file mode 100644 index 000000000..a057592bd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollCreateOptionsItemListener.kt @@ -0,0 +1,32 @@ +/* + * 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.adapters + +import android.widget.EditText + +interface PollCreateOptionsItemListener { + + fun onRemoveOptionsItemClick(pollCreateOptionItem: PollCreateOptionItem, position: Int) + + fun onOptionsItemTextChanged(pollCreateOptionItem: PollCreateOptionItem) + + fun requestFocus(textField: EditText) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderItem.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderItem.kt new file mode 100644 index 000000000..b8f14bc90 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderItem.kt @@ -0,0 +1,39 @@ +/* + * 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.adapters + +import com.nextcloud.talk.R + +data class PollResultHeaderItem( + val name: String, + val percent: Int, + val selfVoted: Boolean +) : PollResultItem { + + override fun getViewType(): Int { + return VIEW_TYPE + } + + companion object { + // layout is used as view type for uniqueness + const val VIEW_TYPE: Int = R.layout.poll_result_header_item + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderViewHolder.kt new file mode 100644 index 000000000..b963f515d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultHeaderViewHolder.kt @@ -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.polls.adapters + +import android.annotation.SuppressLint +import android.graphics.Typeface +import com.nextcloud.talk.databinding.PollResultHeaderItemBinding + +class PollResultHeaderViewHolder( + override val binding: PollResultHeaderItemBinding +) : PollResultViewHolder(binding) { + + @SuppressLint("SetTextI18n") + override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) { + val item = pollResultItem as PollResultHeaderItem + + binding.root.setOnClickListener { clickListener.onClick() } + + binding.pollOptionText.text = item.name + binding.pollOptionPercentText.text = "${item.percent}%" + + if (item.selfVoted) { + binding.pollOptionText.setTypeface(null, Typeface.BOLD) + binding.pollOptionPercentText.setTypeface(null, Typeface.BOLD) + } + + binding.pollOptionBar.progress = item.percent + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt new file mode 100644 index 000000000..d4d97fd77 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt @@ -0,0 +1,25 @@ +/* + * 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.adapters + +interface PollResultItem { + fun getViewType(): Int +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt new file mode 100644 index 000000000..d0a15156e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt @@ -0,0 +1,25 @@ +/* + * 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.adapters + +interface PollResultItemClickListener { + fun onClick() +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt new file mode 100644 index 000000000..e8da7ceee --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt @@ -0,0 +1,30 @@ +/* + * 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.adapters + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +abstract class PollResultViewHolder( + open val binding: ViewBinding +) : RecyclerView.ViewHolder(binding.root) { + abstract fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterItem.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterItem.kt new file mode 100644 index 000000000..3512b4c88 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterItem.kt @@ -0,0 +1,38 @@ +/* + * 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.adapters + +import com.nextcloud.talk.R +import com.nextcloud.talk.polls.model.PollDetails + +data class PollResultVoterItem( + val details: PollDetails +) : PollResultItem { + + override fun getViewType(): Int { + return VIEW_TYPE + } + + companion object { + // layout is used as view type for uniqueness + const val VIEW_TYPE: Int = R.layout.poll_result_voter_item + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt new file mode 100644 index 000000000..543591302 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVoterViewHolder.kt @@ -0,0 +1,87 @@ +/* + * 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.adapters + +import android.annotation.SuppressLint +import android.text.TextUtils +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.drawee.interfaces.DraweeController +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.PollResultVoterItemBinding +import com.nextcloud.talk.polls.model.PollDetails +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DisplayUtils + +class PollResultVoterViewHolder( + private val user: User, + override val binding: PollResultVoterItemBinding +) : PollResultViewHolder(binding) { + + @SuppressLint("SetTextI18n") + override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) { + val item = pollResultItem as PollResultVoterItem + + binding.root.setOnClickListener { clickListener.onClick() } + + binding.pollVoterName.text = item.details.actorDisplayName + binding.pollVoterAvatar.controller = getAvatarDraweeController(item.details) + } + + private fun getAvatarDraweeController(pollDetail: PollDetails): DraweeController? { + var draweeController: DraweeController? = null + if (pollDetail.actorType == "guests") { + var displayName = NextcloudTalkApplication.sharedApplication?.resources?.getString(R.string.nc_guest) + if (!TextUtils.isEmpty(pollDetail.actorDisplayName)) { + displayName = pollDetail.actorDisplayName!! + } + draweeController = Fresco.newDraweeControllerBuilder() + .setAutoPlayAnimations(true) + .setImageRequest( + DisplayUtils.getImageRequestForUrl( + ApiUtils.getUrlForGuestAvatar( + user.baseUrl, + displayName, + false + ), + user + ) + ) + .build() + } else if (pollDetail.actorType == "users") { + draweeController = Fresco.newDraweeControllerBuilder() + .setAutoPlayAnimations(true) + .setImageRequest( + DisplayUtils.getImageRequestForUrl( + ApiUtils.getUrlForAvatar( + user.baseUrl, + pollDetail.actorId, + false + ), + user + ) + ) + .build() + } + return draweeController + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewItem.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewItem.kt new file mode 100644 index 000000000..ffb3065f3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewItem.kt @@ -0,0 +1,38 @@ +/* + * 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.adapters + +import com.nextcloud.talk.R +import com.nextcloud.talk.polls.model.PollDetails + +data class PollResultVotersOverviewItem( + val detailsList: List<PollDetails> +) : PollResultItem { + + override fun getViewType(): Int { + return VIEW_TYPE + } + + companion object { + // layout is used as view type for uniqueness + const val VIEW_TYPE: Int = R.layout.poll_result_voters_overview_item + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt new file mode 100644 index 000000000..c3805afe1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultVotersOverviewViewHolder.kt @@ -0,0 +1,141 @@ +/* + * 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.adapters + +import android.annotation.SuppressLint +import android.text.TextUtils +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.drawee.generic.RoundingParams +import com.facebook.drawee.interfaces.DraweeController +import com.facebook.drawee.view.SimpleDraweeView +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.PollResultVotersOverviewItemBinding +import com.nextcloud.talk.polls.model.PollDetails +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DisplayUtils + +class PollResultVotersOverviewViewHolder( + private val user: User, + override val binding: PollResultVotersOverviewItemBinding +) : PollResultViewHolder(binding) { + + @SuppressLint("SetTextI18n") + override fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) { + val item = pollResultItem as PollResultVotersOverviewItem + + binding.root.setOnClickListener { clickListener.onClick() } + + val layoutParams = LinearLayout.LayoutParams( + AVATAR_SIZE, + AVATAR_SIZE + ) + + var avatarsToDisplay = MAX_AVATARS + if (item.detailsList.size < avatarsToDisplay) { + avatarsToDisplay = item.detailsList.size + } + val shotsDots = item.detailsList.size > avatarsToDisplay + + for (i in 0 until avatarsToDisplay) { + val pollDetails = item.detailsList[i] + val avatar = SimpleDraweeView(binding.root.context) + + layoutParams.marginStart = i * AVATAR_OFFSET + avatar.layoutParams = layoutParams + + avatar.translationZ = i.toFloat() * -1 + + val roundingParams = RoundingParams.fromCornersRadius(AVATAR_RADIUS) + roundingParams.roundAsCircle = true + roundingParams.borderColor = ResourcesCompat.getColor( + itemView.context.resources!!, + R.color.colorPrimary, + null + ) + roundingParams.borderWidth = 2.0f + + avatar.hierarchy.roundingParams = roundingParams + avatar.controller = getAvatarDraweeController(pollDetails) + + binding.votersAvatarsOverviewWrapper.addView(avatar) + + if (i == avatarsToDisplay - 1 && shotsDots) { + val dotsView = TextView(itemView.context) + layoutParams.marginStart = i * AVATAR_OFFSET + DOTS_OFFSET + dotsView.layoutParams = layoutParams + dotsView.text = DOTS_TEXT + binding.votersAvatarsOverviewWrapper.addView(dotsView) + } + } + } + + private fun getAvatarDraweeController(pollDetail: PollDetails): DraweeController? { + var draweeController: DraweeController? = null + if (pollDetail.actorType == "guests") { + var displayName = NextcloudTalkApplication.sharedApplication?.resources?.getString(R.string.nc_guest) + if (!TextUtils.isEmpty(pollDetail.actorDisplayName)) { + displayName = pollDetail.actorDisplayName!! + } + draweeController = Fresco.newDraweeControllerBuilder() + .setAutoPlayAnimations(true) + .setImageRequest( + DisplayUtils.getImageRequestForUrl( + ApiUtils.getUrlForGuestAvatar( + user.baseUrl, + displayName, + false + ), + user + ) + ) + .build() + } else if (pollDetail.actorType == "users") { + draweeController = Fresco.newDraweeControllerBuilder() + .setAutoPlayAnimations(true) + .setImageRequest( + DisplayUtils.getImageRequestForUrl( + ApiUtils.getUrlForAvatar( + user.baseUrl, + pollDetail.actorId, + false + ), + user + ) + ) + .build() + } + return draweeController + } + + companion object { + const val AVATAR_SIZE = 60 + const val AVATAR_RADIUS = 5f + const val MAX_AVATARS = 10 + const val AVATAR_OFFSET = AVATAR_SIZE - 10 + const val DOTS_OFFSET = 70 + const val DOTS_TEXT = "…" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt new file mode 100644 index 000000000..98a576cba --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt @@ -0,0 +1,90 @@ +/* + * 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.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.PollResultHeaderItemBinding +import com.nextcloud.talk.databinding.PollResultVoterItemBinding +import com.nextcloud.talk.databinding.PollResultVotersOverviewItemBinding + +class PollResultsAdapter( + private val user: User, + private val clickListener: PollResultItemClickListener, +) : RecyclerView.Adapter<PollResultViewHolder>() { + internal var list: MutableList<PollResultItem> = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollResultViewHolder { + var viewHolder: PollResultViewHolder? = null + + when (viewType) { + PollResultHeaderItem.VIEW_TYPE -> { + val itemBinding = PollResultHeaderItemBinding.inflate( + LayoutInflater.from(parent.context), parent, + false + ) + viewHolder = PollResultHeaderViewHolder(itemBinding) + } + PollResultVoterItem.VIEW_TYPE -> { + val itemBinding = PollResultVoterItemBinding.inflate( + LayoutInflater.from(parent.context), parent, + false + ) + viewHolder = PollResultVoterViewHolder(user, itemBinding) + } + PollResultVotersOverviewItem.VIEW_TYPE -> { + val itemBinding = PollResultVotersOverviewItemBinding.inflate( + LayoutInflater.from(parent.context), parent, + false + ) + viewHolder = PollResultVotersOverviewViewHolder(user, itemBinding) + } + } + return viewHolder!! + } + + override fun onBindViewHolder(holder: PollResultViewHolder, position: Int) { + when (holder.itemViewType) { + PollResultHeaderItem.VIEW_TYPE -> { + val pollResultItem = list[position] + holder.bind(pollResultItem as PollResultHeaderItem, clickListener) + } + PollResultVoterItem.VIEW_TYPE -> { + val pollResultItem = list[position] + holder.bind(pollResultItem as PollResultVoterItem, clickListener) + } + PollResultVotersOverviewItem.VIEW_TYPE -> { + val pollResultItem = list[position] + holder.bind(pollResultItem as PollResultVotersOverviewItem, clickListener) + } + } + } + + override fun getItemCount(): Int { + return list.size + } + + override fun getItemViewType(position: Int): Int { + return list[position].getViewType() + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt b/app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt new file mode 100644 index 000000000..f96e0009e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt @@ -0,0 +1,44 @@ +/* + * 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.model + +data class Poll( + val id: String, + val question: String?, + val options: List<String>?, + val votes: Map<String, Int>?, + val actorType: String?, + val actorId: String?, + val actorDisplayName: String?, + val status: Int, + val resultMode: Int, + val maxVotes: Int, + val votedSelf: List<Int>?, + val numVoters: Int, + val details: List<PollDetails>? +) { + companion object { + const val STATUS_OPEN: Int = 0 + const val STATUS_CLOSED: Int = 1 + const val RESULT_MODE_PUBLIC: Int = 0 + const val RESULT_MODE_HIDDEN: Int = 1 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/model/PollDetails.kt b/app/src/main/java/com/nextcloud/talk/polls/model/PollDetails.kt new file mode 100644 index 000000000..6b025bf8f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/model/PollDetails.kt @@ -0,0 +1,28 @@ +/* + * 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.model + +data class PollDetails( + val actorType: String?, + val actorId: String?, + val actorDisplayName: String?, + val optionId: Int +) diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepository.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepository.kt new file mode 100644 index 000000000..feef4b563 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepository.kt @@ -0,0 +1,43 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * @author Álvaro Brey + * Copyright (C) 2022 Álvaro Brey + * 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 <https://www.gnu.org/licenses/>. + */ + +package com.nextcloud.talk.polls.repositories + +import com.nextcloud.talk.polls.model.Poll +import io.reactivex.Observable + +interface PollRepository { + + fun createPoll( + roomToken: String, + question: String, + options: List<String>, + resultMode: Int, + maxVotes: Int + ): Observable<Poll> + + fun getPoll(roomToken: String, pollId: String): Observable<Poll> + + fun vote(roomToken: String, pollId: String, options: List<Int>): Observable<Poll> + + fun closePoll(roomToken: String, pollId: String): Observable<Poll> +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt new file mode 100644 index 000000000..30138b67b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt @@ -0,0 +1,138 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * @author Álvaro Brey + * Copyright (C) 2022 Álvaro Brey + * 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 <https://www.gnu.org/licenses/>. + */ + +package com.nextcloud.talk.polls.repositories + +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.polls.model.Poll +import com.nextcloud.talk.polls.model.PollDetails +import com.nextcloud.talk.polls.repositories.model.PollDetailsResponse +import com.nextcloud.talk.polls.repositories.model.PollResponse +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew +import io.reactivex.Observable + +class PollRepositoryImpl(private val ncApi: NcApi, private val currentUserProvider: CurrentUserProviderNew) : + PollRepository { + + val currentUser: User = currentUserProvider.currentUser.blockingGet() + val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token) + + override fun createPoll( + roomToken: String, + question: String, + options: List<String>, + resultMode: Int, + maxVotes: + Int + ): Observable<Poll> { + return ncApi.createPoll( + credentials, + ApiUtils.getUrlForPoll( + currentUser.baseUrl, + roomToken + ), + question, + options, + resultMode, + maxVotes + ).map { mapToPoll(it.ocs?.data!!) } + } + + override fun getPoll(roomToken: String, pollId: String): Observable<Poll> { + + return ncApi.getPoll( + credentials, + ApiUtils.getUrlForPoll( + currentUser.baseUrl, + roomToken, + pollId + ), + ).map { mapToPoll(it.ocs?.data!!) } + } + + override fun vote(roomToken: String, pollId: String, options: List<Int>): Observable<Poll> { + + return ncApi.votePoll( + credentials, + ApiUtils.getUrlForPoll( + currentUser.baseUrl, + roomToken, + pollId + ), + options + ).map { mapToPoll(it.ocs?.data!!) } + } + + override fun closePoll(roomToken: String, pollId: String): Observable<Poll> { + + return ncApi.closePoll( + credentials, + ApiUtils.getUrlForPoll( + currentUser.baseUrl, + roomToken, + pollId + ), + ).map { mapToPoll(it.ocs?.data!!) } + } + + companion object { + + private fun mapToPoll(pollResponse: PollResponse): Poll { + val pollDetails = pollResponse.details?.map { it -> mapToPollDetails(it) } + + return Poll( + pollResponse.id, + pollResponse.question, + pollResponse.options, + convertVotes(pollResponse.votes), + pollResponse.actorType, + pollResponse.actorId, + pollResponse.actorDisplayName, + pollResponse.status, + pollResponse.resultMode, + pollResponse.maxVotes, + pollResponse.votedSelf, + pollResponse.numVoters, + pollDetails + ) + } + + private fun convertVotes(votes: Map<String, Int>?): Map<String, Int> { + val resultMap: MutableMap<String, Int> = HashMap() + votes?.forEach { + resultMap[it.key.replace("option-", "")] = it.value + } + return resultMap + } + + private fun mapToPollDetails(pollDetailsResponse: PollDetailsResponse): PollDetails { + return PollDetails( + pollDetailsResponse.actorType, + pollDetailsResponse.actorId, + pollDetailsResponse.actorDisplayName, + pollDetailsResponse.optionId, + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollDetailsResponse.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollDetailsResponse.kt new file mode 100644 index 000000000..0d03e0172 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollDetailsResponse.kt @@ -0,0 +1,44 @@ +/* + * 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 PollDetailsResponse( + @JsonField(name = ["actorType"]) + var actorType: String? = null, + + @JsonField(name = ["actorId"]) + var actorId: String, + + @JsonField(name = ["actorDisplayName"]) + var actorDisplayName: String, + + @JsonField(name = ["optionId"]) + var optionId: Int, +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this(null, "", "", 0) +} 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..1adf3832b --- /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 <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: PollResponse? +) : 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 <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) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt new file mode 100644 index 000000000..9010ee2f5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt @@ -0,0 +1,71 @@ +/* + * 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 PollResponse( + @JsonField(name = ["id"]) + var id: String, + + @JsonField(name = ["question"]) + var question: String? = null, + + @JsonField(name = ["options"]) + var options: ArrayList<String>? = null, + + @JsonField(name = ["votes"]) + var votes: Map<String, 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, + + @JsonField(name = ["numVoters"]) + var numVoters: Int = 0, + + @JsonField(name = ["details"]) + var details: ArrayList<PollDetailsResponse>? = null, +) : Parcelable { + // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' + constructor() : this("id", null, null, null, null, null, null, 0, 0, 0, null, 0, null) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt new file mode 100644 index 000000000..cdaa8fa58 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollCreateDialogFragment.kt @@ -0,0 +1,187 @@ +/* + * 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.ui + +import android.annotation.SuppressLint +import android.app.Dialog +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogPollCreateBinding +import com.nextcloud.talk.polls.adapters.PollCreateOptionItem +import com.nextcloud.talk.polls.adapters.PollCreateOptionsAdapter +import com.nextcloud.talk.polls.adapters.PollCreateOptionsItemListener +import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PollCreateDialogFragment : DialogFragment(), PollCreateOptionsItemListener { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + private lateinit var binding: DialogPollCreateBinding + private lateinit var viewModel: PollCreateViewModel + + private var adapter: PollCreateOptionsAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + viewModel = ViewModelProvider(this, viewModelFactory)[PollCreateViewModel::class.java] + val roomToken = arguments?.getString(KEY_ROOM_TOKEN)!! + viewModel.setData(roomToken) + } + + @SuppressLint("InflateParams") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogPollCreateBinding.inflate(LayoutInflater.from(context)) + + return AlertDialog.Builder(requireContext()) + .setView(binding.root) + .create() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.options.observe(viewLifecycleOwner) { options -> adapter?.updateOptionsList(options) } + + binding.pollCreateOptionsList.layoutManager = LinearLayoutManager(context) + + adapter = PollCreateOptionsAdapter(this) + binding.pollCreateOptionsList.adapter = adapter + + setupListeners() + setupStateObserver() + } + + private fun setupListeners() { + binding.pollAddOptionsItem.setOnClickListener { + viewModel.addOption() + adapter?.itemCount?.minus(1)?.let { binding.pollCreateOptionsList.scrollToPosition(it) } + } + + binding.pollDismiss.setOnClickListener { + dismiss() + } + + binding.pollCreateQuestion.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable) { + // unused atm + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + // unused atm + } + + override fun onTextChanged(question: CharSequence, start: Int, before: Int, count: Int) { + if (question.toString() != viewModel.question) { + viewModel.setQuestion(question.toString()) + } + } + }) + + binding.pollPrivatePollCheckbox.setOnClickListener { + viewModel.setPrivatePoll(binding.pollPrivatePollCheckbox.isChecked) + } + + binding.pollMultipleAnswersCheckbox.setOnClickListener { + viewModel.setMultipleAnswer(binding.pollMultipleAnswersCheckbox.isChecked) + } + + binding.pollCreateButton.setOnClickListener { + viewModel.createPoll() + } + } + + private fun setupStateObserver() { + viewModel.viewState.observe(viewLifecycleOwner) { state -> + when (state) { + is PollCreateViewModel.PollCreatedState -> dismiss() + is PollCreateViewModel.PollCreationFailedState -> showError() + is PollCreateViewModel.PollCreationState -> updateButtons(state) + } + } + } + + private fun updateButtons(state: PollCreateViewModel.PollCreationState) { + binding.pollAddOptionsItem.isEnabled = state.enableAddOptionButton + binding.pollCreateButton.isEnabled = state.enableCreatePollButton + } + + private fun showError() { + dismiss() + Log.e(TAG, "Failed to create poll") + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } + + override fun onRemoveOptionsItemClick(pollCreateOptionItem: PollCreateOptionItem, position: Int) { + viewModel.removeOption(pollCreateOptionItem) + } + + override fun onOptionsItemTextChanged(pollCreateOptionItem: PollCreateOptionItem) { + viewModel.optionsItemTextChanged() + } + + override fun requestFocus(textField: EditText) { + if (binding.pollCreateQuestion.text.isBlank()) { + binding.pollCreateQuestion.requestFocus() + } else { + textField.requestFocus() + } + } + + /** + * Fragment creator + */ + companion object { + private val TAG = PollCreateDialogFragment::class.java.simpleName + private const val KEY_ROOM_TOKEN = "keyRoomToken" + + @JvmStatic + fun newInstance(roomTokenParam: String): PollCreateDialogFragment { + val args = Bundle() + args.putString(KEY_ROOM_TOKEN, roomTokenParam) + val fragment = PollCreateDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt new file mode 100644 index 000000000..77b46f8aa --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollLoadingFragment.kt @@ -0,0 +1,74 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ + +package com.nextcloud.talk.polls.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import autodagger.AutoInjector +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogPollLoadingBinding + +@AutoInjector(NextcloudTalkApplication::class) +class PollLoadingFragment : Fragment() { + + private lateinit var binding: DialogPollLoadingBinding + + var fragmentHeight = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + fragmentHeight = arguments?.getInt(KEY_FRAGMENT_HEIGHT)!! + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DialogPollLoadingBinding.inflate(inflater, container, false) + binding.root.layoutParams.height = fragmentHeight + return binding.root + } + + companion object { + private val TAG = PollLoadingFragment::class.java.simpleName + private const val KEY_FRAGMENT_HEIGHT = "keyFragmentHeight" + + @JvmStatic + fun newInstance( + fragmentHeight: Int + ): PollLoadingFragment { + + val args = bundleOf( + KEY_FRAGMENT_HEIGHT to fragmentHeight, + ) + + val fragment = PollLoadingFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt new file mode 100644 index 000000000..85cba1f64 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt @@ -0,0 +1,188 @@ +/* + * 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.ui + +import android.annotation.SuppressLint +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.databinding.DialogPollMainBinding +import com.nextcloud.talk.polls.viewmodels.PollMainViewModel +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PollMainDialogFragment : DialogFragment() { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + private lateinit var binding: DialogPollMainBinding + private lateinit var viewModel: PollMainViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + + viewModel = ViewModelProvider(this, viewModelFactory)[PollMainViewModel::class.java] + + val user: User = arguments?.getParcelable(KEY_USER_ENTITY)!! + val roomToken = arguments?.getString(KEY_ROOM_TOKEN)!! + val isOwnerOrModerator = arguments?.getBoolean(KEY_OWNER_OR_MODERATOR)!! + val pollId = arguments?.getString(KEY_POLL_ID)!! + val pollTitle = arguments?.getString(KEY_POLL_TITLE)!! + + viewModel.setData(user, roomToken, isOwnerOrModerator, pollId, pollTitle) + } + + @SuppressLint("InflateParams") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogPollMainBinding.inflate(LayoutInflater.from(context)) + + val dialog = AlertDialog.Builder(requireContext()) + .setView(binding.root) + .create() + + binding.messagePollTitle.text = viewModel.pollTitle + + return dialog + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.viewState.observe(viewLifecycleOwner) { state -> + when (state) { + PollMainViewModel.InitialState -> {} + is PollMainViewModel.PollVoteState -> { + initVotersAmount(state.showVotersAmount, state.poll.numVoters, false) + showVoteScreen() + } + is PollMainViewModel.PollResultState -> { + initVotersAmount(state.showVotersAmount, state.poll.numVoters, true) + showResultsScreen() + } + is PollMainViewModel.LoadingState -> { + showLoadingScreen() + } + is PollMainViewModel.DismissDialogState -> { + dismiss() + } + else -> {} + } + } + } + + private fun showLoadingScreen() { + binding.root.post { + run() { + val fragmentHeight = binding.messagePollContentFragment.measuredHeight + + val contentFragment = PollLoadingFragment.newInstance(fragmentHeight) + val transaction = childFragmentManager.beginTransaction() + transaction.replace(binding.messagePollContentFragment.id, contentFragment) + transaction.commit() + } + } + } + + private fun showVoteScreen() { + val contentFragment = PollVoteFragment.newInstance() + + val transaction = childFragmentManager.beginTransaction() + transaction.replace(binding.messagePollContentFragment.id, contentFragment) + transaction.commit() + } + + private fun showResultsScreen() { + val contentFragment = PollResultsFragment.newInstance() + + val transaction = childFragmentManager.beginTransaction() + transaction.replace(binding.messagePollContentFragment.id, contentFragment) + transaction.commit() + } + + private fun initVotersAmount(showVotersAmount: Boolean, numVoters: Int, showResultSubtitle: Boolean) { + if (showVotersAmount) { + binding.pollVotesAmount.visibility = View.VISIBLE + binding.pollVotesAmount.text = String.format( + resources.getString(R.string.polls_amount_voters), + numVoters + ) + } else { + binding.pollVotesAmount.visibility = View.GONE + } + + if (showResultSubtitle) { + binding.pollResultsSubtitle.visibility = View.VISIBLE + binding.pollResultsSubtitleSeperator.visibility = View.VISIBLE + } else { + binding.pollResultsSubtitle.visibility = View.GONE + binding.pollResultsSubtitleSeperator.visibility = View.GONE + } + } + + /** + * Fragment creator + */ + companion object { + private const val KEY_USER_ENTITY = "keyUserEntity" + private const val KEY_ROOM_TOKEN = "keyRoomToken" + private const val KEY_OWNER_OR_MODERATOR = "keyIsOwnerOrModerator" + private const val KEY_POLL_ID = "keyPollId" + private const val KEY_POLL_TITLE = "keyPollTitle" + + @JvmStatic + fun newInstance( + user: User, + roomTokenParam: String, + isOwnerOrModerator: Boolean, + pollId: String, + name: String + ): PollMainDialogFragment { + + val args = bundleOf( + KEY_USER_ENTITY to user, + KEY_ROOM_TOKEN to roomTokenParam, + KEY_OWNER_OR_MODERATOR to isOwnerOrModerator, + KEY_POLL_ID to pollId, + KEY_POLL_TITLE to name + ) + + val fragment = PollMainDialogFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt new file mode 100644 index 000000000..b1cbe4392 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt @@ -0,0 +1,139 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * @author Álvaro Brey + * Copyright (C) 2022 Álvaro Brey + * 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 <https://www.gnu.org/licenses/>. + */ + +package com.nextcloud.talk.polls.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogPollResultsBinding +import com.nextcloud.talk.polls.adapters.PollResultItemClickListener +import com.nextcloud.talk.polls.adapters.PollResultsAdapter +import com.nextcloud.talk.polls.viewmodels.PollMainViewModel +import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PollResultsFragment : Fragment(), PollResultItemClickListener { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + private lateinit var parentViewModel: PollMainViewModel + lateinit var viewModel: PollResultsViewModel + + lateinit var binding: DialogPollResultsBinding + + private var adapter: PollResultsAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + viewModel = ViewModelProvider(this, viewModelFactory)[PollResultsViewModel::class.java] + parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory)[PollMainViewModel::class.java] + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DialogPollResultsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + parentViewModel.viewState.observe(viewLifecycleOwner) { state -> + if (state is PollMainViewModel.PollResultState) { + initAdapter() + viewModel.setPoll(state.poll) + initEditButton(state.showEditButton) + initEndPollButton(state.showEndPollButton) + } + } + + viewModel.items.observe(viewLifecycleOwner) { + val adapter = PollResultsAdapter(parentViewModel.user, this).apply { + if (it != null) { + list = it + } + } + binding.pollResultsList.adapter = adapter + } + } + + private fun initAdapter() { + adapter = PollResultsAdapter(parentViewModel.user, this) + binding.pollResultsList.adapter = adapter + binding.pollResultsList.layoutManager = LinearLayoutManager(context) + } + + private fun initEditButton(showEditButton: Boolean) { + if (showEditButton) { + binding.editVoteButton.visibility = View.VISIBLE + binding.editVoteButton.setOnClickListener { + parentViewModel.editVotes() + } + } else { + binding.editVoteButton.visibility = View.GONE + } + } + + private fun initEndPollButton(showEndPollButton: Boolean) { + if (showEndPollButton) { + binding.pollResultsEndPollButton.visibility = View.VISIBLE + binding.pollResultsEndPollButton.setOnClickListener { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.polls_end_poll) + .setMessage(R.string.polls_end_poll_confirm) + .setPositiveButton(R.string.polls_end_poll) { _, _ -> + parentViewModel.endPoll() + } + .setNegativeButton(R.string.nc_cancel, null) + .show() + } + } else { + binding.pollResultsEndPollButton.visibility = View.GONE + } + } + + override fun onClick() { + viewModel.toggleDetails() + } + + companion object { + @JvmStatic + fun newInstance(): PollResultsFragment { + return PollResultsFragment() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt new file mode 100644 index 000000000..67b878442 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt @@ -0,0 +1,219 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * @author Álvaro Brey + * Copyright (C) 2022 Álvaro Brey + * 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 <https://www.gnu.org/licenses/>. + */ + +package com.nextcloud.talk.polls.ui + +import android.graphics.Typeface +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.CheckBox +import android.widget.CompoundButton +import android.widget.LinearLayout +import android.widget.RadioButton +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogPollVoteBinding +import com.nextcloud.talk.polls.model.Poll +import com.nextcloud.talk.polls.viewmodels.PollMainViewModel +import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PollVoteFragment : Fragment() { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + private lateinit var parentViewModel: PollMainViewModel + lateinit var viewModel: PollVoteViewModel + + private lateinit var binding: DialogPollVoteBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + viewModel = ViewModelProvider(this, viewModelFactory)[PollVoteViewModel::class.java] + + parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory)[PollMainViewModel::class.java] + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DialogPollVoteBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + parentViewModel.viewState.observe(viewLifecycleOwner) { state -> + if (state is PollMainViewModel.PollVoteState) { + initPollOptions(state.poll) + initEndPollButton(state.showEndPollButton) + updateSubmitButton() + updateDismissEditButton(state.showDismissEditButton) + } + } + + viewModel.viewState.observe(viewLifecycleOwner) { state -> + when (state) { + PollVoteViewModel.InitialState -> {} + is PollVoteViewModel.PollVoteFailedState -> { + Log.e(TAG, "Failed to vote on poll.") + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } + is PollVoteViewModel.PollVoteHiddenSuccessState -> { + Toast.makeText(context, R.string.polls_voted_hidden_success, Toast.LENGTH_LONG).show() + parentViewModel.dismissDialog() + } + is PollVoteViewModel.PollVoteSuccessState -> { + parentViewModel.voted() + } + } + } + + viewModel.submitButtonEnabled.observe(viewLifecycleOwner) { enabled -> + binding.pollVoteSubmitButton.isEnabled = enabled + } + + binding.pollVoteRadioGroup.setOnCheckedChangeListener { _, checkedId -> + viewModel.selectOption(checkedId, true) + updateSubmitButton() + } + + binding.pollVoteSubmitButton.setOnClickListener { + viewModel.vote(parentViewModel.roomToken, parentViewModel.pollId) + } + + binding.pollVoteEditDismiss.setOnClickListener { + parentViewModel.dismissEditVotes() + } + } + + private fun updateDismissEditButton(showDismissEditButton: Boolean) { + if (showDismissEditButton) { + binding.pollVoteEditDismiss.visibility = View.VISIBLE + } else { + binding.pollVoteEditDismiss.visibility = View.GONE + } + } + + private fun initPollOptions(poll: Poll) { + poll.votedSelf?.let { viewModel.initVotedOptions(it as ArrayList<Int>) } + + if (poll.maxVotes == 1) { + binding.pollVoteRadioGroup.removeAllViews() + poll.options?.map { option -> + RadioButton(context).apply { text = option } + }?.forEachIndexed { index, radioButton -> + radioButton.id = index + makeOptionBoldIfSelfVoted(radioButton, poll, index) + binding.pollVoteRadioGroup.addView(radioButton) + + radioButton.isChecked = viewModel.selectedOptions.contains(index) == true + } + } else { + binding.voteOptionsCheckboxesWrapper.removeAllViews() + + val layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + layoutParams.marginStart = CHECKBOX_MARGIN_LEFT + + poll.options?.map { option -> + CheckBox(context).apply { + text = option + setLayoutParams(layoutParams) + } + }?.forEachIndexed { index, checkBox -> + checkBox.id = index + makeOptionBoldIfSelfVoted(checkBox, poll, index) + binding.voteOptionsCheckboxesWrapper.addView(checkBox) + + checkBox.isChecked = viewModel.selectedOptions.contains(index) == true + checkBox.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + if (poll.maxVotes == UNLIMITED_VOTES || viewModel.selectedOptions.size < poll.maxVotes) { + viewModel.selectOption(index, false) + } else { + checkBox.isChecked = false + Toast.makeText(context, R.string.polls_max_votes_reached, Toast.LENGTH_LONG).show() + } + } else { + viewModel.deSelectOption(index) + } + updateSubmitButton() + } + } + } + } + + private fun updateSubmitButton() { + viewModel.updateSubmitButton() + } + + private fun makeOptionBoldIfSelfVoted(button: CompoundButton, poll: Poll, index: Int) { + if (poll.votedSelf?.contains(index) == true) { + button.setTypeface(null, Typeface.BOLD) + } + } + + private fun initEndPollButton(showEndPollButton: Boolean) { + if (showEndPollButton) { + binding.pollVoteEndPollButton.visibility = View.VISIBLE + binding.pollVoteEndPollButton.setOnClickListener { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.polls_end_poll) + .setMessage(R.string.polls_end_poll_confirm) + .setPositiveButton(R.string.polls_end_poll) { _, _ -> + parentViewModel.endPoll() + } + .setNegativeButton(R.string.nc_cancel, null) + .show() + } + } else { + binding.pollVoteEndPollButton.visibility = View.GONE + } + } + + companion object { + private val TAG = PollVoteFragment::class.java.simpleName + private const val UNLIMITED_VOTES = 0 + private const val CHECKBOX_MARGIN_LEFT = -18 + + @JvmStatic + fun newInstance(): PollVoteFragment { + return PollVoteFragment() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt new file mode 100644 index 000000000..ebaae84be --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollCreateViewModel.kt @@ -0,0 +1,204 @@ +/* + * 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.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.polls.adapters.PollCreateOptionItem +import com.nextcloud.talk.polls.model.Poll +import com.nextcloud.talk.polls.repositories.PollRepository +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class PollCreateViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() { + + private lateinit var roomToken: String + + sealed interface ViewState + open class PollCreationState(val enableAddOptionButton: Boolean, val enableCreatePollButton: Boolean) : ViewState + object PollCreatedState : ViewState + object PollCreationFailedState : ViewState + + private val _viewState: MutableLiveData<ViewState> = MutableLiveData( + PollCreationState( + enableAddOptionButton = true, + enableCreatePollButton = false + ) + ) + val viewState: LiveData<ViewState> + get() = _viewState + + private var _options: MutableLiveData<ArrayList<PollCreateOptionItem>> = + MutableLiveData<ArrayList<PollCreateOptionItem>>() + val options: LiveData<ArrayList<PollCreateOptionItem>> + get() = _options + + private var _question: String = "" + val question: String + get() = _question + + private var _privatePoll: Boolean = false + val privatePoll: Boolean + get() = _privatePoll + + private var _multipleAnswer: Boolean = false + val multipleAnswer: Boolean + get() = _multipleAnswer + + private var disposable: Disposable? = null + + init { + addOption() + addOption() + } + + fun setData(roomToken: String) { + this.roomToken = roomToken + updateCreationState() + } + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + + fun addOption() { + val item = PollCreateOptionItem("") + val currentOptions: ArrayList<PollCreateOptionItem> = _options.value ?: ArrayList() + currentOptions.add(item) + _options.value = currentOptions + updateCreationState() + } + + fun removeOption(item: PollCreateOptionItem) { + val currentOptions: ArrayList<PollCreateOptionItem> = _options.value ?: ArrayList() + currentOptions.remove(item) + _options.value = currentOptions + updateCreationState() + } + + fun createPoll() { + var maxVotes = 1 + if (multipleAnswer) { + maxVotes = 0 + } + + var resultMode = 0 + if (privatePoll) { + resultMode = 1 + } + + _options.value = _options.value?.filter { it.pollOption.isNotEmpty() } as ArrayList<PollCreateOptionItem> + + if (_question.isNotEmpty() && _options.value?.isNotEmpty() == true) { + _viewState.value = PollCreationState(enableAddOptionButton = false, enableCreatePollButton = false) + + repository.createPoll( + roomToken, _question, _options.value!!.map { it.pollOption }, resultMode, + maxVotes + ) + .doOnSubscribe { disposable = it } + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(PollObserver()) + } + } + + fun setQuestion(question: String) { + _question = question + updateCreationState() + } + + fun setPrivatePoll(checked: Boolean) { + _privatePoll = checked + } + + fun setMultipleAnswer(checked: Boolean) { + _multipleAnswer = checked + } + + fun optionsItemTextChanged() { + updateCreationState() + } + + private fun updateCreationState() { + _viewState.value = PollCreationState(enableAddOptionButton(), enableCreatePollButton()) + } + + private fun enableCreatePollButton(): Boolean { + return _question.isNotEmpty() && atLeastTwoOptionsAreFilled() + } + + private fun atLeastTwoOptionsAreFilled(): Boolean { + if (_options.value != null) { + var filledOptions = 0 + _options.value?.forEach { + if (it.pollOption.isNotEmpty()) { + filledOptions++ + } + if (filledOptions >= 2) { + return true + } + } + } + return false + } + + private fun enableAddOptionButton(): Boolean { + if (_options.value != null && _options.value?.size != 0) { + _options.value?.forEach { + if (it.pollOption.isBlank()) { + return false + } + } + } + return true + } + + inner class PollObserver : Observer<Poll> { + + lateinit var poll: Poll + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(response: Poll) { + poll = response + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Failed to create poll", e) + _viewState.value = PollCreationFailedState + } + + override fun onComplete() { + _viewState.value = PollCreatedState + } + } + + companion object { + private val TAG = PollCreateViewModel::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt new file mode 100644 index 000000000..734b93fdf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollMainViewModel.kt @@ -0,0 +1,188 @@ +/* + * 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.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.polls.model.Poll +import com.nextcloud.talk.polls.repositories.PollRepository +import com.nextcloud.talk.utils.database.user.UserUtils +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class PollMainViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() { + + @Inject + lateinit var userUtils: UserUtils + + lateinit var user: User + lateinit var roomToken: String + private var isOwnerOrModerator: Boolean = false + lateinit var pollId: String + lateinit var pollTitle: String + + private var editVotes: Boolean = false + + sealed interface ViewState + object InitialState : ViewState + object DismissDialogState : ViewState + object LoadingState : ViewState + + open class PollVoteState( + val poll: Poll, + val showVotersAmount: Boolean, + val showEndPollButton: Boolean, + val showDismissEditButton: Boolean + ) : ViewState + + open class PollResultState( + val poll: Poll, + val showVotersAmount: Boolean, + val showEndPollButton: Boolean, + val showEditButton: Boolean + ) : ViewState + + private val _viewState: MutableLiveData<ViewState> = MutableLiveData(InitialState) + val viewState: LiveData<ViewState> + get() = _viewState + + private var disposable: Disposable? = null + + fun setData(user: User, roomToken: String, isOwnerOrModerator: Boolean, pollId: String, pollTitle: String) { + this.user = user + this.roomToken = roomToken + this.isOwnerOrModerator = isOwnerOrModerator + this.pollId = pollId + this.pollTitle = pollTitle + + loadPoll() + } + + fun voted() { + loadPoll() + } + + fun editVotes() { + editVotes = true + loadPoll() + } + + fun dismissEditVotes() { + loadPoll() + } + + private fun loadPoll() { + _viewState.value = LoadingState + repository.getPoll(roomToken, pollId) + .doOnSubscribe { disposable = it } + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(PollObserver()) + } + + fun endPoll() { + _viewState.value = LoadingState + repository.closePoll(roomToken, pollId) + .doOnSubscribe { disposable = it } + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(PollObserver()) + } + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + + inner class PollObserver : Observer<Poll> { + + lateinit var poll: Poll + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(response: Poll) { + poll = response + } + + override fun onError(e: Throwable) { + Log.e(TAG, "An error occurred: $e") + } + + override fun onComplete() { + val showEndPollButton = showEndPollButton(poll) + val showVotersAmount = showVotersAmount(poll) + + if (votedForOpenHiddenPoll(poll)) { + _viewState.value = PollVoteState(poll, showVotersAmount, showEndPollButton, false) + } else if (editVotes && poll.status == Poll.STATUS_OPEN) { + _viewState.value = PollVoteState(poll, false, showEndPollButton, true) + editVotes = false + } else if (poll.status == Poll.STATUS_CLOSED || poll.votedSelf?.isNotEmpty() == true) { + val showEditButton = poll.status == Poll.STATUS_OPEN && poll.resultMode == Poll.RESULT_MODE_PUBLIC + _viewState.value = PollResultState(poll, showVotersAmount, showEndPollButton, showEditButton) + } else if (poll.votedSelf.isNullOrEmpty()) { + _viewState.value = PollVoteState(poll, showVotersAmount, showEndPollButton, false) + } else { + Log.w(TAG, "unknown poll state") + } + } + } + + private fun showEndPollButton(poll: Poll): Boolean { + return poll.status == Poll.STATUS_OPEN && (isPollCreatedByCurrentUser(poll) || isOwnerOrModerator) + } + + private fun showVotersAmount(poll: Poll): Boolean { + return votedForPublicPoll(poll) || + poll.status == Poll.STATUS_CLOSED || + isOwnerOrModerator || + isPollCreatedByCurrentUser(poll) + } + + private fun votedForOpenHiddenPoll(poll: Poll): Boolean { + return poll.status == Poll.STATUS_OPEN && + poll.resultMode == Poll.RESULT_MODE_HIDDEN && + poll.votedSelf?.isNotEmpty() == true + } + + private fun votedForPublicPoll(poll: Poll): Boolean { + return poll.resultMode == Poll.RESULT_MODE_PUBLIC && + poll.votedSelf?.isNotEmpty() == true + } + + private fun isPollCreatedByCurrentUser(poll: Poll): Boolean { + return userUtils.currentUser?.userId == poll.actorId + } + + fun dismissDialog() { + _viewState.value = DismissDialogState + } + + companion object { + private val TAG = PollMainViewModel::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt new file mode 100644 index 000000000..430b0d37c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt @@ -0,0 +1,128 @@ +/* + * Nextcloud Talk application + * + * @author Álvaro Brey + * @author Marcel Hibbe + * Copyright (C) 2022 Álvaro Brey + * 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 <https://www.gnu.org/licenses/>. + */ + +package com.nextcloud.talk.polls.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.polls.adapters.PollResultHeaderItem +import com.nextcloud.talk.polls.adapters.PollResultItem +import com.nextcloud.talk.polls.adapters.PollResultVoterItem +import com.nextcloud.talk.polls.adapters.PollResultVotersOverviewItem +import com.nextcloud.talk.polls.model.Poll +import io.reactivex.disposables.Disposable +import javax.inject.Inject + +class PollResultsViewModel @Inject constructor() : ViewModel() { + + sealed interface ViewState + object InitialState : ViewState + + private var _poll: Poll? = null + val poll: Poll? + get() = _poll + + private var _itemsOverviewList: ArrayList<PollResultItem> = ArrayList() + private var _itemsDetailsList: ArrayList<PollResultItem> = ArrayList() + + private var _items: MutableLiveData<ArrayList<PollResultItem>?> = MutableLiveData<ArrayList<PollResultItem>?>() + val items: MutableLiveData<ArrayList<PollResultItem>?> + get() = _items + + private var disposable: Disposable? = null + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + + fun setPoll(poll: Poll) { + _poll = poll + initPollResults(_poll!!) + } + + private fun initPollResults(poll: Poll) { + _items.value = ArrayList() + + var oneVoteInPercent = 0 + if (poll.numVoters != 0) { + oneVoteInPercent = HUNDRED / poll.numVoters + } + + poll.options?.forEachIndexed { index, option -> + val votersAmountForThisOption = getVotersAmountForOption(poll, index) + val optionsPercent = oneVoteInPercent * votersAmountForThisOption + + val pollResultHeaderItem = PollResultHeaderItem( + option, + optionsPercent, + isOptionSelfVoted(poll, index) + ) + _itemsOverviewList.add(pollResultHeaderItem) + _itemsDetailsList.add(pollResultHeaderItem) + + val voters = poll.details?.filter { it.optionId == index } + + if (!voters.isNullOrEmpty()) { + _itemsOverviewList.add(PollResultVotersOverviewItem(voters)) + } + + if (!voters.isNullOrEmpty()) { + voters.forEach { + _itemsDetailsList.add(PollResultVoterItem(it)) + } + } + } + + _items.value = _itemsOverviewList + } + + private fun getVotersAmountForOption(poll: Poll, index: Int): Int { + var votersAmountForThisOption: Int? = 0 + if (poll.details != null) { + votersAmountForThisOption = poll.details.filter { it.optionId == index }.size + } else if (poll.votes != null) { + votersAmountForThisOption = poll.votes.filter { it.key.toInt() == index }[index.toString()] + if (votersAmountForThisOption == null) { + votersAmountForThisOption = 0 + } + } + return votersAmountForThisOption!! + } + + private fun isOptionSelfVoted(poll: Poll, index: Int): Boolean { + return poll.votedSelf?.contains(index) == true + } + + fun toggleDetails() { + if (_items.value?.containsAll(_itemsDetailsList) == true) { + _items.value = _itemsOverviewList + } else { + _items.value = _itemsDetailsList + } + } + + companion object { + private val TAG = PollResultsViewModel::class.java.simpleName + private const val HUNDRED = 100 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt new file mode 100644 index 000000000..e3689ff62 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollVoteViewModel.kt @@ -0,0 +1,133 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * @author Álvaro Brey + * Copyright (C) 2022 Álvaro Brey + * 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 <https://www.gnu.org/licenses/>. + */ + +package com.nextcloud.talk.polls.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.polls.model.Poll +import com.nextcloud.talk.polls.repositories.PollRepository +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class PollVoteViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() { + + sealed interface ViewState + object InitialState : ViewState + open class PollVoteSuccessState : ViewState + open class PollVoteHiddenSuccessState : ViewState + open class PollVoteFailedState : ViewState + + private val _viewState: MutableLiveData<ViewState> = MutableLiveData(InitialState) + val viewState: LiveData<ViewState> + get() = _viewState + + private val _submitButtonEnabled: MutableLiveData<Boolean> = MutableLiveData() + val submitButtonEnabled: LiveData<Boolean> + get() = _submitButtonEnabled + + private var disposable: Disposable? = null + + private var _votedOptions: List<Int> = emptyList() + val votedOptions: List<Int> + get() = _votedOptions + + private var _selectedOptions: List<Int> = emptyList() + val selectedOptions: List<Int> + get() = _selectedOptions + + fun initVotedOptions(selectedOptions: List<Int>) { + _votedOptions = selectedOptions + _selectedOptions = selectedOptions + } + + fun selectOption(option: Int, isRadioBox: Boolean) { + _selectedOptions = if (isRadioBox) { + listOf(option) + } else { + _selectedOptions.plus(option) + } + } + + fun deSelectOption(option: Int) { + _selectedOptions = _selectedOptions.minus(option) + } + + fun vote(roomToken: String, pollId: String) { + if (_selectedOptions.isNotEmpty()) { + _submitButtonEnabled.value = false + + repository.vote(roomToken, pollId, _selectedOptions) + .doOnSubscribe { disposable = it } + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(PollObserver()) + } + } + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + + fun updateSubmitButton() { + val areSelectedOptionsDifferentToVotedOptions = !( + votedOptions.containsAll(selectedOptions) && + selectedOptions.containsAll(votedOptions) + ) + + _submitButtonEnabled.value = areSelectedOptionsDifferentToVotedOptions && selectedOptions.isNotEmpty() + } + + inner class PollObserver : Observer<Poll> { + + lateinit var poll: Poll + + override fun onSubscribe(d: Disposable) = Unit + + override fun onNext(response: Poll) { + poll = response + } + + override fun onError(e: Throwable) { + Log.e(TAG, "An error occurred: $e") + _viewState.value = PollVoteFailedState() + } + + override fun onComplete() { + if (poll.resultMode == 1) { + _viewState.value = PollVoteHiddenSuccessState() + } else { + _viewState.value = PollVoteSuccessState() + } + } + } + + companion object { + private val TAG = PollVoteViewModel::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt index a668d672d..62f69beee 100644 --- a/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/shareditems/repositories/SharedItemsRepository.kt @@ -28,7 +28,10 @@ import io.reactivex.Observable interface SharedItemsRepository { - fun media(parameters: Parameters, type: SharedItemType): Observable<SharedMediaItems>? + fun media( + parameters: Parameters, + type: SharedItemType + ): Observable<SharedMediaItems>? fun media( parameters: Parameters, diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt index c760990ab..13a0564fb 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt @@ -43,6 +43,12 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle setContentView(dialogAttachmentBinding.root) window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + initItemsStrings() + initItemsVisibility() + initItemsClickListeners() + } + + private fun initItemsStrings() { var serverName = CapabilitiesUtilNew.getServerName(chatController.conversationUser) dialogAttachmentBinding.txtAttachFileFromCloud.text = chatController.resources?.let { if (serverName.isNullOrEmpty()) { @@ -50,7 +56,9 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle } String.format(it.getString(R.string.nc_upload_from_cloud), serverName) } + } + private fun initItemsVisibility() { if (!CapabilitiesUtilNew.hasSpreedFeatureCapability( chatController.conversationUser, "geo-location-sharing" @@ -59,6 +67,12 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle dialogAttachmentBinding.menuShareLocation.visibility = View.GONE } + if (!CapabilitiesUtilNew.hasSpreedFeatureCapability(chatController.conversationUser, "talk-polls")) { + dialogAttachmentBinding.menuAttachPoll.visibility = View.GONE + } + } + + private fun initItemsClickListeners() { dialogAttachmentBinding.menuShareLocation.setOnClickListener { chatController.showShareLocationScreen() dismiss() @@ -74,6 +88,11 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle dismiss() } + dialogAttachmentBinding.menuAttachPoll.setOnClickListener { + chatController.createPoll() + dismiss() + } + dialogAttachmentBinding.menuAttachFileFromCloud.setOnClickListener { chatController.showBrowserScreen() dismiss() 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 <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; + } + } 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 @@ +<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> diff --git a/app/src/main/res/drawable/ic_baseline_close_24.xml b/app/src/main/res/drawable/ic_baseline_close_24.xml new file mode 100644 index 000000000..1d6c00461 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_close_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="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_comment_white.xml b/app/src/main/res/drawable/ic_comment_white.xml deleted file mode 100644 index 74f759c9e..000000000 --- a/app/src/main/res/drawable/ic_comment_white.xml +++ /dev/null @@ -1,29 +0,0 @@ -<!-- - ~ Nextcloud Talk application - ~ - ~ @author Mario Danic - ~ Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com> - ~ - ~ 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/>. - --> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="32" - android:viewportHeight="32"> - <path - android:fillColor="#FFFFFF" - android:pathData="M6.667,4C4.089,4 2,6.105 2,8.7v11.282c0,2.597 2.09,4.701 4.667,4.701 1.716,0.01 12.083,0.003 17.057,0 1.115,0.842 1.807,1.748 3.057,3.206a0.93,0.93 0,0 0,0.561 0.103,0.969 0.969,0 0,0 0.445,-0.187c0.302,-0.223 0.466,-0.603 0.427,-0.988l-0.314,-2.912a4.699,4.699 0,0 0,2.1 -3.923L30,8.701C30,6.105 27.91,4 25.333,4zM10.4,12.461c1.03,0 1.867,0.842 1.867,1.88 0,1.676 -2.01,2.514 -3.187,1.33 -1.176,-1.184 -0.343,-3.21 1.32,-3.21zM16,12.461c1.03,0 1.867,0.842 1.867,1.88 0,1.676 -2.01,2.514 -3.187,1.33 -1.176,-1.184 -0.343,-3.21 1.32,-3.21zM21.6,12.461c1.03,0 1.867,0.842 1.867,1.88 0,1.676 -2.01,2.514 -3.187,1.33 -1.176,-1.184 -0.343,-3.21 1.32,-3.21z"/> -</vector> diff --git a/app/src/main/res/layout/dialog_attachment.xml b/app/src/main/res/layout/dialog_attachment.xml index ed381203c..8280c8a90 100644 --- a/app/src/main/res/layout/dialog_attachment.xml +++ b/app/src/main/res/layout/dialog_attachment.xml @@ -40,6 +40,39 @@ android:textSize="@dimen/bottom_sheet_text_size" /> <LinearLayout + android:id="@+id/menu_attach_poll" + 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" + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_padding" + tools:ignore="UseCompoundDrawables"> + + <ImageView + android:id="@+id/menu_icon_attach_poll" + 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" /> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/txt_attach_poll" + 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_create_poll" + android:textAlignment="viewStart" + android:textColor="@color/high_emphasis_text" + android:textSize="@dimen/bottom_sheet_text_size" /> + + </LinearLayout> + + <LinearLayout android:id="@+id/menu_attach_contact" android:layout_width="match_parent" android:layout_height="@dimen/bottom_sheet_item_height" diff --git a/app/src/main/res/layout/dialog_poll_create.xml b/app/src/main/res/layout/dialog_poll_create.xml new file mode 100644 index 000000000..9106aa53c --- /dev/null +++ b/app/src/main/res/layout/dialog_poll_create.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/>. + --> + +<ScrollView 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="match_parent" + android:orientation="vertical" + android:padding="@dimen/standard_padding" + tools:background="@color/white"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="@color/colorPrimary" + android:textStyle="bold" + android:text="@string/polls_question" /> + + <EditText + android:id="@+id/poll_create_question" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textMultiLine" + tools:ignore="Autofill,LabelFor"/> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="@color/colorPrimary" + android:textStyle="bold" + android:layout_marginTop="@dimen/standard_margin" + android:text="@string/polls_options" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/poll_create_options_list" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/poll_create_options_item" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/poll_add_options_item" + style="@style/OutlinedButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/standard_half_margin" + app:icon="@drawable/ic_add_grey600_24px" + app:cornerRadius="@dimen/button_corner_radius" + app:layout_constraintEnd_toEndOf="parent" + android:text="@string/polls_add_option" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/colorPrimary" + android:textStyle="bold" + android:layout_marginTop="@dimen/standard_margin" + android:layout_marginBottom="@dimen/standard_half_margin" + android:text="@string/polls_settings" /> + + <CheckBox + android:id="@+id/poll_private_poll_checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/polls_private_poll" /> + + <CheckBox + android:id="@+id/poll_multiple_answers_checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/polls_multiple_answers" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginTop="@dimen/standard_margin" + android:gravity="end"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/poll_dismiss" + style="@style/OutlinedButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/standard_half_margin" + app:cornerRadius="@dimen/button_corner_radius" + app:layout_constraintEnd_toEndOf="parent" + android:text="@string/nc_common_dismiss" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/poll_create_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/standard_half_margin" + app:cornerRadius="@dimen/button_corner_radius" + app:layout_constraintEnd_toEndOf="parent" + android:text="@string/nc_create_poll" + android:theme="@style/Button.Primary" /> + + </LinearLayout> + </LinearLayout> +</ScrollView> diff --git a/app/src/main/res/layout/dialog_poll_loading.xml b/app/src/main/res/layout/dialog_poll_loading.xml new file mode 100644 index 000000000..07e00ecd0 --- /dev/null +++ b/app/src/main/res/layout/dialog_poll_loading.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/>. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="center" + tools:background="@color/white"> + + <ProgressBar + android:layout_width="25dp" + android:layout_height="25dp"> + </ProgressBar> + +</LinearLayout> diff --git a/app/src/main/res/layout/dialog_poll_main.xml b/app/src/main/res/layout/dialog_poll_main.xml new file mode 100644 index 000000000..d583b2905 --- /dev/null +++ b/app/src/main/res/layout/dialog_poll_main.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/>. + --> +<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="match_parent" + android:padding="@dimen/standard_padding" + android:orientation="vertical" + tools:background="@color/white"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <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" /> + + <androidx.emoji.widget.EmojiTextView + android:id="@+id/message_poll_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:textStyle="bold" + tools:text="This is the poll title?" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <TextView + android:id="@+id/poll_results_subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:textColor="@color/low_emphasis_text" + android:text="@string/polls_results_subtitle" + android:visibility="gone" + tools:visibility="visible"/> + + <TextView + android:id="@+id/poll_results_subtitle_seperator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:textColor="@color/low_emphasis_text" + android:text=" - " + android:visibility="gone" + tools:visibility="visible" + tools:ignore="HardcodedText" /> + + <TextView + android:id="@+id/poll_votes_amount" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:textColor="@color/low_emphasis_text" + tools:text="93 votes" /> + + </LinearLayout> + + <FrameLayout + android:id="@+id/message_poll_content_fragment" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_marginTop="16dp" + android:layout_weight="1" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/dialog_poll_results.xml b/app/src/main/res/layout/dialog_poll_results.xml new file mode 100644 index 000000000..0e347c4a5 --- /dev/null +++ b/app/src/main/res/layout/dialog_poll_results.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/>. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools" + tools:background="@color/white" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/poll_results_list_wrapper" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/poll_results_list" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:listitem="@layout/poll_result_header_item" /> + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/standard_margin" + android:gravity="end"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/poll_results_end_poll_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/polls_end_poll" + style="@style/OutlinedButton" + android:layout_marginEnd="@dimen/standard_margin" + app:cornerRadius="@dimen/button_corner_radius" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/edit_vote_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/polls_edit_vote" + android:theme="@style/Button.Primary" + app:cornerRadius="@dimen/button_corner_radius" /> + </LinearLayout> + +</LinearLayout> diff --git a/app/src/main/res/layout/dialog_poll_vote.xml b/app/src/main/res/layout/dialog_poll_vote.xml new file mode 100644 index 000000000..c69f0a946 --- /dev/null +++ b/app/src/main/res/layout/dialog_poll_vote.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/>. + --> +<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="match_parent" + android:orientation="vertical" + tools:background="@color/white"> + + <ScrollView + android:id="@+id/vote_options_wrapper" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/vote_options_checkboxes_wrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" /> + + <RadioGroup + android:id="@+id/poll_vote_radio_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="-4dp" + tools:layout_height="400dp" /> + </LinearLayout> + </ScrollView> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/standard_margin" + android:gravity="end"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/poll_vote_end_poll_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/polls_end_poll" + style="@style/OutlinedButton" + android:layout_marginEnd="@dimen/standard_margin" + app:cornerRadius="@dimen/button_corner_radius" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/poll_vote_edit_dismiss" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/nc_common_dismiss" + style="@style/OutlinedButton" + android:layout_marginEnd="@dimen/standard_margin" + android:visibility="gone" + app:cornerRadius="@dimen/button_corner_radius" + tools:visibility="visible"/> + + <com.google.android.material.button.MaterialButton + android:id="@+id/poll_vote_submit_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/polls_submit_vote" + android:theme="@style/Button.Primary" + app:cornerRadius="@dimen/button_corner_radius" /> + </LinearLayout> + +</LinearLayout> 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..6c8520a51 --- /dev/null +++ b/app/src/main/res/layout/item_custom_incoming_poll_message.xml @@ -0,0 +1,110 @@ +<?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:gravity="center_vertical" + android:orientation="horizontal"> + + <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" /> + + <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:id="@+id/message_poll_subtitle" + 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_open" /> + + <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> 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..1f3cfbdfb --- /dev/null +++ b/app/src/main/res/layout/item_custom_outcoming_poll_message.xml @@ -0,0 +1,105 @@ +<?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:gravity="center_vertical" + android:orientation="horizontal"> + + <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/nc_outcoming_text_default" /> + + <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" + android:textColor="@color/nc_outcoming_text_default" + tools:text="This is the poll title?" /> + + </LinearLayout> + + <TextView + android:id="@+id/message_poll_subtitle" + 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_open" + android:textColor="@color/nc_outcoming_text_default" /> + + <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" + android:textColor="@color/nc_outcoming_text_default" + 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" + android:textColor="@color/nc_outcoming_text_default" + app:layout_alignSelf="center" + android:contentDescription="@null" /> + + <include + android:id="@+id/reactions" + layout="@layout/reactions_inside_message" /> + + </com.google.android.flexbox.FlexboxLayout> +</RelativeLayout> diff --git a/app/src/main/res/layout/poll_create_options_item.xml b/app/src/main/res/layout/poll_create_options_item.xml new file mode 100644 index 000000000..2ae9e0fcf --- /dev/null +++ b/app/src/main/res/layout/poll_create_options_item.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/>. + --> + +<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="horizontal" + tools:background="@color/white"> + + <EditText + android:id="@+id/poll_option_text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:singleLine="true" + android:inputType="text" + tools:ignore="Autofill,LabelFor" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/poll_option_delete" + style="@style/Widget.AppTheme.Button.IconButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="5dp" + android:contentDescription="@string/nc_action_open_main_menu" + app:cornerRadius="@dimen/button_corner_radius" + app:icon="@drawable/ic_baseline_close_24" + app:iconTint="@color/fontAppbar" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/poll_result_header_item.xml b/app/src/main/res/layout/poll_result_header_item.xml new file mode 100644 index 000000000..44bfbf364 --- /dev/null +++ b/app/src/main/res/layout/poll_result_header_item.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/>. + --> + +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:background="@color/white"> + + <TextView + android:id="@+id/poll_option_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Option Number One" /> + + <TextView + android:id="@+id/poll_option_percent_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/poll_option_text" + tools:text="50%" /> + + <com.google.android.material.progressindicator.LinearProgressIndicator + android:id="@+id/poll_option_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:indeterminate="false" + app:indicatorColor="@color/poll_bar_color" + app:layout_constraintStart_toStartOf="@+id/poll_option_text" + app:layout_constraintTop_toBottomOf="@+id/poll_option_text" + app:trackColor="@color/dialog_background" + app:trackCornerRadius="5dp" + app:trackThickness="5dp" + android:paddingBottom="@dimen/standard_half_padding" + tools:progress="50" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/poll_result_voter_item.xml b/app/src/main/res/layout/poll_result_voter_item.xml new file mode 100644 index 000000000..1e7774224 --- /dev/null +++ b/app/src/main/res/layout/poll_result_voter_item.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/>. + --> + +<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:paddingBottom="4dp" + tools:background="@color/white"> + + <com.facebook.drawee.view.SimpleDraweeView + android:id="@+id/poll_voter_avatar" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_marginEnd="8dp" + android:layout_gravity="center" + app:roundAsCircle="true" /> + + <TextView + android:id="@+id/poll_voter_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + tools:text="Bill Murray" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/poll_result_voters_overview_item.xml b/app/src/main/res/layout/poll_result_voters_overview_item.xml new file mode 100644 index 000000000..06b03b840 --- /dev/null +++ b/app/src/main/res/layout/poll_result_voters_overview_item.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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/>. + --> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/voters_avatars_overview_wrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="4dp" + android:orientation="horizontal" + tools:background="@color/white"> +</RelativeLayout> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 0d5bf97ad..2a0d69148 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -106,6 +106,9 @@ <color name="list_divider_background">#eeeeee</color> <color name="grey_200">#EEEEEE</color> + <!-- poll --> + <color name="poll_bar_color">#8dd4f6</color> + <!-- this is just a helper for status icon background because getting the background color of a dialog is not possible?! don't use this to set the background of dialogs --> <color name="dialog_background">#FFFFFF</color> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f6acc81cd..ec961c518 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,6 +26,7 @@ <string name="nc_no">No</string> <string name="nc_common_skip">Skip</string> <string name="nc_common_set">Set</string> + <string name="nc_common_dismiss">Dismiss</string> <string name="nc_common_error_sorry">Sorry, something went wrong!</string> <!-- Bottom Navigation --> @@ -309,6 +310,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 +319,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> @@ -401,6 +404,7 @@ <!-- Upload --> <string name="nc_add_file">Add to conversation</string> <string name="nc_upload_picture_from_cam">Take photo</string> + <string name="nc_create_poll">Create poll</string> <string name="nc_upload_from_cloud">Share from %1$s</string> <string name="nc_upload_failed">Sorry, upload failed</string> <string name="nc_upload_choose_local_files">Choose files</string> @@ -527,6 +531,23 @@ <string name="message_search_begin_typing">Start typing to search …</string> <string name="message_search_begin_empty">No search results</string> + <!-- Polls --> + <string name="message_poll_tap_to_open">Tap to open poll</string> + <string name="polls_amount_voters">%1$s votes</string> + <string name="polls_add_option">Add option</string> + <string name="polls_edit_vote">Edit vote</string> + <string name="polls_submit_vote">Vote</string> + <string name="polls_voted_hidden_success">Successfully voted</string> + <string name="polls_end_poll">End poll</string> + <string name="polls_end_poll_confirm">Do you really want to end this poll? This can\'t be undone.</string> + <string name="polls_max_votes_reached">You can\'t vote with more options for this poll.</string> + <string name="polls_results_subtitle">Results</string> + <string name="polls_question">Question</string> + <string name="polls_options">Options</string> + <string name="polls_settings">Settings</string> + <string name="polls_private_poll">Private poll</string> + <string name="polls_multiple_answers">Multiple answers</string> + <string name="title_attachments">Attachments</string> <string name="reactions_tab_all">All</string> @@ -534,4 +555,5 @@ <string name="call_without_notification">Call without notification</string> <string name="set_avatar_from_camera">Set avatar from camera</string> + </resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 02455e83e..011181880 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -147,6 +147,7 @@ <item name="android:textColor">@color/white</item> <item name="android:typeface">sans</item> <item name="android:textStyle">bold</item> + <item name="android:layout_gravity">center_vertical</item> </style> <style name="Widget.AppTheme.Button.IconButton" parent="Widget.MaterialComponents.Button.TextButton"> @@ -268,6 +269,7 @@ <item name="android:textAllCaps">false</item> <item name="android:typeface">sans</item> <item name="android:textStyle">bold</item> + <item name="android:layout_gravity">center_vertical</item> </style> <style name="TextAppearanceTab" parent="TextAppearance.Design.Tab"> |