Merge pull request #4846 from nextcloud/event_conversations

Event conversations
This commit is contained in:
Marcel Hibbe 2025-05-14 16:00:24 +00:00 committed by GitHub
commit 52f58372b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1170 additions and 24 deletions

View File

@ -0,0 +1,719 @@
{
"formatVersion": 1,
"database": {
"version": 14,
"identityHash": "506abc931eb3b657cafe6ad1b25f635d",
"entities": [
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` TEXT, `username` TEXT, `baseUrl` TEXT, `token` TEXT, `displayName` TEXT, `pushConfigurationState` TEXT, `capabilities` TEXT, `serverVersion` TEXT DEFAULT '', `clientCertificate` TEXT, `externalSignalingServer` TEXT, `current` INTEGER NOT NULL, `scheduledForDeletion` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "TEXT"
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT"
},
{
"fieldPath": "baseUrl",
"columnName": "baseUrl",
"affinity": "TEXT"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT"
},
{
"fieldPath": "pushConfigurationState",
"columnName": "pushConfigurationState",
"affinity": "TEXT"
},
{
"fieldPath": "capabilities",
"columnName": "capabilities",
"affinity": "TEXT"
},
{
"fieldPath": "serverVersion",
"columnName": "serverVersion",
"affinity": "TEXT",
"defaultValue": "''"
},
{
"fieldPath": "clientCertificate",
"columnName": "clientCertificate",
"affinity": "TEXT"
},
{
"fieldPath": "externalSignalingServer",
"columnName": "externalSignalingServer",
"affinity": "TEXT"
},
{
"fieldPath": "current",
"columnName": "current",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scheduledForDeletion",
"columnName": "scheduledForDeletion",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "ArbitraryStorage",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
"fields": [
{
"fieldPath": "accountIdentifier",
"columnName": "accountIdentifier",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "storageObject",
"columnName": "object",
"affinity": "TEXT"
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"accountIdentifier",
"key"
]
}
},
{
"tableName": "Conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `displayName` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `avatarVersion` TEXT NOT NULL, `callFlag` INTEGER NOT NULL, `callRecording` INTEGER NOT NULL, `callStartTime` INTEGER NOT NULL, `canDeleteConversation` INTEGER NOT NULL, `canLeaveConversation` INTEGER NOT NULL, `canStartCall` INTEGER NOT NULL, `description` TEXT NOT NULL, `hasCall` INTEGER NOT NULL, `hasPassword` INTEGER NOT NULL, `isCustomAvatar` INTEGER NOT NULL, `isFavorite` INTEGER NOT NULL, `lastActivity` INTEGER NOT NULL, `lastCommonReadMessage` INTEGER NOT NULL, `lastMessage` TEXT, `lastPing` INTEGER NOT NULL, `lastReadMessage` INTEGER NOT NULL, `lobbyState` TEXT NOT NULL, `lobbyTimer` INTEGER NOT NULL, `messageExpiration` INTEGER NOT NULL, `name` TEXT NOT NULL, `notificationCalls` INTEGER NOT NULL, `notificationLevel` TEXT NOT NULL, `objectType` TEXT NOT NULL, `objectId` TEXT NOT NULL, `participantType` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `readOnly` TEXT NOT NULL, `recordingConsent` INTEGER NOT NULL, `remoteServer` TEXT, `remoteToken` TEXT, `sessionId` TEXT NOT NULL, `status` TEXT, `statusClearAt` INTEGER, `statusIcon` TEXT, `statusMessage` TEXT, `type` TEXT NOT NULL, `unreadMention` INTEGER NOT NULL, `unreadMentionDirect` INTEGER NOT NULL, `unreadMessages` INTEGER NOT NULL, `hasArchived` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`accountId`) REFERENCES `User`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatarVersion",
"columnName": "avatarVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "callFlag",
"columnName": "callFlag",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callRecording",
"columnName": "callRecording",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "callStartTime",
"columnName": "callStartTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canDeleteConversation",
"columnName": "canDeleteConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canLeaveConversation",
"columnName": "canLeaveConversation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canStartCall",
"columnName": "canStartCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasCall",
"columnName": "hasCall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasPassword",
"columnName": "hasPassword",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasCustomAvatar",
"columnName": "isCustomAvatar",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favorite",
"columnName": "isFavorite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastActivity",
"columnName": "lastActivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCommonReadMessage",
"columnName": "lastCommonReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessage",
"columnName": "lastMessage",
"affinity": "TEXT"
},
{
"fieldPath": "lastPing",
"columnName": "lastPing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastReadMessage",
"columnName": "lastReadMessage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lobbyState",
"columnName": "lobbyState",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lobbyTimer",
"columnName": "lobbyTimer",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageExpiration",
"columnName": "messageExpiration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationCalls",
"columnName": "notificationCalls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLevel",
"columnName": "notificationLevel",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectType",
"columnName": "objectType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "objectId",
"columnName": "objectId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "participantType",
"columnName": "participantType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "permissions",
"columnName": "permissions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "conversationReadOnlyState",
"columnName": "readOnly",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recordingConsentRequired",
"columnName": "recordingConsent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "remoteServer",
"columnName": "remoteServer",
"affinity": "TEXT"
},
{
"fieldPath": "remoteToken",
"columnName": "remoteToken",
"affinity": "TEXT"
},
{
"fieldPath": "sessionId",
"columnName": "sessionId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT"
},
{
"fieldPath": "statusClearAt",
"columnName": "statusClearAt",
"affinity": "INTEGER"
},
{
"fieldPath": "statusIcon",
"columnName": "statusIcon",
"affinity": "TEXT"
},
{
"fieldPath": "statusMessage",
"columnName": "statusMessage",
"affinity": "TEXT"
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unreadMention",
"columnName": "unreadMention",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMentionDirect",
"columnName": "unreadMentionDirect",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "unreadMessages",
"columnName": "unreadMessages",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasArchived",
"columnName": "hasArchived",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_Conversations_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Conversations_accountId` ON `${TABLE_NAME}` (`accountId`)"
}
],
"foreignKeys": [
{
"table": "User",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ChatMessages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendingFailed` INTEGER NOT NULL, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "internalId",
"columnName": "internalId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorDisplayName",
"columnName": "actorDisplayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorId",
"columnName": "actorId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actorType",
"columnName": "actorType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expirationTimestamp",
"columnName": "expirationTimestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "replyable",
"columnName": "isReplyable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isTemporary",
"columnName": "isTemporary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastEditActorDisplayName",
"columnName": "lastEditActorDisplayName",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorId",
"columnName": "lastEditActorId",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditActorType",
"columnName": "lastEditActorType",
"affinity": "TEXT"
},
{
"fieldPath": "lastEditTimestamp",
"columnName": "lastEditTimestamp",
"affinity": "INTEGER"
},
{
"fieldPath": "renderMarkdown",
"columnName": "markdown",
"affinity": "INTEGER"
},
{
"fieldPath": "messageParameters",
"columnName": "messageParameters",
"affinity": "TEXT"
},
{
"fieldPath": "messageType",
"columnName": "messageType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentMessageId",
"columnName": "parent",
"affinity": "INTEGER"
},
{
"fieldPath": "reactions",
"columnName": "reactions",
"affinity": "TEXT"
},
{
"fieldPath": "reactionsSelf",
"columnName": "reactionsSelf",
"affinity": "TEXT"
},
{
"fieldPath": "referenceId",
"columnName": "referenceId",
"affinity": "TEXT"
},
{
"fieldPath": "sendingFailed",
"columnName": "sendingFailed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "silent",
"columnName": "silent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "systemMessageType",
"columnName": "systemMessage",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"internalId"
]
},
"indices": [
{
"name": "index_ChatMessages_internalId",
"unique": true,
"columnNames": [
"internalId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ChatMessages_internalId` ON `${TABLE_NAME}` (`internalId`)"
},
{
"name": "index_ChatMessages_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatMessages_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
},
{
"tableName": "ChatBlocks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `internalConversationId` TEXT NOT NULL, `accountId` INTEGER, `token` TEXT, `oldestMessageId` INTEGER NOT NULL, `newestMessageId` INTEGER NOT NULL, `hasHistory` INTEGER NOT NULL, FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "internalConversationId",
"columnName": "internalConversationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER"
},
{
"fieldPath": "token",
"columnName": "token",
"affinity": "TEXT"
},
{
"fieldPath": "oldestMessageId",
"columnName": "oldestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newestMessageId",
"columnName": "newestMessageId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasHistory",
"columnName": "hasHistory",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ChatBlocks_internalConversationId",
"unique": false,
"columnNames": [
"internalConversationId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ChatBlocks_internalConversationId` ON `${TABLE_NAME}` (`internalConversationId`)"
}
],
"foreignKeys": [
{
"table": "Conversations",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"internalConversationId"
],
"referencedColumns": [
"internalId"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '506abc931eb3b657cafe6ad1b25f635d')"
]
}
}

