mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 02:25:21 +00:00
Merge branch 'main' of ssh://github.com/permissionlesstech/bitchat-android into prudhvir3ddy/upgrade_deps
This commit is contained in:
@@ -9,11 +9,11 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: "Checklist"
|
label: "Checklist"
|
||||||
options:
|
options:
|
||||||
- label: "I made sure that there are *no existing issues or discussions* - [open](https://github.com/callebtc/bitchat-android/issues) or [closed](https://github.com/callebtc/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
- label: "I made sure that there are *no existing issues or discussions* - [open](https://github.com/permissionlesstech/bitchat-android/issues) or [closed](https://github.com/permissionlesstech/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||||
required: true
|
required: true
|
||||||
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
|
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
|
||||||
required: true
|
required: true
|
||||||
- label: "I have read and understood the [technical architecture](https://github.com/callebtc/bitchat-android/blob/main/README.md#technical-architecture)."
|
- label: "I have read and understood the [technical architecture](https://github.com/permissionlesstech/bitchat-android/blob/main/README.md#technical-architecture)."
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: "Checklist"
|
label: "Checklist"
|
||||||
options:
|
options:
|
||||||
- label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/callebtc/bitchat-android/releases/latest)."
|
- label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/permissionlesstech/bitchat-android/releases/latest)."
|
||||||
required: true
|
required: true
|
||||||
- label: "I made sure that there are *no existing issues* - [open](https://github.com/callebtc/bitchat-android/issues) or [closed](https://github.com/callebtc/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
- label: "I made sure that there are *no existing issues* - [open](https://github.com/permissionlesstech/bitchat-android/issues) or [closed](https://github.com/permissionlesstech/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
|
||||||
required: true
|
required: true
|
||||||
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
|
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
|
||||||
required: true
|
required: true
|
||||||
- label: "This issue contains only one bug."
|
- label: "This issue contains only one bug."
|
||||||
required: true
|
required: true
|
||||||
- label: "I have read and understood the [contribution guidelines](https://github.com/callebtc/bitchat-android/blob/main/README.md#contributing)."
|
- label: "I have read and understood the [contribution guidelines](https://github.com/permissionlesstech/bitchat-android/blob/main/README.md#contributing)."
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Checklist
|
label: Checklist
|
||||||
options:
|
options:
|
||||||
- label: I have used the search function for [**OPEN**](https://github.com/callebtc/bitchat-android/issues) issues to see if someone else has already submitted the same feature request.
|
- label: I have used the search function for [**OPEN**](https://github.com/permissionlesstech/bitchat-android/issues) issues to see if someone else has already submitted the same feature request.
|
||||||
required: true
|
required: true
|
||||||
- label: I have **also** used the search function for [**CLOSED**](https://github.com/callebtc/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) issues to see if the feature was already implemented and is just waiting to be released, or if the feature was rejected.
|
- label: I have **also** used the search function for [**CLOSED**](https://github.com/permissionlesstech/bitchat-android/issues?q=is%3Aissue+is%3Aclosed) issues to see if the feature was already implemented and is just waiting to be released, or if the feature was rejected.
|
||||||
required: true
|
required: true
|
||||||
- label: I will describe the problem with as much detail as possible.
|
- label: I will describe the problem with as much detail as possible.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
please make sure that you have done all of the following.
|
please make sure that you have done all of the following.
|
||||||
You can tick the boxes below by placing an x inside the brackets like this: [x]
|
You can tick the boxes below by placing an x inside the brackets like this: [x]
|
||||||
-->
|
-->
|
||||||
- [ ] I have read the contribution guidelines: <https://github.com/callebtc/bitchat-android?tab=readme-ov-file#contributing>
|
- [ ] I have read the contribution guidelines: <https://github.com/permissionlesstech/bitchat-android?tab=readme-ov-file#contributing>
|
||||||
- [ ] I have performed a self-review of my code
|
- [ ] I have performed a self-review of my code
|
||||||
<!-- - [ ] I have run the automated code checks using `./gradlew checkstyle spotbugsPlayDebug spotbugsDebug :app:lintPlayDebug` -->
|
<!-- - [ ] I have run the automated code checks using `./gradlew checkstyle spotbugsPlayDebug spotbugsDebug :app:lintPlayDebug` -->
|
||||||
- [ ] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see <https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue>)
|
- [ ] I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see <https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue>)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Android Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/gradle-build-action@v2
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew build
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `/w` command now displays user nicknames instead of peer IDs
|
||||||
|
|
||||||
|
## [0.5.1] - 2025-07-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Bluetooth startup check with user prompt to enable Bluetooth if disabled
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Improved Bluetooth initialization reliability on first app launch
|
||||||
|
|
||||||
|
## [0.5] - 2025-07-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New user onboarding screen with permission explanations
|
||||||
|
- Educational content explaining why each permission is required
|
||||||
|
- Privacy assurance messaging (no tracking, no servers, local-only data)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Comprehensive permission validation - ensures all required permissions are granted
|
||||||
|
- Proper Bluetooth stack initialization on first app load
|
||||||
|
- Eliminated need for manual app restart after installation
|
||||||
|
- Enhanced permission request coordination and error handling
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improved first-time user experience with guided setup flow
|
||||||
|
|
||||||
|
## [0.4] - 2025-07-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Push notifications for direct messages
|
||||||
|
- Enhanced notification system with proper click handling and grouping
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Direct message (DM) view with better user interface
|
||||||
|
- Enhanced private messaging experience
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
- Favorite peer functionality currently broken
|
||||||
|
|
||||||
|
## [0.3] - 2025-07-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Battery-aware scanning policies for improved power management
|
||||||
|
- Dynamic scan behavior based on device battery state
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Android-to-Android Bluetooth Low Energy connections
|
||||||
|
- Peer discovery reliability between Android devices
|
||||||
|
- Connection stability improvements
|
||||||
|
|
||||||
|
## [0.2] - 2025-07-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial Android implementation of bitchat protocol
|
||||||
|
- Bluetooth Low Energy mesh networking
|
||||||
|
- End-to-end encryption for private messages
|
||||||
|
- Channel-based messaging with password protection
|
||||||
|
- Store-and-forward message delivery
|
||||||
|
- IRC-style commands (/msg, /join, /clear, etc.)
|
||||||
|
- RSSI-based signal quality indicators
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Various Bluetooth handling improvements
|
||||||
|
- User interface refinements
|
||||||
|
- Connection reliability enhancements
|
||||||
|
|
||||||
|
## [0.1] - 2025-07-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release of bitchat Android client
|
||||||
|
- Basic mesh networking functionality
|
||||||
|
- Core messaging features
|
||||||
|
- Protocol compatibility with iOS bitchat client
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/permissionlesstech/bitchat-android/compare/0.5.1...HEAD
|
||||||
|
[0.5.1]: https://github.com/permissionlesstech/bitchat-android/compare/0.5...0.5.1
|
||||||
|
[0.5]: https://github.com/permissionlesstech/bitchat-android/compare/0.4...0.5
|
||||||
|
[0.4]: https://github.com/permissionlesstech/bitchat-android/compare/0.3...0.4
|
||||||
|
[0.3]: https://github.com/permissionlesstech/bitchat-android/compare/0.2...0.3
|
||||||
|
[0.2]: https://github.com/permissionlesstech/bitchat-android/compare/0.1...0.2
|
||||||
|
[0.1]: https://github.com/permissionlesstech/bitchat-android/releases/tag/0.1
|
||||||
@@ -13,7 +13,7 @@ This is the **Android port** of the original [bitchat iOS app](https://github.co
|
|||||||
|
|
||||||
## Install bitchat
|
## Install bitchat
|
||||||
|
|
||||||
You can download the latest version of bitchat for Android from the [GitHub Releases page](https://github.com/callebtc/bitchat-android/releases).
|
You can download the latest version of bitchat for Android from the [GitHub Releases page](https://github.com/permissionlesstech/bitchat-android/releases).
|
||||||
|
|
||||||
**Instructions:**
|
**Instructions:**
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ This project is released into the public domain. See the [LICENSE](LICENSE.md) f
|
|||||||
|
|
||||||
1. **Clone the repository:**
|
1. **Clone the repository:**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/your-username/bitchat-android.git
|
git clone https://github.com/permissionlesstech/bitchat-android.git
|
||||||
cd bitchat-android
|
cd bitchat-android
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
|
|||||||
@@ -30,10 +30,11 @@ fun PermissionExplanationScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
.verticalScroll(scrollState),
|
.verticalScroll(scrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
// Header
|
// Header
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -164,6 +165,7 @@ fun PermissionExplanationScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -285,8 +285,20 @@ class ChannelManager(
|
|||||||
state.setPasswordPromptChannel(null)
|
state.setPasswordPromptChannel(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setChannelPassword(channel: String, password: String): Boolean {
|
fun setChannelPassword(channel: String, password: String) {
|
||||||
return verifyChannelPassword(channel, password)
|
|
||||||
|
channelPasswords[channel] = password
|
||||||
|
|
||||||
|
channelKeys[channel] = deriveChannelKey(password, channel)
|
||||||
|
|
||||||
|
state.setPasswordProtectedChannels(
|
||||||
|
state.getPasswordProtectedChannelsValue().toMutableSet().apply { add(channel) }
|
||||||
|
)
|
||||||
|
|
||||||
|
dataManager.saveChannelData(
|
||||||
|
state.getJoinedChannelsValue(),
|
||||||
|
state.getPasswordProtectedChannelsValue()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Emergency Clear
|
// MARK: - Emergency Clear
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ fun PeerCounter(
|
|||||||
imageVector = Icons.Filled.Email,
|
imageVector = Icons.Filled.Email,
|
||||||
contentDescription = "Unread private messages",
|
contentDescription = "Unread private messages",
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = Color(0xFFFF8C00) // Orange to match private message theme
|
tint = Color(0xFFFF9500) // Orange to match private message theme
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
}
|
}
|
||||||
@@ -201,19 +201,18 @@ private fun PrivateChatHeader(
|
|||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val peerNickname = peerNicknames[peerID] ?: peerID
|
val peerNickname = peerNicknames[peerID] ?: peerID
|
||||||
|
|
||||||
Row(
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
// Back button - positioned all the way to the left with minimal margin
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// Fixed: Make back button wider to prevent text cropping
|
|
||||||
Button(
|
Button(
|
||||||
onClick = onBackClick,
|
onClick = onBackClick,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
contentColor = colorScheme.primary
|
contentColor = colorScheme.primary
|
||||||
),
|
),
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 4.dp), // Reduced horizontal padding
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.offset(x = (-8).dp) // Move even further left to minimize margin
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@@ -233,27 +232,30 @@ private fun PrivateChatHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
// Title - perfectly centered regardless of other elements
|
||||||
|
Row(
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Lock,
|
imageVector = Icons.Filled.Lock,
|
||||||
contentDescription = "Private chat",
|
contentDescription = "Private chat",
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = Color(0xFFFF8C00) // Orange to match private message theme
|
tint = Color(0xFFFF9500) // Orange to match private message theme
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = peerNickname,
|
text = peerNickname,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = Color(0xFFFF8C00) // Orange
|
color = Color(0xFFFF9500) // Orange
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
// Favorite button - positioned on the right
|
||||||
|
IconButton(
|
||||||
// Favorite button with proper filled/outlined star
|
onClick = onToggleFavorite,
|
||||||
IconButton(onClick = onToggleFavorite) {
|
modifier = Modifier.align(Alignment.CenterEnd)
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
|
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
|
||||||
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
|
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
|
||||||
@@ -273,12 +275,19 @@ private fun ChannelHeader(
|
|||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
Row(
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
// Back button - positioned all the way to the left with minimal margin
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
Button(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
onClick = onBackClick,
|
||||||
) {
|
colors = ButtonDefaults.buttonColors(
|
||||||
IconButton(onClick = onBackClick) {
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = colorScheme.primary
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 4.dp), // Reduced horizontal padding
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.offset(x = (-8).dp) // Move even further left to minimize margin
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@@ -297,18 +306,21 @@ private fun ChannelHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
// Title - perfectly centered regardless of other elements
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "channel: $channel",
|
text = "channel: $channel",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = Color(0xFF0080FF), // Blue
|
color = Color(0xFFFF9500), // Orange to match input field
|
||||||
modifier = Modifier.clickable { onSidebarClick() }
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.clickable { onSidebarClick() }
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
// Leave button - positioned on the right
|
||||||
|
TextButton(
|
||||||
TextButton(onClick = onLeaveChannel) {
|
onClick = onLeaveChannel,
|
||||||
|
modifier = Modifier.align(Alignment.CenterEnd)
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "leave",
|
text = "leave",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ private fun appendFormattedContent(
|
|||||||
}
|
}
|
||||||
"mention" -> {
|
"mention" -> {
|
||||||
builder.pushStyle(SpanStyle(
|
builder.pushStyle(SpanStyle(
|
||||||
color = Color(0xFFFF8C00), // Orange
|
color = Color(0xFFFF9500), // Orange
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ class CommandProcessor(
|
|||||||
when (cmd) {
|
when (cmd) {
|
||||||
"/j", "/join" -> handleJoinCommand(parts, myPeerID)
|
"/j", "/join" -> handleJoinCommand(parts, myPeerID)
|
||||||
"/m", "/msg" -> handleMessageCommand(parts, meshService)
|
"/m", "/msg" -> handleMessageCommand(parts, meshService)
|
||||||
"/w" -> handleWhoCommand()
|
"/w" -> handleWhoCommand(meshService)
|
||||||
"/clear" -> handleClearCommand()
|
"/clear" -> handleClearCommand()
|
||||||
|
"/pass" -> handlePassCommand(parts, myPeerID)
|
||||||
"/block" -> handleBlockCommand(parts, meshService)
|
"/block" -> handleBlockCommand(parts, meshService)
|
||||||
"/unblock" -> handleUnblockCommand(parts, meshService)
|
"/unblock" -> handleUnblockCommand(parts, meshService)
|
||||||
"/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage)
|
"/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage)
|
||||||
@@ -54,7 +55,8 @@ class CommandProcessor(
|
|||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val channelName = parts[1]
|
val channelName = parts[1]
|
||||||
val channel = if (channelName.startsWith("#")) channelName else "#$channelName"
|
val channel = if (channelName.startsWith("#")) channelName else "#$channelName"
|
||||||
val success = channelManager.joinChannel(channel, null, myPeerID)
|
val password = if (parts.size > 2) parts[2] else null
|
||||||
|
val success = channelManager.joinChannel(channel, password, myPeerID)
|
||||||
if (success) {
|
if (success) {
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
sender = "system",
|
sender = "system",
|
||||||
@@ -127,11 +129,11 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleWhoCommand() {
|
private fun handleWhoCommand(meshService: Any) {
|
||||||
val connectedPeers = state.getConnectedPeersValue()
|
val connectedPeers = state.getConnectedPeersValue()
|
||||||
val peerList = connectedPeers.joinToString(", ") { peerID ->
|
val peerList = connectedPeers.joinToString(", ") { peerID ->
|
||||||
// This would need mesh service access for nicknames
|
// Convert peerID to nickname using the mesh service
|
||||||
peerID // For now just use peer ID
|
getPeerNickname(peerID, meshService)
|
||||||
}
|
}
|
||||||
|
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
@@ -166,6 +168,52 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handlePassCommand(parts: List<String>, peerID: String) {
|
||||||
|
val currentChannel = state.getCurrentChannelValue()
|
||||||
|
|
||||||
|
if (currentChannel == null) {
|
||||||
|
val systemMessage = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = "you must be in a channel to set a password.",
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false
|
||||||
|
)
|
||||||
|
messageManager.addMessage(systemMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.size == 2){
|
||||||
|
if(!channelManager.isChannelCreator(channel = currentChannel, peerID = peerID)){
|
||||||
|
val systemMessage = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = "you must be the channel creator to set a password.",
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false
|
||||||
|
)
|
||||||
|
channelManager.addChannelMessage(currentChannel,systemMessage,null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val newPassword = parts[1]
|
||||||
|
channelManager.setChannelPassword(currentChannel, newPassword)
|
||||||
|
val systemMessage = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = "password changed for channel $currentChannel",
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false
|
||||||
|
)
|
||||||
|
channelManager.addChannelMessage(currentChannel,systemMessage,null)
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
val systemMessage = BitchatMessage(
|
||||||
|
sender = "system",
|
||||||
|
content = "usage: /pass <password>",
|
||||||
|
timestamp = Date(),
|
||||||
|
isRelay = false
|
||||||
|
)
|
||||||
|
channelManager.addChannelMessage(currentChannel,systemMessage,null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleBlockCommand(parts: List<String>, meshService: Any) {
|
private fun handleBlockCommand(parts: List<String>, meshService: Any) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
|
|||||||
@@ -44,20 +44,17 @@ fun MessageInput(
|
|||||||
modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding
|
modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Fixed: Remove arrow from private message input
|
// Remove arrow from both private and channel inputs to match DM style
|
||||||
Text(
|
Text(
|
||||||
text = when {
|
text = "<@$nickname>", // No arrow for both private and channel
|
||||||
selectedPrivatePeer != null -> "<@$nickname>" // Removed arrow for private
|
|
||||||
currentChannel != null -> "<@$nickname> →" // Keep arrow for channels
|
|
||||||
else -> "<@$nickname>"
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium),
|
style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium),
|
||||||
color = when {
|
color = when {
|
||||||
selectedPrivatePeer != null -> Color(0xFFFF8C00) // Orange for private
|
selectedPrivatePeer != null -> Color(0xFFFF9500) // Orange for private
|
||||||
currentChannel != null -> Color(0xFFFF8C00) // Orange if encrypted channel
|
currentChannel != null -> Color(0xFFFF9500) // Orange for channels too
|
||||||
else -> colorScheme.primary
|
else -> colorScheme.primary
|
||||||
},
|
},
|
||||||
fontFamily = FontFamily.Monospace
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
@@ -78,7 +75,7 @@ fun MessageInput(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing
|
Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing
|
||||||
|
|
||||||
// Fixed: Make send button orange in private mode to match nickname color
|
// Update send button to match input field colors
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onSend,
|
onClick = onSend,
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
@@ -87,9 +84,9 @@ fun MessageInput(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(30.dp)
|
.size(30.dp)
|
||||||
.background(
|
.background(
|
||||||
color = if (selectedPrivatePeer != null) {
|
color = if (selectedPrivatePeer != null || currentChannel != null) {
|
||||||
// Orange for private messages to match nickname color
|
// Orange for both private messages and channels to match nickname color
|
||||||
Color(0xFFFF8C00).copy(alpha = 0.75f)
|
Color(0xFFFF9500).copy(alpha = 0.75f)
|
||||||
} else if (colorScheme.background == Color.Black) {
|
} else if (colorScheme.background == Color.Black) {
|
||||||
Color(0xFF00FF00).copy(alpha = 0.75f) // Bright green for dark theme
|
Color(0xFF00FF00).copy(alpha = 0.75f) // Bright green for dark theme
|
||||||
} else {
|
} else {
|
||||||
@@ -100,11 +97,11 @@ fun MessageInput(
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.KeyboardArrowUp,
|
imageVector = Icons.Filled.KeyboardArrowRight,
|
||||||
contentDescription = "Send message",
|
contentDescription = "Send message",
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
tint = if (selectedPrivatePeer != null) {
|
tint = if (selectedPrivatePeer != null || currentChannel != null) {
|
||||||
// Black arrow on orange in private mode
|
// Black arrow on orange for both private and channel modes
|
||||||
Color.Black
|
Color.Black
|
||||||
} else if (colorScheme.background == Color.Black) {
|
} else if (colorScheme.background == Color.Black) {
|
||||||
Color.Black // Black arrow on bright green in dark theme
|
Color.Black // Black arrow on bright green in dark theme
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ fun ChannelsSection(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "#$channel",
|
text = channel, // Channel already contains the # prefix
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = if (isSelected) colorScheme.primary else colorScheme.onSurface,
|
color = if (isSelected) colorScheme.primary else colorScheme.onSurface,
|
||||||
fontWeight = if (isSelected) FontWeight.Medium else FontWeight.Normal,
|
fontWeight = if (isSelected) FontWeight.Medium else FontWeight.Normal,
|
||||||
@@ -320,7 +320,7 @@ private fun PeerItem(
|
|||||||
imageVector = Icons.Filled.Email,
|
imageVector = Icons.Filled.Email,
|
||||||
contentDescription = "Unread messages",
|
contentDescription = "Unread messages",
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = Color(0xFFFF8C00) // Orange to match private message theme
|
tint = Color(0xFFFF9500) // Orange to match private message theme
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Signal strength indicators
|
// Signal strength indicators
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:name="vector"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
android:name="path"
|
||||||
|
android:pathData="M 40 45 L 68 45 C 70.2 45 72 46.8 72 49 L 72 59 C 72 61.2 70.2 63 68 63 L 40 63 C 37.8 63 36 61.2 36 59 L 36 49 C 36 46.8 37.8 45 40 45 Z"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
android:name="path_1"
|
||||||
|
android:pathData="M 41 47 L 67 47 C 68.1 47 69 47.9 69 49 L 69 57 C 69 58.1 68.1 59 67 59 L 41 59 C 39.9 59 39 58.1 39 57 L 39 49 C 39 47.9 39.9 47 41 47 Z"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
android:name="path_2"
|
||||||
|
android:pathData="M 43 51 L 46 51 L 46 55 L 43 55 Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
android:name="path_3"
|
||||||
|
android:pathData="M 48 52 L 64 52 L 64 53.5 L 48 53.5 Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
android:name="path_4"
|
||||||
|
android:pathData="M 48 54.5 L 60 54.5 L 60 56 L 48 56 Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
android:name="path_5"
|
||||||
|
android:pathData="M 62 38 M 60.5 38 C 60.5 37.602 60.658 37.221 60.939 36.939 C 61.221 36.658 61.602 36.5 62 36.5 C 62.398 36.5 62.779 36.658 63.061 36.939 C 63.342 37.221 63.5 37.602 63.5 38 C 63.5 38.398 63.342 38.779 63.061 39.061 C 62.779 39.342 62.398 39.5 62 39.5 C 61.602 39.5 61.221 39.342 60.939 39.061 C 60.658 38.779 60.5 38.398 60.5 38"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
android:name="path_6"
|
||||||
|
android:pathData="M 58.5 35 M 57.5 35 C 57.5 34.735 57.605 34.48 57.793 34.293 C 57.98 34.105 58.235 34 58.5 34 C 58.765 34 59.02 34.105 59.207 34.293 C 59.395 34.48 59.5 34.735 59.5 35 C 59.5 35.265 59.395 35.52 59.207 35.707 C 59.02 35.895 58.765 36 58.5 36 C 58.235 36 57.98 35.895 57.793 35.707 C 57.605 35.52 57.5 35.265 57.5 35"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
android:name="path_7"
|
||||||
|
android:pathData="M 65.5 35 M 64.5 35 C 64.5 34.735 64.605 34.48 64.793 34.293 C 64.98 34.105 65.235 34 65.5 34 C 65.765 34 66.02 34.105 66.207 34.293 C 66.395 34.48 66.5 34.735 66.5 35 C 66.5 35.265 66.395 35.52 66.207 35.707 C 66.02 35.895 65.765 36 65.5 36 C 65.235 36 64.98 35.895 64.793 35.707 C 64.605 35.52 64.5 35.265 64.5 35"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
android:name="path_8"
|
||||||
|
android:pathData="M 59 35.5 L 62.5 38 L 65 35.5"
|
||||||
|
android:fillColor="#55000000"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
/>
|
||||||
|
</vector>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.bitchat
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a consistent color for a username based on their peer ID or nickname
|
||||||
|
* Returns colors that work well on both light and dark backgrounds
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ColorTest {
|
||||||
|
fun getUsernameColor(identifier: String): Color {
|
||||||
|
// Hash the identifier to get a consistent number
|
||||||
|
val hash = identifier.hashCode().toUInt()
|
||||||
|
|
||||||
|
// Terminal-friendly colors that work on both black and white backgrounds
|
||||||
|
val colors = listOf(
|
||||||
|
Color(0xFF00FF00), // Bright Green
|
||||||
|
Color(0xFF00FFFF), // Cyan
|
||||||
|
Color(0xFFFFFF00), // Yellow
|
||||||
|
Color(0xFFFF00FF), // Magenta
|
||||||
|
Color(0xFF0080FF), // Bright Blue
|
||||||
|
Color(0xFFFF8000), // Orange
|
||||||
|
Color(0xFF80FF00), // Lime Green
|
||||||
|
Color(0xFF8000FF), // Purple
|
||||||
|
Color(0xFFFF0080), // Pink
|
||||||
|
Color(0xFF00FF80), // Spring Green
|
||||||
|
Color(0xFF80FFFF), // Light Cyan
|
||||||
|
Color(0xFFFF8080), // Light Red
|
||||||
|
Color(0xFF8080FF), // Light Blue
|
||||||
|
Color(0xFFFFFF80), // Light Yellow
|
||||||
|
Color(0xFFFF80FF), // Light Magenta
|
||||||
|
Color(0xFF80FF80), // Light Green
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use modulo to get consistent color for same identifier
|
||||||
|
return colors[(hash % colors.size.toUInt()).toInt()]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun is_username_derived_color_consistent() {
|
||||||
|
|
||||||
|
println("Testing username color function:")
|
||||||
|
|
||||||
|
val testUsers = listOf("alice", "bob", "charlie", "diana", "eve")
|
||||||
|
|
||||||
|
testUsers.forEach { user ->
|
||||||
|
val color = getUsernameColor(user)
|
||||||
|
println("User '$user' gets color: ${color.value.toString(16).uppercase()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val `alice'sColor` = getUsernameColor(testUsers[0])
|
||||||
|
val `bob'sColor` = getUsernameColor(testUsers[1])
|
||||||
|
val `charlie'sColor` = getUsernameColor(testUsers[2])
|
||||||
|
val `diana'sColor` = getUsernameColor(testUsers[3])
|
||||||
|
val `eve'sColor` = getUsernameColor(testUsers[4])
|
||||||
|
|
||||||
|
// Test consistency - same user should always get same color
|
||||||
|
println("\nTesting consistency:")
|
||||||
|
repeat(3) {
|
||||||
|
val `alice's_color` = getUsernameColor(testUsers[0])
|
||||||
|
val `bob's_color` = getUsernameColor(testUsers[1])
|
||||||
|
val `charlie's_color` = getUsernameColor(testUsers[2])
|
||||||
|
val `diana's_color` = getUsernameColor(testUsers[3])
|
||||||
|
val `eve's_color` = getUsernameColor(testUsers[4])
|
||||||
|
|
||||||
|
|
||||||
|
assertEquals(`alice'sColor`, `alice's_color`)
|
||||||
|
assertEquals(`bob'sColor`, `bob's_color`)
|
||||||
|
assertEquals(`charlie'sColor`, `charlie's_color`)
|
||||||
|
assertEquals(`diana'sColor`, `diana's_color`)
|
||||||
|
assertEquals(`eve'sColor`, `eve's_color`)
|
||||||
|
|
||||||
|
println("Alice color (test ${it + 1}): ${`alice'sColor`.value.toString(16).uppercase()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a consistent color for a username based on their peer ID or nickname
|
|
||||||
* Returns colors that work well on both light and dark backgrounds
|
|
||||||
*/
|
|
||||||
fun getUsernameColor(identifier: String): Color {
|
|
||||||
// Hash the identifier to get a consistent number
|
|
||||||
val hash = identifier.hashCode().toUInt()
|
|
||||||
|
|
||||||
// Terminal-friendly colors that work on both black and white backgrounds
|
|
||||||
val colors = listOf(
|
|
||||||
Color(0xFF00FF00), // Bright Green
|
|
||||||
Color(0xFF00FFFF), // Cyan
|
|
||||||
Color(0xFFFFFF00), // Yellow
|
|
||||||
Color(0xFFFF00FF), // Magenta
|
|
||||||
Color(0xFF0080FF), // Bright Blue
|
|
||||||
Color(0xFFFF8000), // Orange
|
|
||||||
Color(0xFF80FF00), // Lime Green
|
|
||||||
Color(0xFF8000FF), // Purple
|
|
||||||
Color(0xFFFF0080), // Pink
|
|
||||||
Color(0xFF00FF80), // Spring Green
|
|
||||||
Color(0xFF80FFFF), // Light Cyan
|
|
||||||
Color(0xFFFF8080), // Light Red
|
|
||||||
Color(0xFF8080FF), // Light Blue
|
|
||||||
Color(0xFFFFFF80), // Light Yellow
|
|
||||||
Color(0xFFFF80FF), // Light Magenta
|
|
||||||
Color(0xFF80FF80), // Light Green
|
|
||||||
)
|
|
||||||
|
|
||||||
// Use modulo to get consistent color for same identifier
|
|
||||||
return colors[(hash % colors.size.toUInt()).toInt()]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
println("Testing username color function:")
|
|
||||||
|
|
||||||
val testUsers = listOf("alice", "bob", "charlie", "diana", "eve")
|
|
||||||
|
|
||||||
testUsers.forEach { user ->
|
|
||||||
val color = getUsernameColor(user)
|
|
||||||
println("User '$user' gets color: ${color.value.toString(16).uppercase()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test consistency - same user should always get same color
|
|
||||||
println("\nTesting consistency:")
|
|
||||||
repeat(3) {
|
|
||||||
val aliceColor = getUsernameColor("alice")
|
|
||||||
println("Alice color (test ${it + 1}): ${aliceColor.value.toString(16).uppercase()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user