Merge pull request #2095 from nextcloud/feature/2024/simplePolls

Support "Simple polls"
This commit is contained in:
Marcel Hibbe 2022-07-22 20:10:48 +02:00 committed by GitHub
commit 2896d0758f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 4233 additions and 64 deletions

View File

@ -50,7 +50,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage 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.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.UriUtils import com.nextcloud.talk.utils.UriUtils
@ -117,7 +116,7 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
if (!TextUtils.isEmpty(author)) { if (!TextUtils.isEmpty(author)) {
binding.messageAuthor.text = author binding.messageAuthor.text = author
binding.messageUserAvatar.setOnClickListener { binding.messageUserAvatar.setOnClickListener {
(payload as? ProfileBottomSheet)?.showFor(message.actorId!!, itemView.context) (payload as? MessagePayload)?.profileBottomSheet?.showFor(message.actorId!!, itemView.context)
} }
} else { } else {
binding.messageAuthor.setText(R.string.nc_nick_guest) binding.messageAuthor.setText(R.string.nc_nick_guest)

View File

@ -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
}
}

View File

@ -48,7 +48,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage 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.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
@ -210,7 +209,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
if (!TextUtils.isEmpty(author)) { if (!TextUtils.isEmpty(author)) {
binding.messageAuthor.text = author binding.messageAuthor.text = author
binding.messageUserAvatar.setOnClickListener { binding.messageUserAvatar.setOnClickListener {
(payload as? ProfileBottomSheet)?.showFor(message.actorId!!, itemView.context) (payload as? MessagePayload)?.profileBottomSheet?.showFor(message.actorId!!, itemView.context)
} }
} else { } else {
binding.messageAuthor.setText(R.string.nc_nick_guest) binding.messageAuthor.setText(R.string.nc_nick_guest)

View File

@ -47,14 +47,12 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding
import com.nextcloud.talk.models.json.chat.ChatMessage 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.ui.recyclerview.MessageSwipeCallback
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.TextMatchers import com.nextcloud.talk.utils.TextMatchers
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders import com.stfalcon.chatkit.messages.MessageHolders
import java.util.HashMap
import javax.inject.Inject import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
@ -136,7 +134,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
if (!TextUtils.isEmpty(message.actorDisplayName)) { if (!TextUtils.isEmpty(message.actorDisplayName)) {
binding.messageAuthor.text = message.actorDisplayName binding.messageAuthor.text = message.actorDisplayName
binding.messageUserAvatar.setOnClickListener { binding.messageUserAvatar.setOnClickListener {
(payload as? ProfileBottomSheet)?.showFor(message.actorId!!, itemView.context) (payload as? MessagePayload)?.profileBottomSheet?.showFor(message.actorId!!, itemView.context)
} }
} else { } else {
binding.messageAuthor.setText(R.string.nc_nick_guest) binding.messageAuthor.setText(R.string.nc_nick_guest)

View File

@ -49,7 +49,6 @@ import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
import com.nextcloud.talk.data.user.model.User; import com.nextcloud.talk.data.user.model.User;
import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding; import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
import com.nextcloud.talk.models.json.chat.ChatMessage; 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.DisplayUtils;
import com.nextcloud.talk.utils.DrawableUtils; import com.nextcloud.talk.utils.DrawableUtils;
import com.nextcloud.talk.utils.FileViewerUtils; import com.nextcloud.talk.utils.FileViewerUtils;
@ -125,8 +124,9 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
} else { } else {
userAvatar.setVisibility(View.VISIBLE); userAvatar.setVisibility(View.VISIBLE);
userAvatar.setOnClickListener(v -> { userAvatar.setOnClickListener(v -> {
if (payload instanceof ProfileBottomSheet) { if (payload instanceof MessagePayload) {
((ProfileBottomSheet) payload).showFor(message.getActorId(), v.getContext()); ((MessagePayload) payload).getProfileBottomSheet().showFor(message.getActorId(),
v.getContext());
} }
}); });

View File

@ -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
)

View File

@ -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
}
}

View File

@ -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.unifiedsearch.UnifiedSearchOverall;
import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall; import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall;
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall; import com.nextcloud.talk.models.json.userprofile.UserProfileOverall;
import com.nextcloud.talk.polls.repositories.model.PollOverall;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -526,4 +527,27 @@ public interface NcApi {
@Query("from") String fromUrl, @Query("from") String fromUrl,
@Query("limit") Integer limit, @Query("limit") Integer limit,
@Query("cursor") Integer cursor); @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);
} }