View File

@ -215,6 +215,7 @@ class ChatBlocksDaoTest {
lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY,
lobbyTimer = 0,
objectType = ConversationEnums.ObjectType.FILE,
objectId = "",
statusIcon = null,
description = "",
displayName = "",

View File

@ -211,6 +211,7 @@ class ChatMessagesDaoTest {
lobbyState = ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY,
lobbyTimer = 0,
objectType = ConversationEnums.ObjectType.FILE,
objectId = "",
statusIcon = null,
description = "",
displayName = "",

View File

@ -38,12 +38,14 @@ import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.AbsListView
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.PopupWindow
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
@ -51,6 +53,7 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ContextThemeWrapper
import androidx.cardview.widget.CardView
import androidx.compose.runtime.mutableStateOf
@ -82,6 +85,7 @@ import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.target.Target
import coil.transform.CircleCropTransformation
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.common.ui.color.ColorUtil
import com.nextcloud.android.common.ui.theme.utils.ColorRole
@ -127,6 +131,7 @@ import com.nextcloud.talk.databinding.ActivityChatBinding
import com.nextcloud.talk.events.UserMentionClickEvent
import com.nextcloud.talk.events.WebSocketCommunicationEvent
import com.nextcloud.talk.extensions.loadAvatarOrImagePreview
import com.nextcloud.talk.jobs.DeleteConversationWorker
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
import com.nextcloud.talk.jobs.ShareOperationWorker
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
@ -210,13 +215,18 @@ import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.Locale
import java.util.concurrent.ExecutionException
import javax.inject.Inject
import kotlin.collections.set
import kotlin.math.roundToInt
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import com.nextcloud.talk.conversationinfo.viewmodel.ConversationInfoViewModel
import com.nextcloud.talk.models.json.participants.Participant
@AutoInjector(NextcloudTalkApplication::class)
class ChatActivity :
@ -254,6 +264,8 @@ class ChatActivity :
lateinit var networkMonitor: NetworkMonitor
lateinit var chatViewModel: ChatViewModel
lateinit var conversationInfoViewModel: ConversationInfoViewModel
lateinit var messageInputViewModel: MessageInputViewModel
private val startSelectContactForResult = registerForActivityResult(
@ -324,6 +336,7 @@ class ChatActivity :
private var conversationVoiceCallMenuItem: MenuItem? = null
private var conversationVideoMenuItem: MenuItem? = null
private var eventConversationMenuItem: MenuItem? = null
var webSocketInstance: WebSocketInstance? = null
var signalingMessageSender: SignalingMessageSender? = null
@ -418,6 +431,8 @@ class ChatActivity :
chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java]
conversationInfoViewModel = ViewModelProvider(this, viewModelFactory)[ConversationInfoViewModel::class.java]
val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
chatViewModel.initData(
@ -567,6 +582,7 @@ class ChatActivity :
participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)
invalidateOptionsMenu()
isEventConversation()
checkShowCallButtons()
checkLobbyState()
updateRoomTimerHandler()
@ -600,6 +616,7 @@ class ChatActivity :
loadAvatarForStatusBar()
setupSwipeToReply()
setActionBarTitle()
isEventConversation()
checkShowCallButtons()
checkLobbyState()
if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL &&
@ -1889,6 +1906,17 @@ class ChatActivity :
}
}
private fun isEventConversation() {
if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) {
if (eventConversationMenuItem != null) {
eventConversationMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
eventConversationMenuItem?.isEnabled = true
}
} else {
eventConversationMenuItem?.isEnabled = false
}
}
private fun isReadOnlyConversation(): Boolean =
currentConversation?.conversationReadOnlyState != null &&
currentConversation?.conversationReadOnlyState ==
@ -2849,12 +2877,19 @@ class ChatActivity :
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.menu_conversation, menu)
if (currentConversation?.objectType == ConversationEnums.ObjectType.EVENT) {
eventConversationMenuItem = menu.findItem(R.id.conversation_event)
} else {
menu.removeItem(R.id.conversation_event)
}
if (conversationUser?.userId == "?") {
menu.removeItem(R.id.conversation_info)
} else {
loadAvatarForStatusBar()
setActionBarTitle()
}
return true
}
@ -2871,12 +2906,6 @@ class ChatActivity :
searchItem.isVisible = CapabilitiesUtil.isUnifiedSearchAvailable(spreedCapabilities) &&
currentConversation!!.remoteServer.isNullOrEmpty()
if (currentConversation!!.remoteServer != null ||
!CapabilitiesUtil.isSharedItemsAvailable(spreedCapabilities)
) {
menu.removeItem(R.id.shared_items)
}
if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
@ -2909,7 +2938,6 @@ class ChatActivity :
menu.removeItem(R.id.conversation_voice_call)
}
}
return true
}
@ -2940,9 +2968,205 @@ class ChatActivity :
true
}
R.id.conversation_event -> {
val anchorView = findViewById<View>(R.id.conversation_event)
showPopupWindow(anchorView)
true
}
else -> super.onOptionsItemSelected(item)
}
private fun showPopupWindow(anchorView: View) {
val popupView = layoutInflater.inflate(R.layout.item_event_schedule, null)
val titleTextView = popupView.findViewById<TextView>(R.id.event_scheduled)
val subtitleTextView = popupView.findViewById<TextView>(R.id.meetingTime)
val popupWindow = PopupWindow(
popupView,
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
true
)
popupWindow.isOutsideTouchable = true
popupWindow.isFocusable = true
popupWindow.showAsDropDown(anchorView, 0, -anchorView.height)
val meetingStatus = showEventSchedule()
subtitleTextView.text = meetingStatus
deleteEventConversation(meetingStatus, popupWindow, popupView)
archiveEventConversation(meetingStatus, popupWindow, popupView)
}
private fun deleteEventConversation(meetingStatus: String, popupWindow: PopupWindow, popupView: View) {
val deleteConversation = popupView.findViewById<TextView>(R.id.delete_conversation)
if (meetingStatus == context.resources.getString(R.string.nc_meeting_ended) &&
currentConversation?.canDeleteConversation == true
) {
deleteConversation.visibility = View.VISIBLE
deleteConversation.setOnClickListener {
val dialogBuilder = MaterialAlertDialogBuilder(it.context)
.setIcon(
viewThemeUtils.dialog
.colorMaterialAlertDialogIcon(context, R.drawable.ic_delete_black_24dp)
)
.setTitle(R.string.nc_delete_call)
.setMessage(R.string.nc_delete_conversation_more)
.setPositiveButton(R.string.nc_delete) { _, _ ->
currentConversation?.let { conversation ->
deleteConversation(conversation)
}
}
.setNegativeButton(R.string.nc_cancel) { _, _ ->
}
viewThemeUtils.dialog
.colorMaterialAlertDialogBackground(it.context, dialogBuilder)
val dialog = dialogBuilder.show()
viewThemeUtils.platform.colorTextButtons(
dialog.getButton(AlertDialog.BUTTON_POSITIVE),
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
)
popupWindow.dismiss()
}
} else {
deleteConversation.visibility = View.GONE
}
}
private fun archiveEventConversation(meetingStatus: String, popupWindow: PopupWindow, popupView: View) {
val archiveConversation = popupView.findViewById<TextView>(R.id.archive_conversation)
val unarchiveConversation = popupView.findViewById<TextView>(R.id.unarchive_conversation)
if (meetingStatus == context.resources.getString(R.string.nc_meeting_ended) &&
(
Participant.ParticipantType.MODERATOR == currentConversation?.participantType ||
Participant.ParticipantType.OWNER == currentConversation?.participantType
)
) {
if (currentConversation?.hasArchived == false) {
unarchiveConversation.visibility = View.GONE
archiveConversation.visibility = View.VISIBLE
archiveConversation.setOnClickListener {
this.lifecycleScope.launch {
conversationInfoViewModel.archiveConversation(conversationUser!!, currentConversation?.token!!)
Snackbar.make(
binding.root,
String.format(
context.resources.getString(R.string.archived_conversation),
currentConversation?.displayName
),
Snackbar.LENGTH_LONG
).show()
}
popupWindow.dismiss()
}
} else {
unarchiveConversation.visibility = View.VISIBLE
archiveConversation.visibility = View.GONE
unarchiveConversation.setOnClickListener {
this.lifecycleScope.launch {
conversationInfoViewModel.unarchiveConversation(
conversationUser!!,
currentConversation?.token!!
)
Snackbar.make(
binding.root,
String.format(
context.resources.getString(R.string.unarchived_conversation),
currentConversation?.displayName
),
Snackbar.LENGTH_LONG
).show()
}
popupWindow.dismiss()
}
}
} else {
archiveConversation.visibility = View.GONE
unarchiveConversation.visibility = View.GONE
}
}
private fun deleteConversation(conversation: ConversationModel) {
val data = Data.Builder()
data.putLong(
KEY_INTERNAL_USER_ID,
conversationUser?.id!!
)
data.putString(KEY_ROOM_TOKEN, conversation.token)
val deleteConversationWorker =
OneTimeWorkRequest.Builder(DeleteConversationWorker::class.java).setInputData(data.build()).build()
WorkManager.getInstance().enqueue(deleteConversationWorker)
WorkManager.getInstance(context).getWorkInfoByIdLiveData(deleteConversationWorker.id)
.observeForever { workInfo: WorkInfo? ->
if (workInfo != null) {
when (workInfo.state) {
WorkInfo.State.SUCCEEDED -> {
val successMessage = String.format(
context.resources.getString(R.string.deleted_conversation),
conversation.displayName
)
Snackbar.make(binding.root, successMessage, Snackbar.LENGTH_LONG).show()
finish()
}
WorkInfo.State.FAILED -> {
val errorMessage = context.resources.getString(R.string.nc_common_error_sorry)
Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_LONG).show()
}
else -> {
}
}
}
}
}
private fun showEventSchedule(): String {
val meetingTimeStamp = currentConversation?.objectId ?: ""
val status = getMeetingSchedule(meetingTimeStamp)
return status
}
private fun getMeetingSchedule(meetingTimeStamp: String): String {
val timestamps = meetingTimeStamp.split("#")
if (timestamps.size != 2) return context.resources.getString(R.string.nc_invalid_time)
val startEpoch = timestamps[ZERO_INDEX].toLong()
val endEpoch = timestamps[ONE_INDEX].toLong()
val startDateTime = Instant.ofEpochSecond(startEpoch).atZone(ZoneId.systemDefault())
val endDateTime = Instant.ofEpochSecond(endEpoch).atZone(ZoneId.systemDefault())
val currentTime = ZonedDateTime.now(ZoneId.systemDefault())
return when {
currentTime.isBefore(startDateTime) -> {
val isToday = startDateTime.toLocalDate().isEqual(currentTime.toLocalDate())
val isTomorrow = startDateTime.toLocalDate().isEqual(currentTime.toLocalDate().plusDays(1))
when {
isToday -> String.format(
context.resources.getString(R.string.nc_today_meeting),
startDateTime.format(DateTimeFormatter.ofPattern("HH:mm"))
)
isTomorrow -> String.format(
context.resources.getString(R.string.nc_tomorrow_meeting),
startDateTime.format(DateTimeFormatter.ofPattern("HH:mm"))
)
else -> startDateTime.format(DateTimeFormatter.ofPattern("MMM d, yyyy, HH:mm"))
}
}
currentTime.isAfter(endDateTime) -> context.resources.getString(R.string.nc_meeting_ended)
else -> context.resources.getString(R.string.nc_ongoing_meeting)
}
}
private fun showSharedItems() {
val intent = Intent(this, SharedItemsActivity::class.java)
intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
@ -3800,5 +4024,7 @@ class ChatActivity :
const val VOICE_MESSAGE_PLAY_ADD_THRESHOLD = 0.1
const val VOICE_MESSAGE_MARK_PLAYED_FACTOR = 20
const val OUT_OF_OFFICE_ALPHA = 76
const val ZERO_INDEX = 0
const val ONE_INDEX = 1
}
}

View File

@ -185,6 +185,11 @@ class ConversationInfoEditActivity : BaseActivity() {
binding.conversationDescription.isEnabled = false
}
if (conversation?.objectType == ConversationEnums.ObjectType.EVENT) {
binding.conversationName.isEnabled = false
binding.conversationDescription.isEnabled = false
}
loadConversationAvatar()
}
@ -271,8 +276,10 @@ class ConversationInfoEditActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.save) {
if (conversation?.objectType != ConversationEnums.ObjectType.EVENT) {
saveConversationNameAndDescription()
}
}
return true
}

View File

@ -210,6 +210,7 @@ class ConversationsListActivity :
private var conversationItemsWithHeader: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private val searchableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var filterableConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var nearFutureEventConversationItems: MutableList<AbstractFlexibleItem<*>> = ArrayList()
private var searchItem: MenuItem? = null
private var chooseAccountItem: MenuItem? = null
private var searchView: SearchView? = null
@ -519,16 +520,29 @@ class ConversationsListActivity :
// Update Conversations
conversationItems.clear()
conversationItemsWithHeader.clear()
nearFutureEventConversationItems.clear()
for (conversation in list) {
if (!futureEvent(conversation)) {
addToNearFutureEventConversationItems(conversation)
}
addToConversationItems(conversation)
}
sortConversations(conversationItems)
sortConversations(conversationItemsWithHeader)
sortConversations(nearFutureEventConversationItems)
if (!hasFilterEnabled() && searchBehaviorSubject.value == false) {
adapter?.updateDataSet(nearFutureEventConversationItems, false)
} else {
// Filter Conversations
if (!hasFilterEnabled()) filterableConversationItems = conversationItems
if (!hasFilterEnabled()) {
filterableConversationItems = conversationItems
}
filterConversation()
adapter?.updateDataSet(filterableConversationItems, false)
}
Handler().postDelayed({ checkToShowUnreadBubble() }, UNREAD_BUBBLE_DELAY.toLong())
// Fetch Open Conversations
@ -550,6 +564,26 @@ class ConversationsListActivity :
return false
}
private fun futureEvent(conversation: ConversationModel): Boolean {
if (!conversation.objectId.contains("#")) {
return false
}
return conversation.objectType == ConversationEnums.ObjectType.EVENT &&
(conversation.objectId.split("#")[0].toLong() - (System.currentTimeMillis() / LONG_1000)) >
AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS
}
fun showOnlyNearFutureEvents() {
sortConversations(nearFutureEventConversationItems)
adapter?.updateDataSet(nearFutureEventConversationItems, false)
adapter?.smoothScrollToPosition(0)
}
private fun addToNearFutureEventConversationItems(conversation: ConversationModel) {
val conversationItem = ConversationItem(conversation, currentUser!!, this, null, viewThemeUtils)
nearFutureEventConversationItems.add(conversationItem)
}
fun filterConversation() {
val accountId = UserIdUtils.getIdForUser(currentUser)
filterState[FilterConversationFragment.UNREAD] = (
@ -837,15 +871,19 @@ class ConversationsListActivity :
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
adapter?.setHeadersShown(false)
searchBehaviorSubject.onNext(false)
if (!hasFilterEnabled()) filterableConversationItems = conversationItemsWithHeader
adapter?.updateDataSet(filterableConversationItems, false)
if (!hasFilterEnabled()) {
adapter?.updateDataSet(nearFutureEventConversationItems, false)
} else {
filterableConversationItems = conversationItems
}
adapter?.hideAllHeaders()
if (searchHelper != null) {
// cancel any pending searches
searchHelper!!.cancelSearch()
}
binding.swipeRefreshLayoutView.isRefreshing = false
searchBehaviorSubject.onNext(false)
binding.swipeRefreshLayoutView.isEnabled = true
searchView!!.onActionViewCollapsed()
@ -2114,5 +2152,7 @@ class ConversationsListActivity :
const val NOTIFICATION_WARNING_DATE_NOT_SET = 0L
const val OFFSET_HEIGHT_DIVIDER: Int = 3
const val ROOM_TYPE_ONE_ONE = "1"
private const val AGE_THRESHOLD_FOR_EVENT_CONVERSATIONS: Long = 57600
const val LONG_1000: Long = 1000
}
}