View File

@ -104,13 +104,16 @@ import com.nextcloud.talk.activities.CallActivity
import com.nextcloud.talk.activities.MainActivity import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.activities.TakePhotoActivity import com.nextcloud.talk.activities.TakePhotoActivity
import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder 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.IncomingPreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.IncomingVoiceMessageViewHolder import com.nextcloud.talk.adapters.messages.IncomingVoiceMessageViewHolder
import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder
import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder
import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder
import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder 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.OutcomingLocationMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
import com.nextcloud.talk.adapters.messages.PreviewMessageInterface 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.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.models.json.mention.Mention 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.presenters.MentionAutocompletePresenter
import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
@ -483,10 +487,12 @@ class ChatController(args: Bundle) :
val messageHolders = MessageHolders() val messageHolders = MessageHolders()
val profileBottomSheet = ProfileBottomSheet(ncApi!!, conversationUser!!, router) val profileBottomSheet = ProfileBottomSheet(ncApi!!, conversationUser!!, router)
val payload = MessagePayload(currentConversation!!, profileBottomSheet)
messageHolders.setIncomingTextConfig( messageHolders.setIncomingTextConfig(
MagicIncomingTextMessageViewHolder::class.java, MagicIncomingTextMessageViewHolder::class.java,
R.layout.item_custom_incoming_text_message, R.layout.item_custom_incoming_text_message,
profileBottomSheet payload
) )
messageHolders.setOutcomingTextConfig( messageHolders.setOutcomingTextConfig(
MagicOutcomingTextMessageViewHolder::class.java, MagicOutcomingTextMessageViewHolder::class.java,
@ -496,7 +502,7 @@ class ChatController(args: Bundle) :
messageHolders.setIncomingImageConfig( messageHolders.setIncomingImageConfig(
IncomingPreviewMessageViewHolder::class.java, IncomingPreviewMessageViewHolder::class.java,
R.layout.item_custom_incoming_preview_message, R.layout.item_custom_incoming_preview_message,
profileBottomSheet payload
) )
messageHolders.setOutcomingImageConfig( messageHolders.setOutcomingImageConfig(
@ -525,7 +531,7 @@ class ChatController(args: Bundle) :
messageHolders.registerContentType( messageHolders.registerContentType(
CONTENT_TYPE_LOCATION, CONTENT_TYPE_LOCATION,
IncomingLocationMessageViewHolder::class.java, IncomingLocationMessageViewHolder::class.java,
profileBottomSheet, payload,
R.layout.item_custom_incoming_location_message, R.layout.item_custom_incoming_location_message,
OutcomingLocationMessageViewHolder::class.java, OutcomingLocationMessageViewHolder::class.java,
null, null,
@ -536,7 +542,7 @@ class ChatController(args: Bundle) :
messageHolders.registerContentType( messageHolders.registerContentType(
CONTENT_TYPE_VOICE_MESSAGE, CONTENT_TYPE_VOICE_MESSAGE,
IncomingVoiceMessageViewHolder::class.java, IncomingVoiceMessageViewHolder::class.java,
profileBottomSheet, payload,
R.layout.item_custom_incoming_voice_message, R.layout.item_custom_incoming_voice_message,
OutcomingVoiceMessageViewHolder::class.java, OutcomingVoiceMessageViewHolder::class.java,
null, null,
@ -544,6 +550,17 @@ class ChatController(args: Bundle) :
this 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 = "" var senderId = ""
if (!conversationUser?.userId.equals("?")) { if (!conversationUser?.userId.equals("?")) {
senderId = "users/" + conversationUser?.userId senderId = "users/" + conversationUser?.userId
@ -2576,6 +2593,11 @@ class ChatController(args: Bundle) :
chatMessageIterator.remove() chatMessageIterator.remove()
} }
// delete poll system messages
else if (isPollVotedMessage(currentMessage)) {
chatMessageIterator.remove()
}
} }
return chatMessageMap.values.toList() return chatMessageMap.values.toList()
} }
@ -2591,6 +2613,10 @@ class ChatController(args: Bundle) :
currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED 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) { private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
if (currentConversation?.canStartCall == false && currentConversation?.hasCall == false) { if (currentConversation?.canStartCall == false && currentConversation?.hasCall == false) {
Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show()
@ -3012,6 +3038,7 @@ class ChatController(args: Bundle) :
return when (type) { return when (type) {
CONTENT_TYPE_LOCATION -> message.hasGeoLocation() CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage
CONTENT_TYPE_POLL -> message.isPoll()
CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage) CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1" CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
else -> false 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 { companion object {
private const val TAG = "ChatController" private const val TAG = "ChatController"
private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1 private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1
private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2 private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2
private const val CONTENT_TYPE_LOCATION: Byte = 3 private const val CONTENT_TYPE_LOCATION: Byte = 3
private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 4 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 NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100 private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100
private const val LOBBY_TIMER_DELAY: Long = 5000 private const val LOBBY_TIMER_DELAY: Long = 5000

View File

@ -29,6 +29,8 @@ import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository
import com.nextcloud.talk.data.storage.ArbitraryStoragesRepositoryImpl import com.nextcloud.talk.data.storage.ArbitraryStoragesRepositoryImpl
import com.nextcloud.talk.data.user.UsersRepository import com.nextcloud.talk.data.user.UsersRepository
import com.nextcloud.talk.data.user.UsersRepositoryImpl 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.RemoteFileBrowserItemsRepository
import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepositoryImpl import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepositoryImpl
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository 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.SharedItemsRepository
import com.nextcloud.talk.shareditems.repositories.SharedItemsRepositoryImpl import com.nextcloud.talk.shareditems.repositories.SharedItemsRepositoryImpl
import com.nextcloud.talk.utils.database.user.CurrentUserProvider import com.nextcloud.talk.utils.database.user.CurrentUserProvider
import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -52,6 +55,11 @@ class RepositoryModule {
return UnifiedSearchRepositoryImpl(ncApi, userProvider) return UnifiedSearchRepositoryImpl(ncApi, userProvider)
} }
@Provides
fun provideDialogPollRepository(ncApi: NcApi, userProvider: CurrentUserProviderNew): PollRepository {
return PollRepositoryImpl(ncApi, userProvider)
}
@Provides @Provides
fun provideRemoteFileBrowserItemsRepository(okHttpClient: OkHttpClient, userProvider: CurrentUserProvider): fun provideRemoteFileBrowserItemsRepository(okHttpClient: OkHttpClient, userProvider: CurrentUserProvider):
RemoteFileBrowserItemsRepository { RemoteFileBrowserItemsRepository {

View File

@ -25,6 +25,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel
import com.nextcloud.talk.messagesearch.MessageSearchViewModel 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 com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
import dagger.Binds import dagger.Binds
import dagger.MapKey import dagger.MapKey
@ -61,6 +65,26 @@ abstract class ViewModelModule {
@ViewModelKey(MessageSearchViewModel::class) @ViewModelKey(MessageSearchViewModel::class)
abstract fun messageSearchViewModel(viewModel: MessageSearchViewModel): ViewModel abstract fun messageSearchViewModel(viewModel: MessageSearchViewModel): ViewModel
@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 @Binds
@IntoMap @IntoMap
@ViewModelKey(RemoteFileBrowserItemsViewModel::class) @ViewModelKey(RemoteFileBrowserItemsViewModel::class)

View File

@ -124,6 +124,8 @@ data class ChatMessage(
var voiceMessageDownloadProgress: Int = 0, var voiceMessageDownloadProgress: Int = 0,
) : Parcelable, MessageContentType, MessageContentType.Image { ) : Parcelable, MessageContentType, MessageContentType.Image {
// messageTypesToIgnore is weird. must be deleted by refactoring!!!
@JsonIgnore @JsonIgnore
var messageTypesToIgnore = Arrays.asList( var messageTypesToIgnore = Arrays.asList(
MessageType.REGULAR_TEXT_MESSAGE, MessageType.REGULAR_TEXT_MESSAGE,
@ -132,7 +134,8 @@ data class ChatMessage(
MessageType.SINGLE_LINK_AUDIO_MESSAGE, MessageType.SINGLE_LINK_AUDIO_MESSAGE,
MessageType.SINGLE_LINK_MESSAGE, MessageType.SINGLE_LINK_MESSAGE,
MessageType.SINGLE_NC_GEOLOCATION_MESSAGE, MessageType.SINGLE_NC_GEOLOCATION_MESSAGE,
MessageType.VOICE_MESSAGE MessageType.VOICE_MESSAGE,
MessageType.POLL_MESSAGE
) )
fun hasFileAttachment(): Boolean { fun hasFileAttachment(): Boolean {
@ -165,6 +168,21 @@ data class ChatMessage(
return false 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? { override fun getImageUrl(): String? {
if (messageParameters != null && messageParameters!!.size > 0) { if (messageParameters != null && messageParameters!!.size > 0) {
for ((_, individualHashMap) in messageParameters!!) { for ((_, individualHashMap) in messageParameters!!) {
@ -207,6 +225,8 @@ data class ChatMessage(
MessageType.SINGLE_NC_ATTACHMENT_MESSAGE MessageType.SINGLE_NC_ATTACHMENT_MESSAGE
} else if (hasGeoLocation()) { } else if (hasGeoLocation()) {
MessageType.SINGLE_NC_GEOLOCATION_MESSAGE MessageType.SINGLE_NC_GEOLOCATION_MESSAGE
} else if (isPoll()) {
MessageType.POLL_MESSAGE
} else { } else {
MessageType.REGULAR_TEXT_MESSAGE MessageType.REGULAR_TEXT_MESSAGE
} }
@ -334,6 +354,15 @@ data class ChatMessage(
getNullsafeActorDisplayName() 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 "" return ""
@ -410,6 +439,7 @@ data class ChatMessage(
SINGLE_LINK_AUDIO_MESSAGE, SINGLE_LINK_AUDIO_MESSAGE,
SINGLE_NC_ATTACHMENT_MESSAGE, SINGLE_NC_ATTACHMENT_MESSAGE,
SINGLE_NC_GEOLOCATION_MESSAGE, SINGLE_NC_GEOLOCATION_MESSAGE,
POLL_MESSAGE,
VOICE_MESSAGE VOICE_MESSAGE
} }
@ -460,7 +490,9 @@ data class ChatMessage(
CLEARED_CHAT, CLEARED_CHAT,
REACTION, REACTION,
REACTION_DELETED, REACTION_DELETED,
REACTION_REVOKED REACTION_REVOKED,
POLL_VOTED,
POLL_CLOSED
} }
companion object { companion object {

View File

@ -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.OBJECT_SHARED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_REMOVED 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.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
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_DELETED import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_DELETED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_REVOKED import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_REVOKED
@ -167,6 +169,8 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst
"reaction" -> REACTION "reaction" -> REACTION
"reaction_deleted" -> REACTION_DELETED "reaction_deleted" -> REACTION_DELETED
"reaction_revoked" -> REACTION_REVOKED "reaction_revoked" -> REACTION_REVOKED
"poll_voted" -> POLL_VOTED
"poll_closed" -> POLL_CLOSED
else -> DUMMY else -> DUMMY
} }
} }
@ -224,6 +228,8 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst
REACTION -> return "reaction" REACTION -> return "reaction"
REACTION_DELETED -> return "reaction_deleted" REACTION_DELETED -> return "reaction_deleted"
REACTION_REVOKED -> return "reaction_revoked" REACTION_REVOKED -> return "reaction_revoked"
POLL_VOTED -> return "poll_voted"
POLL_CLOSED -> return "poll_closed"
else -> return "" else -> return ""
} }
} }

View File

@ -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
)

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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
}
}

View File

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

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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 = ""
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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
)

View File

@ -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>
}

View File

@ -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,
)
}
}
}

View File

@ -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)
}

View File

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

View File

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

View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -28,7 +28,10 @@ import io.reactivex.Observable
interface SharedItemsRepository { interface SharedItemsRepository {
fun media(parameters: Parameters, type: SharedItemType): Observable<SharedMediaItems>? fun media(
parameters: Parameters,
type: SharedItemType
): Observable<SharedMediaItems>?
fun media( fun media(
parameters: Parameters, parameters: Parameters,

View File

@ -43,6 +43,12 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
setContentView(dialogAttachmentBinding.root) setContentView(dialogAttachmentBinding.root)
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
initItemsStrings()
initItemsVisibility()
initItemsClickListeners()
}
private fun initItemsStrings() {
var serverName = CapabilitiesUtilNew.getServerName(chatController.conversationUser) var serverName = CapabilitiesUtilNew.getServerName(chatController.conversationUser)
dialogAttachmentBinding.txtAttachFileFromCloud.text = chatController.resources?.let { dialogAttachmentBinding.txtAttachFileFromCloud.text = chatController.resources?.let {
if (serverName.isNullOrEmpty()) { 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) String.format(it.getString(R.string.nc_upload_from_cloud), serverName)
} }
}
private fun initItemsVisibility() {
if (!CapabilitiesUtilNew.hasSpreedFeatureCapability( if (!CapabilitiesUtilNew.hasSpreedFeatureCapability(
chatController.conversationUser, chatController.conversationUser,
"geo-location-sharing" "geo-location-sharing"
@ -59,6 +67,12 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
dialogAttachmentBinding.menuShareLocation.visibility = View.GONE dialogAttachmentBinding.menuShareLocation.visibility = View.GONE
} }
if (!CapabilitiesUtilNew.hasSpreedFeatureCapability(chatController.conversationUser, "talk-polls")) {
dialogAttachmentBinding.menuAttachPoll.visibility = View.GONE
}
}
private fun initItemsClickListeners() {
dialogAttachmentBinding.menuShareLocation.setOnClickListener { dialogAttachmentBinding.menuShareLocation.setOnClickListener {
chatController.showShareLocationScreen() chatController.showShareLocationScreen()
dismiss() dismiss()
@ -74,6 +88,11 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
dismiss() dismiss()
} }
dialogAttachmentBinding.menuAttachPoll.setOnClickListener {
chatController.createPoll()
dismiss()
}
dialogAttachmentBinding.menuAttachFileFromCloud.setOnClickListener { dialogAttachmentBinding.menuAttachFileFromCloud.setOnClickListener {
chatController.showBrowserScreen() chatController.showBrowserScreen()
dismiss() dismiss()

View File

@ -2,8 +2,10 @@
* Nextcloud Talk application * Nextcloud Talk application
* *
* @author Mario Danic * @author Mario Danic
* @author Marcel Hibbe
* @author Tim Krüger * @author Tim Krüger
* Copyright (C) 2021 Tim Krüger <t@timkrueger.me> * 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> * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
* *
* This program is free software: you can redistribute it and/or modify * 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 * @deprecated This is only supported on API v1-3, in API v4+ please use {@link ApiUtils#getUrlForAttendees(int,
* {@link ApiUtils#getUrlForAttendees(int, String, String)} instead. * String, String)} instead.
*/ */
@Deprecated @Deprecated
public static String getUrlForRemovingParticipantFromConversation(String baseUrl, String roomToken, boolean isGuest) { public static String getUrlForRemovingParticipantFromConversation(String baseUrl, String roomToken, boolean isGuest) {
@ -287,6 +289,7 @@ public class ApiUtils {
public static String getUrlForCall(int version, String baseUrl, String token) { public static String getUrlForCall(int version, String baseUrl, String token) {
return getUrlForApi(version, baseUrl) + "/call/" + token; return getUrlForApi(version, baseUrl) + "/call/" + token;
} }
public static String getUrlForChat(int version, String baseUrl, String token) { public static String getUrlForChat(int version, String baseUrl, String token) {
return getUrlForApi(version, baseUrl) + "/chat/" + token; return getUrlForApi(version, baseUrl) + "/chat/" + token;
} }
@ -294,6 +297,7 @@ public class ApiUtils {
public static String getUrlForMentionSuggestions(int version, String baseUrl, String token) { public static String getUrlForMentionSuggestions(int version, String baseUrl, String token) {
return getUrlForChat(version, baseUrl, token) + "/mentions"; return getUrlForChat(version, baseUrl, token) + "/mentions";
} }
public static String getUrlForChatMessage(int version, String baseUrl, String token, String messageId) { public static String getUrlForChatMessage(int version, String baseUrl, String token, String messageId) {
return getUrlForChat(version, baseUrl, token) + "/" + messageId; return getUrlForChat(version, baseUrl, token) + "/" + messageId;
} }
@ -448,8 +452,10 @@ public class ApiUtils {
return getUrlForChat(version, baseUrl, roomToken) + "/share"; return getUrlForChat(version, baseUrl, roomToken) + "/share";
} }
public static String getUrlForHoverCard(String baseUrl, String userId) { return baseUrl + ocsApiVersion + public static String getUrlForHoverCard(String baseUrl, String userId) {
"/hovercard/v1/" + userId; } return baseUrl + ocsApiVersion +
"/hovercard/v1/" + userId;
}
public static String getUrlForSetChatReadMarker(int version, String baseUrl, String roomToken) { public static String getUrlForSetChatReadMarker(int version, String baseUrl, String roomToken) {
return getUrlForChat(version, baseUrl, roomToken) + "/read"; return getUrlForChat(version, baseUrl, roomToken) + "/read";
@ -497,4 +503,16 @@ public class ApiUtils {
public static String getUrlForUnifiedSearch(@NotNull String baseUrl, @NotNull String providerId) { public static String getUrlForUnifiedSearch(@NotNull String baseUrl, @NotNull String providerId) {
return baseUrl + ocsApiVersion + "/search/providers/" + providerId + "/search"; 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;
}
} }

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -39,6 +39,39 @@
android:textColor="@color/medium_emphasis_text" android:textColor="@color/medium_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" /> 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 <LinearLayout
android:id="@+id/menu_attach_contact" android:id="@+id/menu_attach_contact"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -106,6 +106,9 @@
<color name="list_divider_background">#eeeeee</color> <color name="list_divider_background">#eeeeee</color>
<color name="grey_200">#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 <!-- 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 --> possible?! don't use this to set the background of dialogs -->
<color name="dialog_background">#FFFFFF</color> <color name="dialog_background">#FFFFFF</color>

View File

@ -26,6 +26,7 @@
<string name="nc_no">No</string> <string name="nc_no">No</string>
<string name="nc_common_skip">Skip</string> <string name="nc_common_skip">Skip</string>
<string name="nc_common_set">Set</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> <string name="nc_common_error_sorry">Sorry, something went wrong!</string>
<!-- Bottom Navigation --> <!-- Bottom Navigation -->
@ -309,6 +310,7 @@
<string name="nc_sent_an_audio" formatted="true">%1$s sent an audio.</string> <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_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_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_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_voice" formatted="true">%1$s sent a voice message.</string>
<string name="nc_sent_a_link_you">You sent a link.</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_an_audio_you">You sent an audio.</string>
<string name="nc_sent_a_video_you">You sent a video.</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_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_location_you">You sent a location.</string>
<string name="nc_sent_voice_you">You sent a voice message.</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> <string name="nc_formatted_message" translatable="false">%1$s: %2$s</string>
@ -401,6 +404,7 @@
<!-- Upload --> <!-- Upload -->
<string name="nc_add_file">Add to conversation</string> <string name="nc_add_file">Add to conversation</string>
<string name="nc_upload_picture_from_cam">Take photo</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_from_cloud">Share from %1$s</string>
<string name="nc_upload_failed">Sorry, upload failed</string> <string name="nc_upload_failed">Sorry, upload failed</string>
<string name="nc_upload_choose_local_files">Choose files</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_typing">Start typing to search …</string>
<string name="message_search_begin_empty">No search results</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="title_attachments">Attachments</string>
<string name="reactions_tab_all">All</string> <string name="reactions_tab_all">All</string>
@ -534,4 +555,5 @@
<string name="call_without_notification">Call without notification</string> <string name="call_without_notification">Call without notification</string>
<string name="set_avatar_from_camera">Set avatar from camera</string> <string name="set_avatar_from_camera">Set avatar from camera</string>
</resources> </resources>

View File

@ -147,6 +147,7 @@
<item name="android:textColor">@color/white</item> <item name="android:textColor">@color/white</item>
<item name="android:typeface">sans</item> <item name="android:typeface">sans</item>
<item name="android:textStyle">bold</item> <item name="android:textStyle">bold</item>
<item name="android:layout_gravity">center_vertical</item>
</style> </style>
<style name="Widget.AppTheme.Button.IconButton" parent="Widget.MaterialComponents.Button.TextButton"> <style name="Widget.AppTheme.Button.IconButton" parent="Widget.MaterialComponents.Button.TextButton">
@ -268,6 +269,7 @@
<item name="android:textAllCaps">false</item> <item name="android:textAllCaps">false</item>
<item name="android:typeface">sans</item> <item name="android:typeface">sans</item>
<item name="android:textStyle">bold</item> <item name="android:textStyle">bold</item>
<item name="android:layout_gravity">center_vertical</item>
</style> </style>
<style name="TextAppearanceTab" parent="TextAppearance.Design.Tab"> <style name="TextAppearanceTab" parent="TextAppearance.Design.Tab">