View File

@ -34,6 +34,7 @@ fun ConversationModel.asEntity() =
unreadMention = unreadMention,
lastMessage = lastMessage?.let { LoganSquare.serialize(lastMessage) },
objectType = objectType,
objectId = objectId,
notificationLevel = notificationLevel,
conversationReadOnlyState = conversationReadOnlyState,
lobbyState = lobbyState,
@ -85,6 +86,7 @@ fun ConversationEntity.asModel() =
lastMessage = lastMessage?.let
{ LoganSquare.parse(lastMessage, ChatMessageJson::class.java) },
objectType = objectType,
objectId = objectId,
notificationLevel = notificationLevel,
conversationReadOnlyState = conversationReadOnlyState,
lobbyState = lobbyState,
@ -135,6 +137,7 @@ fun Conversation.asEntity(accountId: Long) =
unreadMention = unreadMention,
lastMessage = lastMessage?.let { LoganSquare.serialize(lastMessage) },
objectType = objectType,
objectId = objectId,
notificationLevel = notificationLevel,
conversationReadOnlyState = conversationReadOnlyState,
lobbyState = lobbyState,

View File

@ -78,6 +78,7 @@ data class ConversationEntity(
@ColumnInfo(name = "notificationCalls") var notificationCalls: Int = 0,
@ColumnInfo(name = "notificationLevel") var notificationLevel: ConversationEnums.NotificationLevel,
@ColumnInfo(name = "objectType") var objectType: ConversationEnums.ObjectType,
@ColumnInfo(name = "objectId") var objectId: String,
@ColumnInfo(name = "participantType") var participantType: Participant.ParticipantType,
@ColumnInfo(name = "permissions") var permissions: Int = 0,
@ColumnInfo(name = "readOnly") var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState,

View File

@ -55,6 +55,13 @@ object Migrations {
}
}
val MIGRATION_13_14 = object : Migration(13, 14) {
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 13 to 14")
addObjectId(db)
}
}
fun migrateToRoom(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE User_new (" +
@ -265,6 +272,17 @@ object Migrations {
}
}
fun addObjectId(db: SupportSQLiteDatabase) {
try {
db.execSQL(
"ALTER TABLE Conversations " +
"ADD COLUMN objectId TEXT NOT NULL DEFAULT '';"
)
} catch (e: SQLException) {
Log.i("Migrations", "Something went wrong when adding column objectId to table Conversations")
}
}
fun addTempMessagesSupport(db: SupportSQLiteDatabase) {
try {
db.execSQL(

View File

@ -49,9 +49,9 @@ import java.util.Locale
ChatMessageEntity::class,
ChatBlockEntity::class
],
version = 13,
version = 14,
autoMigrations = [
AutoMigration(from = 9, to = 11)
AutoMigration(from = 9, to = 10)
],
exportSchema = true
)
@ -115,7 +115,8 @@ abstract class TalkDatabase : RoomDatabase() {
Migrations.MIGRATION_8_9,
Migrations.MIGRATION_10_11,
Migrations.MIGRATION_11_12,
Migrations.MIGRATION_12_13
Migrations.MIGRATION_12_13,
Migrations.MIGRATION_13_14
)
.allowMainThreadQueries()
.addCallback(

View File

@ -33,6 +33,7 @@ class ConversationModel(
var unreadMention: Boolean = false,
var lastMessage: ChatMessageJson? = null,
var objectType: ConversationEnums.ObjectType,
var objectId: String = "",
var notificationLevel: ConversationEnums.NotificationLevel,
var conversationReadOnlyState: ConversationEnums.ConversationReadOnlyState,
var lobbyState: ConversationEnums.LobbyState,
@ -66,6 +67,7 @@ class ConversationModel(
) {
companion object {
@Suppress("LongMethod")
fun mapToConversationModel(conversation: Conversation, user: User): ConversationModel {
return ConversationModel(
internalId = user.id!!.toString() + "@" + conversation.token,
@ -88,6 +90,7 @@ class ConversationModel(
unreadMention = conversation.unreadMention,
lastMessage = conversation.lastMessage,
objectType = conversation.objectType.let { ConversationEnums.ObjectType.valueOf(it.name) },
objectId = conversation.objectId,
notificationLevel = conversation.notificationLevel.let {
ConversationEnums.NotificationLevel.valueOf(
it.name

View File

@ -79,6 +79,9 @@ data class Conversation(
@JsonField(name = ["objectType"], typeConverter = ConversationObjectTypeConverter::class)
var objectType: ConversationEnums.ObjectType = ConversationEnums.ObjectType.DEFAULT,
@JsonField(name = ["objectId"])
var objectId: String = "",
@JsonField(name = ["notificationLevel"], typeConverter = EnumNotificationLevelConverter::class)
var notificationLevel: ConversationEnums.NotificationLevel = ConversationEnums.NotificationLevel.DEFAULT,

View File

@ -43,6 +43,7 @@ class ConversationEnums {
DEFAULT,
SHARE_PASSWORD,
FILE,
ROOM
ROOM,
EVENT
}
}

View File

@ -15,6 +15,7 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter<ConversationEnu
"share:password" -> ConversationEnums.ObjectType.SHARE_PASSWORD
"room" -> ConversationEnums.ObjectType.ROOM
"file" -> ConversationEnums.ObjectType.FILE
"event" -> ConversationEnums.ObjectType.EVENT
else -> ConversationEnums.ObjectType.DEFAULT
}
}
@ -28,6 +29,7 @@ class ConversationObjectTypeConverter : StringBasedTypeConverter<ConversationEnu
ConversationEnums.ObjectType.SHARE_PASSWORD -> "share:password"
ConversationEnums.ObjectType.ROOM -> "room"
ConversationEnums.ObjectType.FILE -> "file"
ConversationEnums.ObjectType.EVENT -> "event"
else -> ""
}
}

View File

@ -7,6 +7,7 @@
package com.nextcloud.talk.ui.dialog
import android.app.Dialog
import android.content.DialogInterface
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
@ -99,6 +100,14 @@ class FilterConversationFragment : DialogFragment() {
}
binding.buttonClose.setOnClickListener {
val noFiltersActive = !(
filterState[MENTION] == true ||
filterState[UNREAD] == true ||
filterState[ARCHIVE] == true
)
if (noFiltersActive) {
(requireActivity() as ConversationsListActivity).showOnlyNearFutureEvents()
}
dismiss()
}
}
@ -130,6 +139,18 @@ class FilterConversationFragment : DialogFragment() {
(requireActivity() as ConversationsListActivity).filterConversation()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
val noFiltersActive = !(
filterState[MENTION] == true ||
filterState[UNREAD] == true ||
filterState[ARCHIVE] == true
)
if (noFiltersActive) {
(requireActivity() as ConversationsListActivity).showOnlyNearFutureEvents()
}
}
companion object {
private const val FILTER_STATE_ARG = "FILTER_STATE_ARG"

View File

@ -0,0 +1,19 @@
<!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kch@gmail.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="20dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="20dp">
<path android:fillColor="@android:color/white"
android:pathData="M20,3h-1L19,1h-2v2L7,3L7,1L5,1v2L4,3c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,5c0,-1.1 -0.9,-2 -2,-2zM20,21L4,21L4,8h16v13z"/>
</vector>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2025 Sowjanya Kota <sowjanya.kota@gmail.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/popup_menu_color">
<TextView
android:id="@+id/event_scheduled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nc_event_schedule"
android:textStyle="bold"
android:textSize="18sp" />
<TextView
android:id="@+id/meetingTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp"
tools:text="Meeting at 8:00 pm"/>
<TextView
android:id="@+id/delete_conversation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nc_delete_call"
android:textColor="@android:color/holo_red_dark"
android:visibility = "gone"
android:textSize="18sp"
android:paddingTop="24dp"/>
<TextView
android:id="@+id/archive_conversation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/archive_conversation"
android:visibility = "gone"
android:textSize="18sp"
android:paddingTop="24dp"/>
<TextView
android:id="@+id/unarchive_conversation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/unarchive_conversation"
android:visibility = "gone"
android:textSize="18sp"
android:paddingTop="24dp"/>
</LinearLayout>

View File

@ -8,36 +8,45 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/conversation_event"
android:icon="@drawable/baseline_calendar_today_24"
android:orderInCategory="0"
android:title="@string/nc_event_conversation_menu"
app:showAsAction="ifRoom">
</item>
<item
android:id="@+id/conversation_voice_call"
android:icon="@drawable/ic_call_white_24dp"
android:orderInCategory="0"
android:orderInCategory="1"
android:title="@string/nc_conversation_menu_voice_call"
app:showAsAction="ifRoom" />
<item
android:id="@+id/conversation_video_call"
android:icon="@drawable/ic_videocam_white_24px"
android:orderInCategory="1"
android:orderInCategory="2"
android:title="@string/nc_conversation_menu_video_call"
app:showAsAction="ifRoom" />
<item
android:id="@+id/conversation_search"
android:icon="@drawable/ic_search_white_24dp"
android:orderInCategory="2"
android:orderInCategory="3"
android:title="@string/nc_search"
app:showAsAction="ifRoom" />
<item
android:id="@+id/conversation_info"
android:orderInCategory="3"
android:orderInCategory="4"
android:title="@string/nc_conversation_menu_conversation_info"
app:showAsAction="never" />
<item
android:id="@+id/shared_items"
android:orderInCategory="4"
android:orderInCategory="5"
android:title="@string/nc_shared_items"
app:showAsAction="never" />
</menu>

View File

@ -12,6 +12,7 @@
<color name="colorPrimaryDark">#006AA3</color>
<color name="colorAccent">@color/colorPrimary</color>
<color name="disabled_text">#ff6F6F6F</color>
<color name="popup_menu_color">#FF37474F</color>
<!-- App bar -->
<color name="appbar">#1E1E1E</color>

View File

@ -13,6 +13,8 @@
<color name="disabled_text">#ff888888</color>
<color name="textColorOnPrimaryBackground">#ffffff</color> <!-- white/black depending on primary color -->
<color name="nc_login_text_color">#B3FFFFFF</color>
<color name="popup_menu_color">#FF607D8B</color>
<!-- App bar -->
<color name="appbar">@android:color/white</color>

View File

@ -242,8 +242,14 @@ How to translate with transifex:
<string name="nc_rename">Rename conversation</string>
<string name="nc_rename_confirm">Rename</string>
<string name="nc_delete_call">Delete conversation</string>
<string name="nc_event_schedule">Schedule</string>
<string name="nc_delete">Delete</string>
<string name="nc_delete_all">Delete all</string>
<string name="nc_ongoing_meeting">Ongoing meeting</string>
<string name="nc_meeting_ended">Meeting ended</string>
<string name="nc_invalid_time">Invalid time</string>
<string name="nc_today_meeting">Today at %1$s</string>
<string name="nc_tomorrow_meeting">Tomorrow at %1$s</string>
<string name="nc_delete_conversation_more">If you delete the conversation, it will also be deleted for all other participants.</string>
<string name="nc_new_conversation">New conversation</string>
@ -403,6 +409,7 @@ How to translate with transifex:
<string name="nc_date_header_today">Today</string>
<string name="nc_conversation_menu_voice_call">Voice call</string>
<string name="nc_conversation_menu_video_call">Video call</string>
<string name="nc_event_conversation_menu">Event conversation menu</string>
<string name="nc_conversation_menu_conversation_info">Conversation info</string>
<string name="nc_new_messages">Unread messages</string>
<string name="nc_sent_a_gif" formatted="true">%1$s sent a GIF.</string>
@ -843,4 +850,5 @@ How to translate with transifex:
<string name="no_conversations_archived">No archived conversations</string>
<string name="archived_conversation">Archived %1$s</string>
<string name="unarchived_conversation">Unarchived %1$s</string>
<string name="conversation_archived">Conversation is archived</string>
</resources>