Compare commits

..

21 Commits

Author SHA1 Message Date
Avently
3cec97c84f android, desktop: moved to Compose 1.6.0 2024-01-26 23:06:02 +07:00
Stanislav Dmitrenko
520d8868ef android: refactor clipboard access to prevent Android error in logs (#3758) 2024-01-26 14:00:53 +00:00
spaced4ndy
7192448303 core: fix invitation as rejected when deleting group (#3759) 2024-01-26 17:56:17 +04:00
spaced4ndy
0f0f65533a android: group welcome message byte limit (#3756)
* android: group welcome message byte limit

* text

* change footer
2024-01-26 09:57:04 +00:00
spaced4ndy
78a38cb080 ios: group welcome message byte limit (#3751)
* ios: group welcome message character limit

* confirmation dialogue key

* use chatJsonLength

* text

* change footer

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-01-26 09:37:49 +00:00
Stanislav Dmitrenko
f102f39147 android, desktop: protocol servers fix (#3755)
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-01-25 20:59:02 +00:00
Stanislav Dmitrenko
cd349e80ce desktop: propertly updating delivery tab of chat item info page (#3752) 2024-01-25 17:51:20 +00:00
Stanislav Dmitrenko
430dc5bd2e ui: don't show context menu on non-sent yet live message (#3754)
* android, desktop: don't show context menu on non-sent yet live message

* ios: don't show context menu on non-sent yet live message

---------

Co-authored-by: Avently <avently@local>
2024-01-25 17:48:40 +00:00
spaced4ndy
3e0b863b64 core: add cChatJsonLength function (#3753) 2024-01-25 18:57:38 +04:00
Stanislav Dmitrenko
d6afee11bc desktop: prevent clicking enter on alert and text field at the same time (#3714) 2024-01-25 14:50:53 +00:00
spaced4ndy
6ef3a9e668 ios: fix welcome view (#3743)
* welcome view (still doesn't keep change on re-open)

* fix

* remove debug log

* rename
2024-01-25 16:08:10 +04:00
spaced4ndy
fbe3353434 ui: fix link preview cancellation (#3750) 2024-01-25 14:58:39 +04:00
Evgeny Poberezkin
d6d2e6e1eb Merge branch 'stable' 2024-01-24 19:55:28 +00:00
Stanislav Dmitrenko
da1d20c17f desktop: alignment for reactions (#3747) 2024-01-24 16:25:44 +00:00
Stanislav Dmitrenko
afc324dc4f android, desktop: marking chat as read if it was set unread (#3746) 2024-01-24 16:24:49 +00:00
Stanislav Dmitrenko
f81e457e09 android: trying to start service again in case it was destroyed (#3745) 2024-01-24 16:22:29 +00:00
Stanislav Dmitrenko
bd30b80e15 desktop: custom time picker (#3741)
* desktop: custom time picker

* text color

* formatting

* changes in UI

* optimization

* desktop: opening SimpleX links inside the app (#3738)

* 5.5: ios 194, android 175, desktop 26

* docs: update downloads page

* ui: fix chat preview showing incorrect timestamp when chat is empty (#3739)

* ui: align call buttons with calls preference (#3740)

* ui: deleted item preview (#3726)

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-01-24 16:15:27 +00:00
spaced4ndy
da9a7f4642 ui: exclude not ready and active contacts from list of contacts to add to group (e.g. simplex team contact) (#3737) 2024-01-24 13:56:38 +04:00
spaced4ndy
838a759a76 ui: deleted item preview (#3726) 2024-01-24 13:44:29 +04:00
spaced4ndy
f1ff27218c ui: align call buttons with calls preference (#3740) 2024-01-24 13:43:18 +04:00
spaced4ndy
8738cf332f ui: fix chat preview showing incorrect timestamp when chat is empty (#3739) 2024-01-24 13:37:29 +04:00
53 changed files with 1392 additions and 891 deletions

View File

@@ -87,7 +87,7 @@ jobs:
uses: actions/checkout@v3
- name: Setup Haskell
uses: haskell/actions/setup@v2
uses: haskell-actions/setup@v2
with:
ghc-version: ${{ matrix.ghc }}
cabal-version: "3.10.1.0"
@@ -197,7 +197,7 @@ jobs:
APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }}
APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }}
run: |
scripts/build-desktop-mac.sh
scripts/ci/build-desktop-mac.sh
path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg)
echo "package_path=$path" >> $GITHUB_OUTPUT
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
@@ -277,9 +277,7 @@ jobs:
# Unix /
# / Windows
# * In powershell multiline commands do not fail if individual commands fail - https://github.community/t/multiline-commands-on-windows-do-not-fail-if-individual-commands-fail/16753
# * And GitHub Actions does not support parameterizing shell in a matrix job - https://github.community/t/using-matrix-to-specify-shell-is-it-possible/17065
# rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing
- name: 'Setup MSYS2'
if: matrix.os == 'windows-latest'

View File

@@ -1172,7 +1172,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
return ChatModel.shared.chats
.compactMap{ $0.chatInfo.contact }
.filter{ !memberContactIds.contains($0.apiId) }
.filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) }
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
}

View File

@@ -65,6 +65,8 @@ struct MarkedDeletedItemView: View {
}
}
// same texts are in markedDeletedText in ChatPreviewView, but it returns String;
// can be refactored into a single function if functions calling these are changed to return same type
var markedDeletedText: LocalizedStringKey {
switch chatItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"

View File

@@ -159,12 +159,13 @@ struct ChatView: View {
switch cInfo {
case let .direct(contact):
HStack {
if contact.allowsFeature(.calls) {
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
if callsPrefEnabled {
callButton(contact, .audio, imageName: "phone")
.disabled(!contact.ready || !contact.active)
}
Menu {
if contact.allowsFeature(.calls) {
if callsPrefEnabled {
Button {
CallController.shared.startCall(contact, .video)
} label: {
@@ -748,7 +749,9 @@ struct ChatView: View {
if ci.meta.editable && !mc.isVoice && !live {
menu.append(editAction(ci))
}
menu.append(viewInfoUIAction(ci))
if !ci.isLiveDummy {
menu.append(viewInfoUIAction(ci))
}
if revealed {
menu.append(hideUIAction())
}

View File

@@ -978,6 +978,9 @@ struct ComposeView: View {
}
private func cancelLinkPreview() {
if let pendingLink = pendingLinkUrl?.absoluteString {
cancelledLinks.insert(pendingLink)
}
if let uri = composeState.linkPreview?.uri.absoluteString {
cancelledLinks.insert(uri)
}

View File

@@ -370,7 +370,11 @@ struct GroupChatInfoView: View {
private func addOrEditWelcomeMessage() -> some View {
NavigationLink {
GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo)
GroupWelcomeView(
groupInfo: $groupInfo,
groupProfile: groupInfo.groupProfile,
welcomeText: groupInfo.groupProfile.description ?? ""
)
.navigationTitle("Welcome message")
.navigationBarTitleDisplayMode(.large)
} label: {

View File

@@ -11,29 +11,32 @@ import SimpleXChat
struct GroupWelcomeView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject private var m: ChatModel
var groupId: Int64
@Binding var groupInfo: GroupInfo
@State private var welcomeText: String = ""
@State var groupProfile: GroupProfile
@State var welcomeText: String
@State private var editMode = true
@FocusState private var keyboardVisible: Bool
@State private var showSaveDialog = false
let maxByteCount = 1200
var body: some View {
VStack {
if groupInfo.canEdit {
editorView()
.modifier(BackButton {
if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) {
if welcomeTextUnchanged() {
dismiss()
} else {
showSaveDialog = true
}
})
.confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) {
Button("Save and update group profile") {
save()
dismiss()
.confirmationDialog(
welcomeTextFitsLimit() ? "Save welcome message?" : "Welcome message is too long",
isPresented: $showSaveDialog
) {
if welcomeTextFitsLimit() {
Button("Save and update group profile") { save() }
}
Button("Exit without saving") { dismiss() }
}
@@ -47,14 +50,15 @@ struct GroupWelcomeView: View {
}
}
.onAppear {
welcomeText = groupInfo.groupProfile.description ?? ""
keyboardVisible = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
keyboardVisible = true
}
}
}
private func textPreview() -> some View {
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
.frame(minHeight: 140, alignment: .topLeading)
.frame(minHeight: 130, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -74,7 +78,7 @@ struct GroupWelcomeView: View {
}
.padding(.horizontal, -5)
.padding(.top, -8)
.frame(height: 140, alignment: .topLeading)
.frame(height: 130, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
}
} else {
@@ -93,6 +97,9 @@ struct GroupWelcomeView: View {
}
.disabled(welcomeText.isEmpty)
copyButton()
} footer: {
Text(!welcomeTextFitsLimit() ? "Message too large" : "")
.foregroundColor(.red)
}
Section {
@@ -113,7 +120,15 @@ struct GroupWelcomeView: View {
Button("Save and update group profile") {
save()
}
.disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil))
.disabled(welcomeTextUnchanged() || !welcomeTextFitsLimit())
}
private func welcomeTextUnchanged() -> Bool {
welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil)
}
private func welcomeTextFitsLimit() -> Bool {
chatJsonLength(welcomeText) <= maxByteCount
}
private func save() {
@@ -123,11 +138,13 @@ struct GroupWelcomeView: View {
if welcome?.count == 0 {
welcome = nil
}
var groupProfileUpdated = groupInfo.groupProfile
groupProfileUpdated.description = welcome
groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated)
m.updateGroup(groupInfo)
welcomeText = welcome ?? ""
groupProfile.description = welcome
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
await MainActor.run {
groupInfo = gInfo
ChatModel.shared.updateGroup(gInfo)
dismiss()
}
} catch let error {
logger.error("apiUpdateGroup error: \(responseError(error))")
}
@@ -137,6 +154,6 @@ struct GroupWelcomeView: View {
struct GroupWelcomeView_Previews: PreviewProvider {
static var previews: some View {
GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData))
GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData)
}
}

View File

@@ -34,7 +34,7 @@ struct ChatPreviewView: View {
HStack(alignment: .top) {
chatPreviewTitle()
Spacer()
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.updatedAt))
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs))
.font(.subheadline)
.frame(minWidth: 60, alignment: .trailing)
.foregroundColor(.secondary)
@@ -171,10 +171,21 @@ struct ChatPreviewView: View {
}
func chatItemPreview(_ cItem: ChatItem) -> Text {
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false)
// same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey;
// can be refactored into a single function if functions calling these are changed to return same type
func markedDeletedText() -> String {
switch cItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): String.localizedStringWithFormat(NSLocalizedString("moderated by %@", comment: "marked deleted chat item preview text"), byGroupMember.displayName)
case .blocked: NSLocalizedString("blocked", comment: "marked deleted chat item preview text")
case .blockedByAdmin: NSLocalizedString("blocked by admin", comment: "marked deleted chat item preview text")
case .deleted, nil: NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
}
}
func attachment() -> String? {
switch cItem.content.msgContent {
case .file: return "doc.fill"

View File

@@ -105,6 +105,11 @@ public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
return nil
}
public func chatJsonLength(_ s: String) -> Int {
var c = s.cString(using: .utf8)!
return Int(chat_json_length(&c))
}
struct ParsedMarkdown: Decodable {
var formattedText: [FormattedText]?
}

View File

@@ -1367,6 +1367,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
public var chatTs: Date {
switch self {
case let .direct(contact): return contact.chatTs ?? contact.updatedAt
case let .group(groupInfo): return groupInfo.chatTs ?? groupInfo.updatedAt
case let .local(noteFolder): return noteFolder.chatTs
case let .contactRequest(contactRequest): return contactRequest.updatedAt
case let .contactConnection(contactConnection): return contactConnection.updatedAt
case .invalidJSON: return .now
}
}
public struct SampleData {
public var direct: ChatInfo
public var group: ChatInfo
@@ -1425,6 +1436,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
public var mergedPreferences: ContactUserPreferences
var createdAt: Date
var updatedAt: Date
var chatTs: Date?
var contactGroupMemberId: Int64?
var contactGrpInvSent: Bool
@@ -1744,6 +1756,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
public var chatSettings: ChatSettings
var createdAt: Date
var updatedAt: Date
var chatTs: Date?
public var id: ChatId { get { "#\(groupId)" } }
public var apiId: Int64 { get { groupId } }
@@ -2049,6 +2062,7 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
public var unread: Bool
var createdAt: Date
public var updatedAt: Date
var chatTs: Date
public var id: ChatId { get { "*\(noteFolderId)" } }
public var apiId: Int64 { get { noteFolderId } }
@@ -2070,7 +2084,8 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
favorite: false,
unread: false,
createdAt: .now,
updatedAt: .now
updatedAt: .now,
chatTs: .now
)
}

View File

@@ -25,6 +25,7 @@ extern char *chat_parse_markdown(char *str);
extern char *chat_parse_server(char *str);
extern char *chat_password_hash(char *pwd, char *salt);
extern char *chat_valid_name(char *name);
extern int chat_json_length(char *str);
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
extern char *chat_decrypt_media(char *key, char *frame, int len);

View File

@@ -1,11 +1,12 @@
package chat.simplex.app
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.*
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.ui.platform.ClipboardManager
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.getUserIdFromIntent
@@ -58,6 +59,17 @@ class MainActivity: FragmentActivity() {
override fun onResume() {
super.onResume()
AppLock.recheckAuthState()
withApi {
delay(1000)
if (!isAppOnForeground) return@withApi
/**
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
* access to clipboard because the app considered in background.
* This will ensure that the app will get the event on resume
* */
val service = getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
chatModel.clipboardHasText.value = service.hasPrimaryClip()
}
}
override fun onPause() {

View File

@@ -97,13 +97,6 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
Lifecycle.Event.ON_RESUME -> {
isAppOnForeground = true
/**
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
* access to clipboard because the app considered in background.
* This will ensure that the app will get the event on resume
* */
val service = androidAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
chatModel.clipboardHasText.value = service.hasPrimaryClip()
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
}
@@ -197,10 +190,18 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
CoroutineScope(Dispatchers.Default).launch {
if (mode == NotificationsMode.SERVICE)
if (mode == NotificationsMode.SERVICE) {
SimplexService.start()
else
// Sometimes, when we change modes fast from one to another, system destroys the service after start.
// We can wait a little and restart the service, and it will work in 100% of cases
delay(2000)
if (!SimplexService.isServiceStarted && appPrefs.notificationsMode.get() == NotificationsMode.SERVICE) {
Log.i(TAG, "Service tried to start but destroyed by system, repeating once more")
SimplexService.start()
}
} else {
SimplexService.safeStopService()
}
}
if (mode != NotificationsMode.PERIODIC) {

View File

@@ -262,7 +262,7 @@ class SimplexService: Service() {
private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE"
private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
private var isServiceStarted = false
var isServiceStarted = false
private var stopAfterStart = false
fun scheduleStart(context: Context) {

View File

@@ -4,6 +4,7 @@ import android.app.*
import android.app.TaskStackBuilder
import android.content.*
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.hardware.display.DisplayManager
import android.media.AudioAttributes
@@ -217,7 +218,7 @@ object NtfManager {
.setGroup(MessageGroup)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setSmallIcon(R.drawable.ntf_icon)
.setLargeIcon(null)
.setLargeIcon(null as Bitmap?)
.setColor(0x88FFFF)
.setAutoCancel(true)
.setVibrate(null)

View File

@@ -0,0 +1,106 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.CustomTimeUnit
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import com.sd.lib.compose.wheel_picker.*
@Composable
actual fun CustomTimePicker(
selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits>
) {
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
}
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
val selectedDuration = remember { mutableStateOf(duration) }
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
val isTriggered = remember { mutableStateOf(false) }
LaunchedEffect(selectedUnit.value) {
// on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue
// (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120),
// selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition
if (isTriggered.value) {
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
if (maxValue != null && selectedDuration.value > maxValue) {
selectedDuration.value = maxValue
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
} else {
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
} else {
isTriggered.value = true
}
}
LaunchedEffect(selectedDuration.value) {
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.spacedBy(0.dp)
) {
Column(Modifier.weight(1f)) {
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
FVerticalWheelPicker(
count = selectedUnitValues.value.count(),
state = durationPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
selectedUnitValues.value[index].toString(),
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(durationPickerState) {
snapshotFlow { durationPickerState.currentIndex }
.collect {
selectedDuration.value = selectedUnitValues.value[it]
}
}
}
Column(Modifier.weight(1f)) {
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
FVerticalWheelPicker(
count = timeUnitsLimits.count(),
state = unitPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
timeUnitsLimits[index].timeUnit.text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(unitPickerState) {
snapshotFlow { unitPickerState.currentIndex }
.collect {
selectedUnit.value = timeUnitsLimits[it].timeUnit
}
}
}
}
}

View File

@@ -66,6 +66,7 @@ extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
@@ -163,6 +164,14 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
return res;
}
JNIEXPORT int JNICALL
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
int res = chat_json_length(_str);
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);

View File

@@ -71,7 +71,7 @@ if(NOT APPLE)
else()
# Without direct linking it can't find hs_init in linking step
add_library( rts SHARED IMPORTED )
FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/deps/libHSrts*_thr-*.${OS_LIB_EXT})
FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libHSrts*_thr-*.${OS_LIB_EXT})
set_target_properties( rts PROPERTIES IMPORTED_LOCATION ${RTSLIB})
target_link_libraries(app-lib rts simplex)

View File

@@ -39,6 +39,7 @@ extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
@@ -173,6 +174,14 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
return res;
}
JNIEXPORT int JNICALL
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = encode_to_utf8_chars(env, str);
int res = chat_json_length(_str);
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
const char *_path = encode_to_utf8_chars(env, path);

View File

@@ -63,8 +63,6 @@ object ChatModel {
val terminalItems = mutableStateOf<List<TerminalItem>>(listOf())
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
// Allows to temporary save servers that are being edited on multiple screens
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
// set when app opened from external intent
@@ -969,6 +967,16 @@ sealed class ChatInfo: SomeChat, NamedChat {
is Group -> groupInfo.chatSettings
else -> null
}
val chatTs: Instant
get() = when(this) {
is Direct -> contact.chatTs ?: contact.updatedAt
is Group -> groupInfo.chatTs ?: groupInfo.updatedAt
is Local -> noteFolder.chatTs
is ContactRequest -> contactRequest.updatedAt
is ContactConnection -> contactConnection.updatedAt
is InvalidJSON -> updatedAt
}
}
@Serializable
@@ -1009,6 +1017,7 @@ data class Contact(
val mergedPreferences: ContactUserPreferences,
override val createdAt: Instant,
override val updatedAt: Instant,
val chatTs: Instant?,
val contactGroupMemberId: Long? = null,
val contactGrpInvSent: Boolean
): SomeChat, NamedChat {
@@ -1077,6 +1086,7 @@ data class Contact(
mergedPreferences = ContactUserPreferences.sampleData,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
chatTs = Clock.System.now(),
contactGrpInvSent = false
)
}
@@ -1204,7 +1214,8 @@ data class GroupInfo (
val hostConnCustomUserProfileId: Long? = null,
val chatSettings: ChatSettings,
override val createdAt: Instant,
override val updatedAt: Instant
override val updatedAt: Instant,
val chatTs: Instant?
): SomeChat, NamedChat {
override val chatType get() = ChatType.Group
override val id get() = "#$groupId"
@@ -1245,7 +1256,8 @@ data class GroupInfo (
hostConnCustomUserProfileId = null,
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
updatedAt = Clock.System.now(),
chatTs = Clock.System.now()
)
}
}
@@ -1507,7 +1519,8 @@ class NoteFolder(
val favorite: Boolean,
val unread: Boolean,
override val createdAt: Instant,
override val updatedAt: Instant
override val updatedAt: Instant,
val chatTs: Instant
): SomeChat, NamedChat {
override val chatType get() = ChatType.Local
override val id get() = "*$noteFolderId"
@@ -1530,7 +1543,8 @@ class NoteFolder(
favorite = false,
unread = false,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
updatedAt = Clock.System.now(),
chatTs = Clock.System.now()
)
}
}

View File

@@ -28,6 +28,7 @@ external fun chatParseMarkdown(str: String): String
external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String
external fun chatValidName(name: String): String
external fun chatJsonLength(str: String): Int
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String

View File

@@ -324,7 +324,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
) {
LaunchedEffect(Unit) {
LaunchedEffect(ciInfo) {
if (ciInfo.memberDeliveryStatuses != null) {
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
}

View File

@@ -68,12 +68,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged()
.onEach { Log.d(TAG, "TODOCHAT: chatId: activeChatId ${activeChat.value?.id} == new chatId $it ${activeChat.value?.id == it} ") }
.filter { it != null && activeChat.value?.id != it }
.filterNotNull()
.collect { chatId ->
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
// Also for situation when chatId changes after clicking in notification, etc
activeChat.value = chatModel.getChat(chatId!!)
Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}")
if (activeChat.value?.id != chatId) {
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
// Also for situation when chatId changes after clicking in notification, etc
activeChat.value = chatModel.getChat(chatId)
Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}")
}
markUnreadChatAsRead(activeChat, chatModel)
}
}
@@ -404,12 +406,15 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
}
ModalManager.end.closeModals()
ModalManager.end.showModal(endButtons = {
ModalManager.end.showModalCloseable(endButtons = {
ShareButton {
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
}
}) {
}) { close ->
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
KeyChangeEffect(chatModel.chatId.value) {
close()
}
}
}
}
@@ -647,7 +652,7 @@ fun ChatInfoToolbar(
}
}
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.mergedPreferences.calls.enabled.forUser) {
if (activeCall == null) {
barButtons.add {
if (appPlatform.isAndroid) {

View File

@@ -583,6 +583,10 @@ fun ComposeView(
}
fun cancelLinkPreview() {
val pendingLink = pendingLinkUrl.value
if (pendingLink != null) {
cancelledLinks.add(pendingLink)
}
val uri = composeState.value.linkPreview?.uri
if (uri != null) {
cancelledLinks.add(uri)

View File

@@ -59,14 +59,6 @@ fun SendMsgView(
) {
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
if (showCustomDisappearingMessageDialog.value) {
CustomDisappearingMessageDialog(
sendMessage = sendMessage,
setShowDialog = { showCustomDisappearingMessageDialog.value = it },
customDisappearingMessageTimePref = customDisappearingMessageTimePref
)
}
Box(Modifier.padding(vertical = 8.dp)) {
val cs = composeState.value
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
@@ -203,6 +195,11 @@ fun SendMsgView(
DefaultDropdownMenu(showDropdown) {
menuItems.forEach { composable -> composable() }
}
CustomDisappearingMessageDialog(
showCustomDisappearingMessageDialog,
sendMessage = sendMessage,
customDisappearingMessageTimePref = customDisappearingMessageTimePref
)
} else {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage)
}
@@ -220,93 +217,43 @@ expect fun VoiceButtonWithoutPermissionByPlatform()
@Composable
private fun CustomDisappearingMessageDialog(
showMenu: MutableState<Boolean>,
sendMessage: (Int?) -> Unit,
setShowDialog: (Boolean) -> Unit,
customDisappearingMessageTimePref: SharedPreference<Int>?
) {
val showCustomTimePicker = remember { mutableStateOf(false) }
if (showCustomTimePicker.value) {
val selectedDisappearingMessageTime = remember {
mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
}
CustomTimePickerDialog(
selectedDisappearingMessageTime,
title = generalGetString(MR.strings.delete_after),
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
confirmButtonAction = { ttl ->
sendMessage(ttl)
customDisappearingMessageTimePref?.set?.invoke(ttl)
setShowDialog(false)
},
cancel = { setShowDialog(false) }
DefaultDropdownMenu(showMenu) {
Text(
generalGetString(MR.strings.send_disappearing_message),
Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING * 1.5f),
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
} else {
@Composable
fun ChoiceButton(
text: String,
onClick: () -> Unit
) {
TextButton(onClick) {
Text(
text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
}
DefaultDialog(onDismissRequest = { setShowDialog(false) }) {
Surface(
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
contentColor = LocalContentColor.current
) {
Box(
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(DEFAULT_PADDING),
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(" ") // centers title
Text(
generalGetString(MR.strings.send_disappearing_message),
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
Icon(
painterResource(MR.images.ic_close),
generalGetString(MR.strings.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(25.dp)
.clickable { setShowDialog(false) }
)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
sendMessage(30)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
sendMessage(60)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
sendMessage(300)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
showCustomTimePicker.value = true
}
}
}
}
ItemAction(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
sendMessage(30)
showMenu.value = false
}
ItemAction(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
sendMessage(60)
showMenu.value = false
}
ItemAction(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
sendMessage(300)
showMenu.value = false
}
ItemAction(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
showMenu.value = false
val selectedDisappearingMessageTime = mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
showCustomTimePickerDialog(
selectedDisappearingMessageTime,
title = generalGetString(MR.strings.delete_after),
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
confirmButtonAction = { ttl ->
sendMessage(ttl)
customDisappearingMessageTimePref?.set?.invoke(ttl)
},
cancel = { showMenu.value = false }
)
}
}
}

View File

@@ -86,7 +86,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
.map { it.chatInfo }
.filterIsInstance<ChatInfo.Direct>()
.map { it.contact }
.filter { it.contactId !in memberContactIds && it.chatViewName.lowercase().contains(s) }
.filter { c -> c.ready && c.active && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) }
.sortedBy { it.displayName.lowercase() }
.toList()
}

View File

@@ -3,6 +3,7 @@ package chat.simplex.common.views.chat.group
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionTextFooter
import SectionView
import TextIconSpaced
import androidx.compose.foundation.layout.*
@@ -14,6 +15,7 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
@@ -27,9 +29,13 @@ import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.chatJsonLength
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.res.MR
import kotlinx.coroutines.delay
private const val maxByteCount = 1200
@Composable
fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) {
var gInfo by remember { mutableStateOf(groupInfo) }
@@ -54,8 +60,11 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
ModalView(
close = {
if (welcomeText.value == gInfo.groupProfile.description || (welcomeText.value == "" && gInfo.groupProfile.description == null)) close()
else showUnsavedChangesAlert({ save(close) }, close)
when {
welcomeTextUnchanged(welcomeText, gInfo) -> close()
!welcomeTextFitsLimit(welcomeText) -> showUnsavedChangesTooLongAlert(close)
else -> showUnsavedChangesAlert({ save(close) }, close)
}
},
) {
GroupWelcomeLayout(
@@ -67,6 +76,14 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
}
}
private fun welcomeTextUnchanged(welcomeText: MutableState<String>, groupInfo: GroupInfo): Boolean {
return welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)
}
private fun welcomeTextFitsLimit(welcomeText: MutableState<String>): Boolean {
return chatJsonLength(welcomeText.value) <= maxByteCount
}
@Composable
private fun GroupWelcomeLayout(
welcomeText: MutableState<String>,
@@ -95,6 +112,13 @@ private fun GroupWelcomeLayout(
} else {
TextPreview(wt.value, linkMode)
}
SectionTextFooter(
if (!welcomeTextFitsLimit(wt)) { generalGetString(MR.strings.message_too_large) } else "",
color = if (welcomeTextFitsLimit(wt)) MaterialTheme.colors.secondary else Color.Red
)
Spacer(Modifier.size(8.dp))
ChangeModeButton(
editMode.value,
click = {
@@ -104,10 +128,18 @@ private fun GroupWelcomeLayout(
)
val clipboard = LocalClipboardManager.current
CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) }
SectionDividerSpaced(maxBottomPadding = false)
Divider(
Modifier.padding(
start = DEFAULT_PADDING_HALF,
top = 8.dp,
end = DEFAULT_PADDING_HALF,
bottom = 8.dp)
)
SaveButton(
save = save,
disabled = wt.value == groupInfo.groupProfile.description || (wt.value == "" && groupInfo.groupProfile.description == null)
disabled = welcomeTextUnchanged(wt, groupInfo) || !welcomeTextFitsLimit(wt)
)
} else {
val clipboard = LocalClipboardManager.current
@@ -182,3 +214,11 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
onDismiss = revert,
)
}
private fun showUnsavedChangesTooLongAlert(revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.welcome_message_is_too_long),
confirmText = generalGetString(MR.strings.exit_without_saving),
onConfirm = revert,
)
}

View File

@@ -103,7 +103,7 @@ fun ChatItemView(
setReaction(cInfo, cItem, !r.userReacted, r.reaction)
}
}
Row(modifier.padding(2.dp)) {
Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) {
ReactionIcon(r.reaction.text, fontSize = 12.sp)
if (r.totalReacted > 1) {
Spacer(Modifier.width(4.dp))
@@ -112,7 +112,6 @@ fun ChatItemView(
fontSize = 11.5.sp,
fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal,
color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = if (appPlatform.isAndroid) Modifier else Modifier.padding(top = 4.dp)
)
}
}
@@ -178,7 +177,8 @@ fun ChatItemView(
fun MsgContentItemDropdownMenu() {
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
when {
cItem.content.msgContent != null -> {
// cItem.id check is a special case for live message chat item which has negative ID while not sent yet
cItem.content.msgContent != null && cItem.id >= 0 -> {
DefaultDropdownMenu(showMenu) {
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
MsgReactionsMenu()
@@ -651,6 +651,23 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo
}
}
@Composable
fun ItemAction(text: String, color: Color = Color.Unspecified, onClick: () -> Unit) {
val finalColor = if (color == Color.Unspecified) {
MenuTextColor
} else color
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
Text(
text,
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = finalColor
)
}
}
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
AlertManager.shared.showAlertDialog(
title = generalGetString(cancelAction.alert.titleId),

View File

@@ -91,7 +91,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState<B
)
}
private fun markedDeletedText(meta: CIMeta): String =
fun markedDeletedText(meta: CIMeta): String =
when (meta.itemDeleted) {
is CIDeleted.Moderated ->
String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName)

View File

@@ -26,6 +26,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chat.item.markedDeletedText
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
@@ -170,7 +171,7 @@ fun ChatPreviewView(
val (text: CharSequence, inlineTextContent) = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
ci.meta.itemDeleted == null -> ci.text to null
else -> generalGetString(MR.strings.marked_deleted_description) to null
else -> markedDeletedText(ci.meta) to null
}
val formattedText = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
@@ -286,7 +287,7 @@ fun ChatPreviewView(
Box(
contentAlignment = Alignment.TopEnd
) {
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt)
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs)
Text(
ts,
color = MaterialTheme.colors.secondary,

View File

@@ -22,6 +22,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
class AlertManager {
@@ -128,6 +129,8 @@ class AlertManager {
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
delay(200)
focusRequester.requestFocus()
}
TextButton(onClick = {
@@ -195,6 +198,8 @@ class AlertManager {
AlertContent(text, hostDevice, extraPadding = true) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
delay(200)
focusRequester.requestFocus()
}
Row(

View File

@@ -1,116 +1,21 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.clickable
import SectionItemView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import androidx.compose.ui.text.style.TextAlign
import chat.simplex.common.model.CustomTimeUnit
import chat.simplex.common.model.timeText
import chat.simplex.res.MR
import com.sd.lib.compose.wheel_picker.*
@Composable
fun CustomTimePicker(
expect fun CustomTimePicker(
selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits
) {
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
}
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
val selectedDuration = remember { mutableStateOf(duration) }
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
val isTriggered = remember { mutableStateOf(false) }
LaunchedEffect(selectedUnit.value) {
// on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue
// (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120),
// selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition
if (isTriggered.value) {
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
if (maxValue != null && selectedDuration.value > maxValue) {
selectedDuration.value = maxValue
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
} else {
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
} else {
isTriggered.value = true
}
}
LaunchedEffect(selectedDuration.value) {
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.spacedBy(0.dp)
) {
Column(Modifier.weight(1f)) {
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
FVerticalWheelPicker(
count = selectedUnitValues.value.count(),
state = durationPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
selectedUnitValues.value[index].toString(),
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(durationPickerState) {
snapshotFlow { durationPickerState.currentIndex }
.collect {
selectedDuration.value = selectedUnitValues.value[it]
}
}
}
Column(Modifier.weight(1f)) {
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
FVerticalWheelPicker(
count = timeUnitsLimits.count(),
state = unitPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
timeUnitsLimits[index].timeUnit.text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(unitPickerState) {
snapshotFlow { unitPickerState.currentIndex }
.collect {
selectedUnit.value = timeUnitsLimits[it].timeUnit
}
}
}
}
}
)
data class TimeUnitLimits(
val timeUnit: CustomTimeUnit,
@@ -141,8 +46,7 @@ data class TimeUnitLimits(
}
}
@Composable
fun CustomTimePickerDialog(
fun showCustomTimePickerDialog(
selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
title: String,
@@ -150,53 +54,26 @@ fun CustomTimePickerDialog(
confirmButtonAction: (Int) -> Unit,
cancel: () -> Unit
) {
DefaultDialog(onDismissRequest = cancel) {
Surface(
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
contentColor = LocalContentColor.current
) {
Box(
contentAlignment = Alignment.Center
AlertManager.shared.showAlertDialogButtonsColumn(
title = title,
onDismissRequest = cancel
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CustomTimePicker(
selection,
timeUnitsLimits
)
SectionItemView({
AlertManager.shared.hideAlert()
confirmButtonAction(selection.value)
}
) {
Column(
modifier = Modifier.padding(DEFAULT_PADDING),
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(" ") // centers title
Text(
title,
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
Icon(
painterResource(MR.images.ic_close),
generalGetString(MR.strings.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(25.dp)
.clickable { cancel() }
)
}
CustomTimePicker(
selection,
timeUnitsLimits
)
TextButton(onClick = { confirmButtonAction(selection.value) }) {
Text(
confirmButtonText,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
}
Text(
confirmButtonText,
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
color = MaterialTheme.colors.primary
)
}
}
}
@@ -220,7 +97,6 @@ fun DropdownCustomTimePickerSettingRow(
val dropdownSelection: MutableState<DropdownSelection> = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) }
val values: MutableState<List<DropdownSelection>> = remember { mutableStateOf(getValues(selection.value)) }
val showCustomTimePicker = remember { mutableStateOf(false) }
fun updateValue(selectedValue: Int?) {
values.value = getValues(selectedValue)
@@ -247,28 +123,22 @@ fun DropdownCustomTimePickerSettingRow(
onSelected = { sel: DropdownSelection ->
when (sel) {
is DropdownSelection.DropdownValue -> updateValue(sel.value)
DropdownSelection.Custom -> showCustomTimePicker.value = true
DropdownSelection.Custom -> {
val selectedCustomTime = mutableStateOf(selection.value ?: 86400)
showCustomTimePickerDialog(
selectedCustomTime,
timeUnitsLimits = customPickerTimeUnitsLimits,
title = customPickerTitle,
confirmButtonText = customPickerConfirmButtonText,
confirmButtonAction = ::updateValue,
cancel = {
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
}
)
}
}
}
)
if (showCustomTimePicker.value) {
val selectedCustomTime = remember { mutableStateOf(selection.value ?: 86400) }
CustomTimePickerDialog(
selectedCustomTime,
timeUnitsLimits = customPickerTimeUnitsLimits,
title = customPickerTitle,
confirmButtonText = customPickerConfirmButtonText,
confirmButtonAction = { time ->
updateValue(time)
showCustomTimePicker.value = false
},
cancel = {
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
showCustomTimePicker.value = false
}
)
}
}
private sealed class DropdownSelection {

View File

@@ -61,10 +61,10 @@ class ModalManager(private val placement: ModalPlacement? = null) {
}
}
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
val data = ModalData()
showCustomModal { close ->
ModalView(close, showClose = showClose, content = { data.content(close) })
ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) })
}
}

View File

@@ -198,16 +198,16 @@ fun <T> SectionItemWithValue(
}
@Composable
fun SectionTextFooter(text: String) {
SectionTextFooter(AnnotatedString(text))
fun SectionTextFooter(text: String, color: Color = MaterialTheme.colors.secondary) {
SectionTextFooter(AnnotatedString(text), color = color)
}
@Composable
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) {
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start, color: Color = MaterialTheme.colors.secondary) {
Text(
text,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
color = MaterialTheme.colors.secondary,
color = color,
lineHeight = 18.sp,
fontSize = 14.sp,
textAlign = textAlign

View File

@@ -47,10 +47,6 @@ fun NetworkAndServersView(
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
LaunchedEffect(Unit) {
chatModel.userSMPServersUnsaved.value = null
}
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
NetworkAndServersLayout(
currentRemoteHost = currentRemoteHost,

View File

@@ -28,19 +28,18 @@ import chat.simplex.res.MR
@Composable
fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) }
var servers by remember(rhId) {
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
}
var servers by remember { stateGetOrPut("servers") { emptyList<ServerCfg>() } }
var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } }
val currServers = remember(rhId) { mutableStateOf(servers) }
val testing = rememberSaveable(rhId) { mutableStateOf(false) }
val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
val saveDisabled = remember {
val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } }
val saveDisabled = remember(servers) {
derivedStateOf {
servers.isEmpty() ||
servers == currServers.value ||
testing.value ||
!servers.all { srv ->
servers.none { srv ->
val address = parseServerAddress(srv.server)
address != null && uniqueAddress(srv, address, servers)
} ||
@@ -49,8 +48,8 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
}
KeyChangeEffect(rhId) {
m.userSMPServersUnsaved.value = null
servers = emptyList()
serversAlreadyLoaded = false
}
LaunchedEffect(rhId) {
@@ -59,8 +58,9 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
if (res != null) {
currServers.value = res.protoServers
presetServers = res.presetServers
if (servers.isEmpty()) {
if (servers.isEmpty() && !serversAlreadyLoaded) {
servers = currServers.value
serversAlreadyLoaded = true
}
}
}
@@ -80,13 +80,11 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
newServers.add(index, updated)
old = updated
servers = newServers
m.userSMPServersUnsaved.value = servers
},
onDelete = {
val newServers = ArrayList(servers)
newServers.removeAt(index)
servers = newServers
m.userSMPServersUnsaved.value = servers
close()
})
}
@@ -125,7 +123,6 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
ScanProtocolServer(rhId) {
close()
servers = servers + it
m.userSMPServersUnsaved.value = servers
}
}
}
@@ -150,13 +147,11 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
testServersJob.value = withLongRunningApi {
testServers(testing, servers, m) {
servers = it
m.userSMPServersUnsaved.value = servers
}
}
},
resetServers = {
servers = currServers.value ?: emptyList()
m.userSMPServersUnsaved.value = null
servers = currServers.value
},
saveSMPServers = {
saveServers(rhId, serverProtocol, currServers, servers, m)
@@ -355,7 +350,6 @@ private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: Muta
withBGApi {
if (m.controller.setUserProtoServers(rhId, protocol, servers)) {
currServers.value = servers
m.userSMPServersUnsaved.value = null
}
afterSave()
}

View File

@@ -1377,9 +1377,11 @@
<!-- GroupWelcomeView.kt -->
<string name="group_welcome_title">Welcome message</string>
<string name="save_welcome_message_question">Save welcome message?</string>
<string name="welcome_message_is_too_long">Welcome message is too long</string>
<string name="save_and_update_group_profile">Save and update group profile</string>
<string name="group_welcome_preview">Preview</string>
<string name="enter_welcome_message">Enter welcome message…</string>
<string name="message_too_large">Message too large</string>
<!-- ConnectionStats -->
<string name="conn_stats_section_title_servers">SERVERS</string>

View File

@@ -0,0 +1,80 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import chat.simplex.common.model.CustomTimeUnit
import chat.simplex.common.ui.theme.DEFAULT_PADDING
@Composable
actual fun CustomTimePicker(
selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits>
) {
val unit = remember {
var res: CustomTimeUnit = CustomTimeUnit.Second
val found = timeUnitsLimits.asReversed().any {
if (selection.value >= it.minValue * it.timeUnit.toSeconds && selection.value <= it.maxValue * it.timeUnit.toSeconds) {
res = it.timeUnit
selection.value = (selection.value / it.timeUnit.toSeconds).coerceIn(it.minValue, it.maxValue) * it.timeUnit.toSeconds
true
} else {
false
}
}
if (!found) {
// If custom interval doesn't fit in any category, set it to 1 second interval
selection.value = 1
}
mutableStateOf(res)
}
val values = remember(unit.value) {
val limit = timeUnitsLimits.first { it.timeUnit == unit.value }
val res = ArrayList<Pair<Int, String>>()
for (i in limit.minValue..limit.maxValue) {
val seconds = i * limit.timeUnit.toSeconds
val desc = i.toString()
res.add(seconds to desc)
}
if (res.none { it.first == selection.value }) {
// Doesn't fit into min..max, put it equal to the closest value
selection.value = selection.value.coerceIn(res.first().first, res.last().first)
//selection.value = res.last { it.first <= selection.value }.first
}
res
}
val units = remember {
val res = ArrayList<Pair<CustomTimeUnit, String>>()
for (unit in timeUnitsLimits) {
res.add(unit.timeUnit to unit.timeUnit.text)
}
res
}
Row(
Modifier.padding(bottom = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
ExposedDropDownSetting(
values,
selection,
textColor = MaterialTheme.colors.onBackground,
enabled = remember { mutableStateOf(true) },
onSelected = { selection.value = it }
)
Spacer(Modifier.width(DEFAULT_PADDING))
ExposedDropDownSetting(
units,
unit,
textColor = MaterialTheme.colors.onBackground,
enabled = remember { mutableStateOf(true) },
onSelected = {
selection.value = selection.value / unit.value.toSeconds * it.toSeconds
unit.value = it
}
)
}
}

View File

@@ -33,4 +33,4 @@ desktop.version_code=26
kotlin.version=1.8.20
gradle.plugin.version=7.4.2
compose.version=1.5.10
compose.version=1.6.0-beta01

695
flake.lock generated
View File

@@ -16,21 +16,6 @@
"type": "github"
}
},
"blank": {
"locked": {
"lastModified": 1625557891,
"narHash": "sha256-O8/MWsPBGhhyPoPLHZAuoZiiHo9q6FLlEeIDEXuj6T4=",
"owner": "divnix",
"repo": "blank",
"rev": "5a5d2684073d9f563072ed07c871d577a6c614a8",
"type": "github"
},
"original": {
"owner": "divnix",
"repo": "blank",
"type": "github"
}
},
"cabal-32": {
"flake": false,
"locked": {
@@ -98,64 +83,6 @@
"type": "github"
}
},
"devshell": {
"inputs": {
"flake-utils": [
"haskellNix",
"tullia",
"std",
"flake-utils"
],
"nixpkgs": [
"haskellNix",
"tullia",
"std",
"nixpkgs"
]
},
"locked": {
"lastModified": 1663445644,
"narHash": "sha256-+xVlcK60x7VY1vRJbNUEAHi17ZuoQxAIH4S4iUFUGBA=",
"owner": "numtide",
"repo": "devshell",
"rev": "e3dc3e21594fe07bdb24bdf1c8657acaa4cb8f66",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"dmerge": {
"inputs": {
"nixlib": [
"haskellNix",
"tullia",
"std",
"nixpkgs"
],
"yants": [
"haskellNix",
"tullia",
"std",
"yants"
]
},
"locked": {
"lastModified": 1659548052,
"narHash": "sha256-fzI2gp1skGA8mQo/FBFrUAtY0GQkAIAaV/V127TJPyY=",
"owner": "divnix",
"repo": "data-merge",
"rev": "d160d18ce7b1a45b88344aa3f13ed1163954b497",
"type": "github"
},
"original": {
"owner": "divnix",
"repo": "data-merge",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
@@ -173,74 +100,34 @@
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1650374568,
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
"lastModified": 1698579227,
"narHash": "sha256-KVWjFZky+gRuWennKsbo6cWyo7c/z/VgCte5pR9pEKg=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f76e870d64779109e41370848074ac4eaa1606ec",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"type": "github"
"inputs": {
"systems": "systems"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"locked": {
"lastModified": 1653893745,
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_4": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -266,33 +153,51 @@
"type": "github"
}
},
"gomod2nix": {
"inputs": {
"nixpkgs": "nixpkgs_2",
"utils": "utils"
},
"ghc98X": {
"flake": false,
"locked": {
"lastModified": 1655245309,
"narHash": "sha256-d/YPoQ/vFn1+GTmSdvbSBSTOai61FONxB4+Lt6w/IVI=",
"owner": "tweag",
"repo": "gomod2nix",
"rev": "40d32f82fc60d66402eb0972e6e368aeab3faf58",
"type": "github"
"lastModified": 1696643148,
"narHash": "sha256-E02DfgISH7EvvNAu0BHiPvl1E5FGMDi0pWdNZtIBC9I=",
"ref": "ghc-9.8",
"rev": "443e870d977b1ab6fc05f47a9a17bc49296adbd6",
"revCount": 61642,
"submodules": true,
"type": "git",
"url": "https://gitlab.haskell.org/ghc/ghc"
},
"original": {
"owner": "tweag",
"repo": "gomod2nix",
"type": "github"
"ref": "ghc-9.8",
"submodules": true,
"type": "git",
"url": "https://gitlab.haskell.org/ghc/ghc"
}
},
"ghc99": {
"flake": false,
"locked": {
"lastModified": 1697054644,
"narHash": "sha256-kKarOuXUaAH3QWv7ASx+gGFMHaHKe0pK5Zu37ky2AL4=",
"ref": "refs/heads/master",
"rev": "f383a242c76f90bcca8a4d7ee001dcb49c172a9a",
"revCount": 62040,
"submodules": true,
"type": "git",
"url": "https://gitlab.haskell.org/ghc/ghc"
},
"original": {
"submodules": true,
"type": "git",
"url": "https://gitlab.haskell.org/ghc/ghc"
}
},
"hackage": {
"flake": false,
"locked": {
"lastModified": 1702340598,
"narHash": "sha256-CC0HI+6iKPtH+8r/ZfcpW5v/OYvL7zMwpr0xfkXV1zU=",
"lastModified": 1702513363,
"narHash": "sha256-kloro9uEe8aYhPMoMjVNq2rfrXNgMOZhOPwVH5DH2K0=",
"owner": "input-output-hk",
"repo": "hackage.nix",
"rev": "24617c569995e38bf3b83b48eec6628a50fdb4fb",
"rev": "a9d931d0398da67846fa257922a924829233cb91",
"type": "github"
},
"original": {
@@ -309,33 +214,40 @@
"cabal-36": "cabal-36",
"cardano-shell": "cardano-shell",
"flake-compat": "flake-compat",
"flake-utils": "flake-utils_2",
"ghc-8.6.5-iohk": "ghc-8.6.5-iohk",
"ghc98X": "ghc98X",
"ghc99": "ghc99",
"hackage": [
"hackage"
],
"hls-1.10": "hls-1.10",
"hls-2.0": "hls-2.0",
"hls-2.2": "hls-2.2",
"hls-2.3": "hls-2.3",
"hls-2.4": "hls-2.4",
"hpc-coveralls": "hpc-coveralls",
"hydra": "hydra",
"iserv-proxy": "iserv-proxy",
"nixpkgs": [
"nixpkgs"
"haskellNix",
"nixpkgs-unstable"
],
"nixpkgs-2003": "nixpkgs-2003",
"nixpkgs-2105": "nixpkgs-2105",
"nixpkgs-2111": "nixpkgs-2111",
"nixpkgs-2205": "nixpkgs-2205",
"nixpkgs-2211": "nixpkgs-2211",
"nixpkgs-2305": "nixpkgs-2305",
"nixpkgs-unstable": "nixpkgs-unstable",
"old-ghc-nix": "old-ghc-nix",
"stackage": "stackage",
"tullia": "tullia"
"stackage": "stackage"
},
"locked": {
"lastModified": 1677975916,
"narHash": "sha256-dbe8lEEPyfzjdRwpePClv7J9p9lQg7BwbBqAMCw4RLw=",
"lastModified": 1701163700,
"narHash": "sha256-sOrewUS3LnzV09nGr7+3R6Q6zsgU4smJc61QsHq+4DE=",
"owner": "input-output-hk",
"repo": "haskell.nix",
"rev": "ab5efd87ce3fd8ade38a01d97693d29a4f1ae7e4",
"rev": "2808bfe3e62e9eb4ee8974cd623a00e1611f302b",
"type": "github"
},
"original": {
@@ -345,6 +257,91 @@
"type": "github"
}
},
"hls-1.10": {
"flake": false,
"locked": {
"lastModified": 1680000865,
"narHash": "sha256-rc7iiUAcrHxwRM/s0ErEsSPxOR3u8t7DvFeWlMycWgo=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "b08691db779f7a35ff322b71e72a12f6e3376fd9",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "1.10.0.0",
"repo": "haskell-language-server",
"type": "github"
}
},
"hls-2.0": {
"flake": false,
"locked": {
"lastModified": 1687698105,
"narHash": "sha256-OHXlgRzs/kuJH8q7Sxh507H+0Rb8b7VOiPAjcY9sM1k=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "783905f211ac63edf982dd1889c671653327e441",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.0.0.1",
"repo": "haskell-language-server",
"type": "github"
}
},
"hls-2.2": {
"flake": false,
"locked": {
"lastModified": 1693064058,
"narHash": "sha256-8DGIyz5GjuCFmohY6Fa79hHA/p1iIqubfJUTGQElbNk=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "b30f4b6cf5822f3112c35d14a0cba51f3fe23b85",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.2.0.0",
"repo": "haskell-language-server",
"type": "github"
}
},
"hls-2.3": {
"flake": false,
"locked": {
"lastModified": 1695910642,
"narHash": "sha256-tR58doOs3DncFehHwCLczJgntyG/zlsSd7DgDgMPOkI=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "458ccdb55c9ea22cd5d13ec3051aaefb295321be",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.3.0.0",
"repo": "haskell-language-server",
"type": "github"
}
},
"hls-2.4": {
"flake": false,
"locked": {
"lastModified": 1696939266,
"narHash": "sha256-VOMf5+kyOeOmfXTHlv4LNFJuDGa7G3pDnOxtzYR40IU=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "362fdd1293efb4b82410b676ab1273479f6d17ee",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.4.0.0",
"repo": "haskell-language-server",
"type": "github"
}
},
"hpc-coveralls": {
"flake": false,
"locked": {
@@ -384,37 +381,14 @@
"type": "indirect"
}
},
"incl": {
"inputs": {
"nixlib": [
"haskellNix",
"tullia",
"std",
"nixpkgs"
]
},
"locked": {
"lastModified": 1669263024,
"narHash": "sha256-E/+23NKtxAqYG/0ydYgxlgarKnxmDbg6rCMWnOBqn9Q=",
"owner": "divnix",
"repo": "incl",
"rev": "ce7bebaee048e4cd7ebdb4cee7885e00c4e2abca",
"type": "github"
},
"original": {
"owner": "divnix",
"repo": "incl",
"type": "github"
}
},
"iserv-proxy": {
"flake": false,
"locked": {
"lastModified": 1670983692,
"narHash": "sha256-avLo34JnI9HNyOuauK5R69usJm+GfW3MlyGlYxZhTgY=",
"lastModified": 1691634696,
"narHash": "sha256-MZH2NznKC/gbgBu8NgIibtSUZeJ00HTLJ0PlWKCBHb0=",
"ref": "hkm/remote-iserv",
"rev": "50d0abb3317ac439a4e7495b185a64af9b7b9300",
"revCount": 10,
"rev": "43a979272d9addc29fbffc2e8542c5d96e993d73",
"revCount": 14,
"type": "git",
"url": "https://gitlab.haskell.org/hamishmack/iserv-proxy.git"
},
@@ -440,32 +414,22 @@
"type": "github"
}
},
"n2c": {
"mac2ios": {
"inputs": {
"flake-utils": [
"haskellNix",
"tullia",
"std",
"flake-utils"
],
"nixpkgs": [
"haskellNix",
"tullia",
"std",
"nixpkgs"
]
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1665039323,
"narHash": "sha256-SAh3ZjFGsaCI8FRzXQyp56qcGdAqgKEfJWPCQ0Sr7tQ=",
"owner": "nlewo",
"repo": "nix2container",
"rev": "b008fe329ffb59b67bf9e7b08ede6ee792f2741a",
"lastModified": 1699767871,
"narHash": "sha256-kxeCUfwC/Vgh2FvVMlBUq0eVx1JvfHyN+5MPKUik9mE=",
"owner": "zw3rk",
"repo": "mobile-core-tools",
"rev": "4dcb77d5ea896d749381806dfab5358851b08951",
"type": "github"
},
"original": {
"owner": "nlewo",
"repo": "nix2container",
"owner": "zw3rk",
"repo": "mobile-core-tools",
"type": "github"
}
},
@@ -490,95 +454,6 @@
"type": "github"
}
},
"nix-nomad": {
"inputs": {
"flake-compat": "flake-compat_2",
"flake-utils": [
"haskellNix",
"tullia",
"nix2container",
"flake-utils"
],
"gomod2nix": "gomod2nix",
"nixpkgs": [
"haskellNix",
"tullia",
"nixpkgs"
],
"nixpkgs-lib": [
"haskellNix",
"tullia",
"nixpkgs"
]
},
"locked": {
"lastModified": 1658277770,
"narHash": "sha256-T/PgG3wUn8Z2rnzfxf2VqlR1CBjInPE0l1yVzXxPnt0=",
"owner": "tristanpemble",
"repo": "nix-nomad",
"rev": "054adcbdd0a836ae1c20951b67ed549131fd2d70",
"type": "github"
},
"original": {
"owner": "tristanpemble",
"repo": "nix-nomad",
"type": "github"
}
},
"nix2container": {
"inputs": {
"flake-utils": "flake-utils_3",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1658567952,
"narHash": "sha256-XZ4ETYAMU7XcpEeAFP3NOl9yDXNuZAen/aIJ84G+VgA=",
"owner": "nlewo",
"repo": "nix2container",
"rev": "60bb43d405991c1378baf15a40b5811a53e32ffa",
"type": "github"
},
"original": {
"owner": "nlewo",
"repo": "nix2container",
"type": "github"
}
},
"nixago": {
"inputs": {
"flake-utils": [
"haskellNix",
"tullia",
"std",
"flake-utils"
],
"nixago-exts": [
"haskellNix",
"tullia",
"std",
"blank"
],
"nixpkgs": [
"haskellNix",
"tullia",
"std",
"nixpkgs"
]
},
"locked": {
"lastModified": 1661824785,
"narHash": "sha256-/PnwdWoO/JugJZHtDUioQp3uRiWeXHUdgvoyNbXesz8=",
"owner": "nix-community",
"repo": "nixago",
"rev": "8c1f9e5f1578d4b2ea989f618588d62a335083c3",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixago",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1657693803,
@@ -645,11 +520,11 @@
},
"nixpkgs-2205": {
"locked": {
"lastModified": 1672580127,
"narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=",
"lastModified": 1685573264,
"narHash": "sha256-Zffu01pONhs/pqH07cjlF10NnMDLok8ix5Uk4rhOnZQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0874168639713f547c05947c76124f78441ea46c",
"rev": "380be19fbd2d9079f677978361792cb25e8a3635",
"type": "github"
},
"original": {
@@ -661,11 +536,11 @@
},
"nixpkgs-2211": {
"locked": {
"lastModified": 1675730325,
"narHash": "sha256-uNvD7fzO5hNlltNQUAFBPlcEjNG5Gkbhl/ROiX+GZU4=",
"lastModified": 1688392541,
"narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b7ce17b1ebf600a72178f6302c77b6382d09323f",
"rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b",
"type": "github"
},
"original": {
@@ -675,6 +550,40 @@
"type": "github"
}
},
"nixpkgs-2305": {
"locked": {
"lastModified": 1695416179,
"narHash": "sha256-610o1+pwbSu+QuF3GE0NU5xQdTHM3t9wyYhB9l94Cd8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "715d72e967ec1dd5ecc71290ee072bcaf5181ed6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-23.05-darwin",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1696019113,
"narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
@@ -693,11 +602,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1675758091,
"narHash": "sha256-7gFSQbSVAFUHtGCNHPF7mPc5CcqDk9M2+inlVPZSneg=",
"lastModified": 1695318763,
"narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "747927516efcb5e31ba03b7ff32f61f6d47e7d87",
"rev": "e12483116b3b51a185a33a272bf351e357ba9a99",
"type": "github"
},
"original": {
@@ -709,82 +618,20 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1653581809,
"narHash": "sha256-Uvka0V5MTGbeOfWte25+tfRL3moECDh1VwokWSZUdoY=",
"lastModified": 1698434055,
"narHash": "sha256-Phxi5mUKSoL7A0IYUiYtkI9e8NcGaaV5PJEaJApU1Ko=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "83658b28fe638a170a19b8933aa008b30640fbd1",
"rev": "1a3c95e3b23b3cdb26750621c08cc2f1560cb883",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"ref": "nixos-23.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1654807842,
"narHash": "sha256-ADymZpr6LuTEBXcy6RtFHcUZdjKTBRTMYwu19WOx17E=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fc909087cc3386955f21b4665731dbdaceefb1d8",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1665087388,
"narHash": "sha256-FZFPuW9NWHJteATOf79rZfwfRn5fE0wi9kRzvGfDHPA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "95fda953f6db2e9496d2682c4fc7b82f959878f7",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1676726892,
"narHash": "sha256-M7OYVR6dKmzmlebIjybFf3l18S2uur8lMyWWnHQooLY=",
"owner": "angerman",
"repo": "nixpkgs",
"rev": "729469087592bdea58b360de59dadf6d58714c42",
"type": "github"
},
"original": {
"owner": "angerman",
"ref": "release-22.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nosys": {
"locked": {
"lastModified": 1667881534,
"narHash": "sha256-FhwJ15uPLRsvaxtt/bNuqE/ykMpNAPF0upozFKhTtXM=",
"owner": "divnix",
"repo": "nosys",
"rev": "2d0d5207f6a230e9d0f660903f8db9807b54814f",
"type": "github"
},
"original": {
"owner": "divnix",
"repo": "nosys",
"type": "github"
}
},
"old-ghc-nix": {
"flake": false,
"locked": {
@@ -807,17 +654,21 @@
"flake-utils": "flake-utils",
"hackage": "hackage",
"haskellNix": "haskellNix",
"nixpkgs": "nixpkgs_5"
"mac2ios": "mac2ios",
"nixpkgs": [
"haskellNix",
"nixpkgs-2305"
]
}
},
"stackage": {
"flake": false,
"locked": {
"lastModified": 1677888571,
"narHash": "sha256-YkhRNOaN6QVagZo1cfykYV8KqkI8/q6r2F5+jypOma4=",
"lastModified": 1699834215,
"narHash": "sha256-g/JKy0BCvJaxPuYDl3QVc4OY8cFEomgG+hW/eEV470M=",
"owner": "input-output-hk",
"repo": "stackage.nix",
"rev": "cb50e6fabdfb2d7e655059039012ad0623f06a27",
"rev": "47aacd04abcce6bad57f43cbbbd133538380248e",
"type": "github"
},
"original": {
@@ -826,110 +677,18 @@
"type": "github"
}
},
"std": {
"inputs": {
"arion": [
"haskellNix",
"tullia",
"std",
"blank"
],
"blank": "blank",
"devshell": "devshell",
"dmerge": "dmerge",
"flake-utils": "flake-utils_4",
"incl": "incl",
"makes": [
"haskellNix",
"tullia",
"std",
"blank"
],
"microvm": [
"haskellNix",
"tullia",
"std",
"blank"
],
"n2c": "n2c",
"nixago": "nixago",
"nixpkgs": "nixpkgs_4",
"nosys": "nosys",
"yants": "yants"
},
"systems": {
"locked": {
"lastModified": 1674526466,
"narHash": "sha256-tMTaS0bqLx6VJ+K+ZT6xqsXNpzvSXJTmogkraBGzymg=",
"owner": "divnix",
"repo": "std",
"rev": "516387e3d8d059b50e742a2ff1909ed3c8f82826",
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "divnix",
"repo": "std",
"type": "github"
}
},
"tullia": {
"inputs": {
"nix-nomad": "nix-nomad",
"nix2container": "nix2container",
"nixpkgs": [
"haskellNix",
"nixpkgs"
],
"std": "std"
},
"locked": {
"lastModified": 1675695930,
"narHash": "sha256-B7rEZ/DBUMlK1AcJ9ajnAPPxqXY6zW2SBX+51bZV0Ac=",
"owner": "input-output-hk",
"repo": "tullia",
"rev": "621365f2c725608f381b3ad5b57afef389fd4c31",
"type": "github"
},
"original": {
"owner": "input-output-hk",
"repo": "tullia",
"type": "github"
}
},
"utils": {
"locked": {
"lastModified": 1653893745,
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"yants": {
"inputs": {
"nixpkgs": [
"haskellNix",
"tullia",
"std",
"nixpkgs"
]
},
"locked": {
"lastModified": 1667096281,
"narHash": "sha256-wRRec6ze0gJHmGn6m57/zhz/Kdvp9HS4Nl5fkQ+uIuA=",
"owner": "divnix",
"repo": "yants",
"rev": "d18f356ec25cb94dc9c275870c3a7927a10f8c3c",
"type": "github"
},
"original": {
"owner": "divnix",
"repo": "yants",
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}

299
flake.nix
View File

@@ -1,15 +1,15 @@
{
description = "nix flake for simplex-chat";
inputs.nixpkgs.url = "github:angerman/nixpkgs/release-22.11";
inputs.haskellNix.url = "github:input-output-hk/haskell.nix/armv7a";
inputs.haskellNix.inputs.nixpkgs.follows = "nixpkgs";
inputs.nixpkgs.follows = "haskellNix/nixpkgs-2305";
inputs.mac2ios.url = "github:zw3rk/mobile-core-tools";
inputs.hackage = {
url = "github:input-output-hk/hackage.nix";
flake = false;
};
inputs.haskellNix.inputs.hackage.follows = "hackage";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, haskellNix, nixpkgs, flake-utils, ... }:
outputs = { self, haskellNix, nixpkgs, flake-utils, mac2ios, ... }:
let systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; in
flake-utils.lib.eachSystem systems (system:
# this android26 overlay makes the pkgsCross.{aarch64-android,armv7a-android-prebuilt} to set stdVer to 26 (Android 8).
@@ -30,7 +30,7 @@
# `appendOverlays` with a singleton is identical to `extend`.
let pkgs = haskellNix.legacyPackages.${system}.appendOverlays [android26]; in
let drv' = { extra-modules, pkgs', ... }: pkgs'.haskell-nix.project {
compiler-nix-name = "ghc8107";
compiler-nix-name = "ghc963";
index-state = "2023-12-12T00:00:00Z";
# We need this, to specify we want the cabal project.
# If the stack.yaml was dropped, this would not be necessary.
@@ -40,9 +40,12 @@
src = ./.;
};
sha256map = import ./scripts/nix/sha256map.nix;
modules = [{
modules = [
({ pkgs, lib, ...}: lib.mkIf (!pkgs.stdenv.hostPlatform.isWindows) {
# This patch adds `dl` as an extra-library to direct-sqlciper, which is needed
# on pretty much all unix platforms, but then blows up on windows m(
packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ];
}
})
({ pkgs,lib, ... }: lib.mkIf (pkgs.stdenv.hostPlatform.isAndroid) {
packages.simplex-chat.components.library.ghcOptions = [ "-pie" ];
})] ++ extra-modules;
@@ -64,6 +67,9 @@
}); in
let iosPostInstall = bundleName: ''
${pkgs.tree}/bin/tree $out
mkdir tmp
find ./dist -name "libHS*-ghc*.a" -exec cp {} tmp \;
(cd tmp; ${pkgs.tree}/bin/tree .; ar x libHS*.a; for o in *.o; do if /usr/bin/otool -xv $o|grep ldadd ; then echo $o; fi; done; cd ..; rm -fR tmp)
mkdir -p $out/_pkg
# copy over includes, we might want those, but maybe not.
# cp -r $out/lib/*/*/include $out/_pkg/
@@ -74,6 +80,18 @@
find ${pkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \;
# There is no static libc
${pkgs.tree}/bin/tree $out/_pkg
for pkg in $out/_pkg/*.a; do
chmod +w $pkg
${mac2ios.packages.${system}.mac2ios}/bin/mac2ios $pkg
chmod -w $pkg
done
mkdir tmp
find $out/_pkg -name "libHS*-ghc*.a" -exec cp {} tmp \;
(cd tmp; ${pkgs.tree}/bin/tree .; ar x libHS*.a; for o in *.o; do if /usr/bin/otool -xv $o|grep ldadd ; then echo $o; fi; done; cd ..; rm -fR tmp)
sha256sum $out/_pkg/*.a
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/${bundleName}.zip *)
rm -fR $out/_pkg
mkdir -p $out/nix-support
@@ -119,13 +137,149 @@
hardeningDisable = [ "fortify" ];
}
);in {
# STATIC x86_64-linux
"${pkgs.pkgsCross.musl64.hostPlatform.system}-static:exe:simplex-chat" = (drv pkgs.pkgsCross.musl64).simplex-chat.components.exes.simplex-chat;
"${pkgs.pkgsCross.musl32.hostPlatform.system}-static:exe:simplex-chat" = (drv pkgs.pkgsCross.musl32).simplex-chat.components.exes.simplex-chat;
# STATIC i686-linux
"${pkgs.pkgsCross.musl32.hostPlatform.system}-static:exe:simplex-chat" = (drv' {
pkgs' = pkgs.pkgsCross.musl32;
extra-modules = [{
# 32 bit patches
packages.basement.patches = [
./scripts/nix/basement-pr-573.patch
];
packages.memory.patches = [
./scripts/nix/memory-pr-99.patch
];
}];
}).simplex-chat.components.exes.simplex-chat;
# WINDOWS x86_64-mingwW64
"${pkgs.pkgsCross.mingwW64.hostPlatform.system}:exe:simplex-chat" = (drv' {
pkgs' = pkgs.pkgsCross.mingwW64;
extra-modules = [{
packages.direct-sqlcipher.flags.openssl = true;
packages.bitvec.flags.simd = false;
packages.direct-sqlcipher.patches = [
./scripts/nix/direct-sqlcipher-2.3.27-win.patch
];
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
(pkgs.pkgsCross.mingwW64.openssl) #.override) # { static = true; enableKTLS = false; })
];
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
(pkgs.pkgsCross.mingwW64.openssl) #.override) # { static = true; enableKTLS = false; })
];
packages.unix-time.postPatch = ''
sed -i 's/mingwex//g' unix-time.cabal
'';
}];
}).simplex-chat.components.exes.simplex-chat.override {
postInstall = ''
set -x
${pkgs.tree}/bin/tree $out
mkdir -p $out/_pkg
cp $out/bin/* $out/_pkg
${pkgs.tree}/bin/tree $out/_pkg
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/${pkgs.pkgsCross.mingwW64.hostPlatform.system}-simplex-chat.zip *)
rm -fR $out/_pkg
mkdir -p $out/nix-support
echo "file binary-dist \"$(echo $out/*.zip)\"" \
> $out/nix-support/hydra-build-products
'';
};
"${pkgs.pkgsCross.mingwW64.hostPlatform.system}:lib:simplex-chat" = (drv' rec {
pkgs' = pkgs.pkgsCross.mingwW64;
extra-modules = [{
packages.direct-sqlcipher.flags.openssl = true;
# simd will try to read __cpu_model, which we don't expose
# from the rts (yet!).
packages.bitvec.flags.simd = false;
packages.direct-sqlcipher.patches = [
./scripts/nix/direct-sqlcipher-2.3.27-win.patch
];
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
pkgs.pkgsCross.mingwW64.openssl
];
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
pkgs.pkgsCross.mingwW64.openssl
];
packages.unix-time.postPatch = ''
sed -i 's/mingwex//g' unix-time.cabal
'';
}];
}).simplex-chat.components.library
.override (p: {
# enableShared = false;
setupBuildFlags = p.component.setupBuildFlags ++ map (x: "--ghc-option=${x}") [
"-shared"
"-threaded"
"-o" "libsimplex.dll"
# "-optl-lHSrts_thr"
"-optl-lffi"
# "-optl-static-libgcc"
# We can't do -optl-static-libstdc++ with gcc. g++ might
# but then we are chaning the compiler altogether.
"${./libsimplex.dll.def}"
];
postInstall = ''
set -x
function deps() {
${pkgs.binutils}/bin/strings "$1" | grep '.\.dll'|grep -v -E 'Winsock|ADVAPI32|dbghelp|KERNEL32|msvcrt|ntdll|ole32|RPCRT4|SHELL32|USER32|WINMM|WS2_32|kernel32|GDI32'|grep -v "$1"
}
${pkgs.tree}/bin/tree $out
mkdir -p $out/_pkg
cp libsimplex.dll $out/_pkg
cp libsimplex.dll.a $out/_pkg
mkdir $out/libs
find ${pkgs.lib.getBin pkgs.pkgsCross.mingwW64.openssl} -name "*.dll" -exec cp {} $out/libs \;
find ${pkgs.lib.getBin pkgs.pkgsCross.mingwW64.libffi} -name "*.dll" -exec cp {} $out/libs \;
find ${pkgs.lib.getBin pkgs.pkgsCross.mingwW64.gmp} -name "*.dll" -exec cp {} $out/libs \;
find ${pkgs.lib.getBin pkgs.pkgsCross.mingwW64.stdenv.cc.cc} -name "*.dll" -exec cp {} $out/libs \;
find ${pkgs.lib.getBin pkgs.pkgsCross.mingwW64.windows.mcfgthreads} -name "*.dll" -exec cp {} $out/libs \;
pushd $out/_pkg
function copyDeps() {
for dep in $(deps "$1"); do
if [ ! -f "$dep" ]; then
if [ ! -f ../libs/"$dep" ]; then
echo "WARN: $1 -> $dep not found!"
else
cp ../libs/"$dep" .
copyDeps "$dep"
fi
fi
done
}
copyDeps libsimplex.dll
popd
${pkgs.tree}/bin/tree $out/_pkg
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-${pkgs.pkgsCross.mingwW64.hostPlatform.system}-libsimplex.zip *)
rm -fR $out/_pkg
mkdir -p $out/nix-support
echo "file binary-dist \"$(echo $out/*.zip)\"" \
> $out/nix-support/hydra-build-products
'';
});
# "${pkgs.pkgsCross.muslpi.hostPlatform.system}-static:exe:simplex-chat" = (drv pkgs.pkgsCross.muslpi).simplex-chat.components.exes.simplex-chat;
# STATIC aarch64-linux
"${pkgs.pkgsCross.aarch64-multiplatform-musl.hostPlatform.system}-static:exe:simplex-chat" = (drv pkgs.pkgsCross.aarch64-multiplatform-musl).simplex-chat.components.exes.simplex-chat;
"armv7a-android:lib:support" = (drv android32Pkgs).android-support.components.library.override {
smallAddressSpace = true; enableShared = false;
setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsupport.so" ];
"armv7a-android:lib:support" = (drv android32Pkgs).android-support.components.library.override (p: {
smallAddressSpace = true;
# we won't want -dyamic (see aarch64-android:lib:simplex-chat)
enableShared = false;
# we also do not want to have any dependencies listed (especially no rts!)
enableStatic = false;
# This used to work with 8.10.7...
# setupBuildFlags = p.component.setupBuildFlags ++ map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsupport.so" ];
# ... but now with 9.6+
# we have to do the -shared thing by hand.
postBuild = ''
armv7a-unknown-linux-androideabi-ghc -shared -o libsupport.so \
-optl-Wl,-u,setLineBuffering \
-optl-Wl,-u,pipe_std_to_socket \
dist/build/*.a
'';
postInstall = ''
mkdir -p $out/_pkg
@@ -138,14 +292,29 @@
echo "file binary-dist \"$(echo $out/*.zip)\"" \
> $out/nix-support/hydra-build-products
'';
};
"aarch64-android:lib:support" = (drv androidPkgs).android-support.components.library.override {
smallAddressSpace = true; enableShared = false;
setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsupport.so" ];
});
# The android-support package is at
# https://github.com/simplex-chat/android-support
"aarch64-android:lib:support" = (drv androidPkgs).android-support.components.library.override (p: {
smallAddressSpace = true;
# no -dynamic
enableShared = false;
# but also no -staticlib
enableStatic = false;
# we have to do the -shared thing by hand.
postBuild = ''
aarch64-unknown-linux-android-ghc -shared -o libsupport.so \
-optl-Wl,-u,setLineBuffering \
-optl-Wl,-u,pipe_std_to_socket \
dist/build/*.a
'';
postInstall = ''
mkdir -p $out/_pkg
cp libsupport.so $out/_pkg
ls -lah $out/_pkg/*
${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsupport.so
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-aarch64-android-libsupport.zip *)
rm -fR $out/_pkg
@@ -154,10 +323,11 @@
echo "file binary-dist \"$(echo $out/*.zip)\"" \
> $out/nix-support/hydra-build-products
'';
};
});
"armv7a-android:lib:simplex-chat" = (drv' {
pkgs' = android32Pkgs;
extra-modules = [{
packages.text.flags.simdutf = false;
packages.direct-sqlcipher.flags.openssl = true;
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
(android32Pkgs.openssl.override { static = true; enableKTLS = false; })
@@ -168,13 +338,55 @@
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
(android32Pkgs.openssl.override { static = true; enableKTLS = false; })
];
# 32 bit patches
packages.basement.patches = [
./scripts/nix/basement-pr-573.patch
];
packages.memory.patches = [
./scripts/nix/memory-pr-99.patch
];
}];
}).simplex-chat.components.library.override {
smallAddressSpace = true; enableShared = false;
}).simplex-chat.components.library.override (p: {
smallAddressSpace = true;
# we want -shared, but not -dyanmic, hence `enableShared = false`.
enableShared = false;
# we _do_ want rts, and other libs. Hence `enableStatic = true`.
enableStatic = true;
# for android we build a shared library, passing these arguments is a bit tricky, as
# we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for
# template haskell cross compilation. Thus we just pass them as linker options (-optl).
setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsimplex.so" "-optl-lHSrts_thr" "-optl-lffi"];
setupBuildFlags = p.component.setupBuildFlags
# flags to tell GHC we want to produce a -shared object, and we want to also link
# - the ffi library (ffi)
++ map (x: "--ghc-option=${x}") [
"-shared" "-o" "libsimplex.so"
"-threaded"
# "-debug"
"-optl-lffi"
]
# This is fairly idiotic. LLD will strip out foreign exported
# symbols (a GHC bug? Codegen bug?). So we need to pass `-u <sym>`
# to ensure they stay in the produced library. Having them
# _undefined_ and _lazy_ (lld will tell with -y <sym> that the
# symbol is lazy), makes them _defined_. m(
++ map (sym: "--ghc-option=-optl-Wl,-u,${sym}") [
"chat_close_store"
"chat_decrypt_file"
"chat_decrypt_media"
"chat_encrypt_file"
"chat_encrypt_media"
"chat_migrate_init"
"chat_parse_markdown"
"chat_parse_server"
"chat_password_hash"
"chat_read_file"
"chat_recv_msg"
"chat_recv_msg_wait"
"chat_send_cmd"
"chat_send_remote_cmd"
"chat_valid_name"
"chat_write_file"
];
postInstall = ''
set -x
${pkgs.tree}/bin/tree $out
@@ -218,10 +430,11 @@
echo "file binary-dist \"$(echo $out/*.zip)\"" \
> $out/nix-support/hydra-build-products
'';
};
});
"aarch64-android:lib:simplex-chat" = (drv' {
pkgs' = androidPkgs;
extra-modules = [{
packages.text.flags.simdutf = false;
packages.direct-sqlcipher.flags.openssl = true;
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
(androidPkgs.openssl.override { static = true; })
@@ -233,12 +446,49 @@
(androidPkgs.openssl.override { static = true; })
];
}];
}).simplex-chat.components.library.override {
smallAddressSpace = true; enableShared = false;
}).simplex-chat.components.library.override (p: {
smallAddressSpace = true;
# we do not want a dynamically linked object, even though we _do_
# want to produce a _shared_ object. But `shared` implied -dyanmic
# with cabal, so we disable and pass `-shared` explicitly.
enableShared = false;
# we do want static (e.g. pass all dependencies in, so we get -staticlib)
enableStatic = true;
# for android we build a shared library, passing these arguments is a bit tricky, as
# we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for
# template haskell cross compilation. Thus we just pass them as linker options (-optl).
setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsimplex.so" "-optl-lHSrts_thr" "-optl-lffi"];
setupBuildFlags = p.component.setupBuildFlags
# flags to tell GHC we want to produce a -shared object, and we want to also link
# - the ffi library (ffi)
++ map (x: "--ghc-option=${x}") [
"-shared" "-o" "libsimplex.so"
"-threaded"
# "-debug"
"-optl-lffi"
]
# This is fairly idiotic. LLD will strip out foreign exported
# symbols (a GHC bug? Codegen bug?). So we need to pass `-u <sym>`
# to ensure they stay in the produced library. Having them
# _undefined_ and _lazy_ (lld will tell with -y <sym> that the
# symbol is lazy), makes them _defined_. m(
++ map (sym: "--ghc-option=-optl-Wl,-u,${sym}") [
"chat_close_store"
"chat_decrypt_file"
"chat_decrypt_media"
"chat_encrypt_file"
"chat_encrypt_media"
"chat_migrate_init"
"chat_parse_markdown"
"chat_parse_server"
"chat_password_hash"
"chat_read_file"
"chat_recv_msg"
"chat_recv_msg_wait"
"chat_send_cmd"
"chat_send_remote_cmd"
"chat_valid_name"
"chat_write_file"
];
postInstall = ''
set -x
${pkgs.tree}/bin/tree $out
@@ -282,7 +532,7 @@
echo "file binary-dist \"$(echo $out/*.zip)\"" \
> $out/nix-support/hydra-build-products
'';
};
});
};
# builds for iOS and iOS simulator
@@ -296,7 +546,8 @@
packages.direct-sqlcipher.flags.commoncrypto = true;
packages.entropy.flags.DoNotGetEntropy = true;
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
(pkgs.openssl.override { static = true; })
# TODO: have a cross override for iOS, that sets this.
((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;}))
];
}];
}).simplex-chat.components.library.override (

View File

@@ -36,6 +36,7 @@ dependencies:
- network >= 3.1.2.7 && < 3.2
- network-transport == 0.5.6
- optparse-applicative >= 0.15 && < 0.17
- process == 1.6.*
- random >= 1.1 && < 1.3
- record-hasfield == 1.0.*
- simple-logger == 0.1.*
@@ -63,13 +64,11 @@ when:
- condition: impl(ghc >= 9.6.2)
dependencies:
- bytestring == 0.11.*
- process == 1.6.*
- template-haskell == 2.20.*
- text >= 2.0.1 && < 2.2
- condition: impl(ghc < 9.6.2)
dependencies:
- bytestring == 0.10.*
- process >= 1.6 && < 1.6.18
- template-haskell == 2.16.*
- text >= 1.2.3.0 && < 1.3
@@ -126,19 +125,13 @@ tests:
- apps/simplex-broadcast-bot/src
- apps/simplex-directory-service/src
main: Test.hs
when:
- condition: impl(ghc >= 9.6.2)
dependencies:
- hspec == 2.11.*
- condition: impl(ghc < 9.6.2)
dependencies:
- hspec == 2.7.*
dependencies:
- QuickCheck == 2.14.*
- simplex-chat
- async == 2.2.*
- deepseq == 1.4.*
- generic-random == 1.5.*
- hspec == 2.11.*
- network == 3.1.*
- silently == 1.2.*
- stm == 2.5.*

View File

@@ -2,7 +2,7 @@
set -e
trap "rm apps/multiplatform/local.properties || true; rm local.properties || true; rm /tmp/simplex.keychain || true" EXIT
trap "rm apps/multiplatform/local.properties 2> /dev/null || true; rm local.properties 2> /dev/null || true; rm /tmp/simplex.keychain" EXIT
echo "desktop.mac.signing.identity=Developer ID Application: SimpleX Chat Ltd (5NN7GUYB6T)" >> apps/multiplatform/local.properties
echo "desktop.mac.signing.keychain=/tmp/simplex.keychain" >> apps/multiplatform/local.properties
echo "desktop.mac.notarization.apple_id=$APPLE_SIMPLEX_NOTARIZATION_APPLE_ID" >> apps/multiplatform/local.properties
@@ -10,6 +10,10 @@ echo "desktop.mac.notarization.password=$APPLE_SIMPLEX_NOTARIZATION_PASSWORD" >>
echo "desktop.mac.notarization.team_id=5NN7GUYB6T" >> apps/multiplatform/local.properties
echo "$APPLE_SIMPLEX_SIGNING_KEYCHAIN" | base64 --decode - > /tmp/simplex.keychain
security unlock-keychain -p "" /tmp/simplex.keychain
# Adding keychain to the list of keychains.
# Otherwise, it can find cert but exits while signing with "error: The specified item could not be found in the keychain."
security list-keychains -s `security list-keychains | xargs` /tmp/simplex.keychain
scripts/desktop/build-lib-mac.sh
cd apps/multiplatform
./gradlew packageDmg

View File

@@ -0,0 +1,10 @@
#!/bin/bash
security create-keychain -p "" simplex.keychain
security set-keychain-settings -u simplex.keychain
security add-certificates -k simplex.keychain "Developer ID Application: SimpleX Chat Ltd (5NN7GUYB6T).cer"
security add-certificates -k simplex.keychain "Developer ID Certification Authority.cer"
# Private key with access from any app
security import "SimpleX Chat.p12" -P "" -k simplex.keychain -A
# Public key
security import "SimpleX Chat.pem" -k simplex.keychain

View File

@@ -8,7 +8,7 @@ function readlink() {
OS=linux
ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`}
GHC_VERSION=8.10.7
GHC_VERSION=9.6.3
if [ "$ARCH" == "aarch64" ]; then
COMPOSE_ARCH=arm64
@@ -21,7 +21,7 @@ cd $root_dir
BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-*
rm -rf $BUILD_DIR
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN' --ghc-options="-optl-L$(ghc --print-libdir)/rts -optl-Wl,--as-needed,-lHSrts_thr-ghc$GHC_VERSION"
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded'
cd $BUILD_DIR/build
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
#patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so

View File

@@ -5,13 +5,14 @@ set -e
OS=mac
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
COMPOSE_ARCH=$ARCH
GHC_VERSION=8.10.7
GHC_VERSION=9.6.3
if [ "$ARCH" == "arm64" ]; then
ARCH=aarch64
else
COMPOSE_ARCH=x64
fi
LIB_EXT=dylib
LIB=libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT
GHC_LIBS_DIR=$(ghc --print-libdir)
@@ -19,13 +20,26 @@ GHC_LIBS_DIR=$(ghc --print-libdir)
BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-*
rm -rf $BUILD_DIR
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/rts -optl-lHSrts_thr-ghc8.10.7 -optl-lffi"
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi"
cd $BUILD_DIR/build
mkdir deps 2> /dev/null || true
# It's not included by default for some reason. Compiled lib tries to find system one but it's not always available
cp $GHC_LIBS_DIR/rts/libffi.dylib ./deps
#cp $GHC_LIBS_DIR/libffi.dylib ./deps
(
BUILD=$PWD
cp /tmp/libffi-3.4.4/*-apple-darwin*/.libs/libffi.dylib $BUILD/deps || \
( \
cd /tmp && \
curl --tlsv1.2 "https://gitlab.haskell.org/ghc/libffi-tarballs/-/raw/libffi-3.4.4/libffi-3.4.4.tar.gz?inline=false" -o libffi.tar.gz && \
tar -xzvf libffi.tar.gz && \
cd "libffi-3.4.4" && \
./configure && \
make && \
cp *-apple-darwin*/.libs/libffi.dylib $BUILD/deps \
)
)
DYLIBS=`otool -L $LIB | grep @rpath | tail -n +2 | cut -d' ' -f 1 | cut -d'/' -f2`
RPATHS=`otool -l $LIB | grep "path "| cut -d' ' -f11`
@@ -66,6 +80,8 @@ function copy_deps() {
}
copy_deps $LIB
# Special case
cp $(ghc --print-libdir)/$ARCH-osx-ghc-$GHC_VERSION/libHSghc-boot-th-$GHC_VERSION-ghc$GHC_VERSION.dylib deps
rm deps/`basename $LIB`
cd -

View File

@@ -0,0 +1,242 @@
From 38be2c93acb6f459d24ed6c626981c35ccf44095 Mon Sep 17 00:00:00 2001
From: Sylvain Henry <sylvain@haskus.fr>
Date: Thu, 16 Feb 2023 15:40:45 +0100
Subject: [PATCH] Fix build on 32-bit architectures
---
Basement/Bits.hs | 4 ++++
Basement/From.hs | 24 -----------------------
Basement/Numerical/Additive.hs | 4 ++++
Basement/Numerical/Conversion.hs | 20 +++++++++++++++++++
Basement/PrimType.hs | 6 +++++-
Basement/Types/OffsetSize.hs | 22 +++++++++++++++++++--
6 files changed, 53 insertions(+), 27 deletions(-)
diff --git a/Basement/Bits.hs b/Basement/Bits.hs
index 7eeea0f5..24520ed7 100644
--- a/Basement/Bits.hs
+++ b/Basement/Bits.hs
@@ -54,8 +54,12 @@ import GHC.Int
import Basement.Compat.Primitive
#if WORD_SIZE_IN_BITS < 64
+#if __GLASGOW_HASKELL__ >= 904
+import GHC.Exts
+#else
import GHC.IntWord64
#endif
+#endif
-- | operation over finite bits
class FiniteBitsOps bits where
diff --git a/Basement/From.hs b/Basement/From.hs
index 7bbe141c..80014b3e 100644
--- a/Basement/From.hs
+++ b/Basement/From.hs
@@ -272,23 +272,11 @@ instance (NatWithinBound (CountOf ty) n, KnownNat n, PrimType ty)
tryFrom = BlockN.toBlockN . UArray.toBlock . BoxArray.mapToUnboxed id
instance (KnownNat n, NatWithinBound Word8 n) => From (Zn64 n) Word8 where
-#if __GLASGOW_HASKELL__ >= 904
- from = narrow . unZn64 where narrow (W64# w) = W8# (wordToWord8# (word64ToWord# (GHC.Prim.word64ToWord# w)))
-#else
from = narrow . unZn64 where narrow (W64# w) = W8# (wordToWord8# (word64ToWord# w))
-#endif
instance (KnownNat n, NatWithinBound Word16 n) => From (Zn64 n) Word16 where
-#if __GLASGOW_HASKELL__ >= 904
- from = narrow . unZn64 where narrow (W64# w) = W16# (wordToWord16# (word64ToWord# (GHC.Prim.word64ToWord# w)))
-#else
from = narrow . unZn64 where narrow (W64# w) = W16# (wordToWord16# (word64ToWord# w))
-#endif
instance (KnownNat n, NatWithinBound Word32 n) => From (Zn64 n) Word32 where
-#if __GLASGOW_HASKELL__ >= 904
- from = narrow . unZn64 where narrow (W64# w) = W32# (wordToWord32# (word64ToWord# (GHC.Prim.word64ToWord# w)))
-#else
from = narrow . unZn64 where narrow (W64# w) = W32# (wordToWord32# (word64ToWord# w))
-#endif
instance From (Zn64 n) Word64 where
from = unZn64
instance From (Zn64 n) Word128 where
@@ -297,23 +285,11 @@ instance From (Zn64 n) Word256 where
from = from . unZn64
instance (KnownNat n, NatWithinBound Word8 n) => From (Zn n) Word8 where
-#if __GLASGOW_HASKELL__ >= 904
- from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W8# (wordToWord8# (word64ToWord# (GHC.Prim.word64ToWord# w)))
-#else
from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W8# (wordToWord8# (word64ToWord# w))
-#endif
instance (KnownNat n, NatWithinBound Word16 n) => From (Zn n) Word16 where
-#if __GLASGOW_HASKELL__ >= 904
- from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W16# (wordToWord16# (word64ToWord# (GHC.Prim.word64ToWord# w)))
-#else
from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W16# (wordToWord16# (word64ToWord# w))
-#endif
instance (KnownNat n, NatWithinBound Word32 n) => From (Zn n) Word32 where
-#if __GLASGOW_HASKELL__ >= 904
- from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W32# (wordToWord32# (word64ToWord# (GHC.Prim.word64ToWord# w)))
-#else
from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W32# (wordToWord32# (word64ToWord# w))
-#endif
instance (KnownNat n, NatWithinBound Word64 n) => From (Zn n) Word64 where
from = naturalToWord64 . unZn
instance (KnownNat n, NatWithinBound Word128 n) => From (Zn n) Word128 where
diff --git a/Basement/Numerical/Additive.hs b/Basement/Numerical/Additive.hs
index d0dfb973..8ab65aa0 100644
--- a/Basement/Numerical/Additive.hs
+++ b/Basement/Numerical/Additive.hs
@@ -30,8 +30,12 @@ import qualified Basement.Types.Word128 as Word128
import qualified Basement.Types.Word256 as Word256
#if WORD_SIZE_IN_BITS < 64
+#if __GLASGOW_HASKELL__ >= 904
+import GHC.Exts
+#else
import GHC.IntWord64
#endif
+#endif
-- | Represent class of things that can be added together,
-- contains a neutral element and is commutative.
diff --git a/Basement/Numerical/Conversion.hs b/Basement/Numerical/Conversion.hs
index db502c07..fddc8232 100644
--- a/Basement/Numerical/Conversion.hs
+++ b/Basement/Numerical/Conversion.hs
@@ -26,8 +26,12 @@ import GHC.Word
import Basement.Compat.Primitive
#if WORD_SIZE_IN_BITS < 64
+#if __GLASGOW_HASKELL__ >= 904
+import GHC.Exts
+#else
import GHC.IntWord64
#endif
+#endif
intToInt64 :: Int -> Int64
#if WORD_SIZE_IN_BITS == 64
@@ -96,11 +100,22 @@ int64ToWord64 (I64# i) = W64# (int64ToWord64# i)
#endif
#if WORD_SIZE_IN_BITS == 64
+#if __GLASGOW_HASKELL__ >= 904
+word64ToWord# :: Word64# -> Word#
+word64ToWord# i = word64ToWord# i
+#else
word64ToWord# :: Word# -> Word#
word64ToWord# i = i
+#endif
{-# INLINE word64ToWord# #-}
#endif
+#if WORD_SIZE_IN_BITS < 64
+word64ToWord32# :: Word64# -> Word32#
+word64ToWord32# i = wordToWord32# (word64ToWord# i)
+{-# INLINE word64ToWord32# #-}
+#endif
+
-- | 2 Word32s
data Word32x2 = Word32x2 {-# UNPACK #-} !Word32
{-# UNPACK #-} !Word32
@@ -113,9 +128,14 @@ word64ToWord32s (W64# w64) = Word32x2 (W32# (wordToWord32# (uncheckedShiftRL# (G
word64ToWord32s (W64# w64) = Word32x2 (W32# (wordToWord32# (uncheckedShiftRL# w64 32#))) (W32# (wordToWord32# w64))
#endif
#else
+#if __GLASGOW_HASKELL__ >= 904
+word64ToWord32s :: Word64 -> Word32x2
+word64ToWord32s (W64# w64) = Word32x2 (W32# (word64ToWord32# (uncheckedShiftRL64# w64 32#))) (W32# (word64ToWord32# w64))
+#else
word64ToWord32s :: Word64 -> Word32x2
word64ToWord32s (W64# w64) = Word32x2 (W32# (word64ToWord# (uncheckedShiftRL64# w64 32#))) (W32# (word64ToWord# w64))
#endif
+#endif
wordToChar :: Word -> Char
wordToChar (W# word) = C# (chr# (word2Int# word))
diff --git a/Basement/PrimType.hs b/Basement/PrimType.hs
index f8ca2926..a888ec91 100644
--- a/Basement/PrimType.hs
+++ b/Basement/PrimType.hs
@@ -54,7 +54,11 @@ import Basement.Nat
import qualified Prelude (quot)
#if WORD_SIZE_IN_BITS < 64
-import GHC.IntWord64
+#if __GLASGOW_HASKELL__ >= 904
+import GHC.Exts
+#else
+import GHC.IntWord64
+#endif
#endif
#ifdef FOUNDATION_BOUNDS_CHECK
diff --git a/Basement/Types/OffsetSize.hs b/Basement/Types/OffsetSize.hs
index cd944927..1ea80dad 100644
--- a/Basement/Types/OffsetSize.hs
+++ b/Basement/Types/OffsetSize.hs
@@ -70,8 +70,12 @@ import Data.List (foldl')
import qualified Prelude
#if WORD_SIZE_IN_BITS < 64
+#if __GLASGOW_HASKELL__ >= 904
+import GHC.Exts
+#else
import GHC.IntWord64
#endif
+#endif
-- | File size in bytes
newtype FileSize = FileSize Word64
@@ -225,20 +229,26 @@ countOfRoundUp alignment (CountOf n) = CountOf ((n + (alignment-1)) .&. compleme
csizeOfSize :: CountOf Word8 -> CSize
#if WORD_SIZE_IN_BITS < 64
+#if __GLASGOW_HASKELL__ >= 904
+csizeOfSize (CountOf (I# sz)) = CSize (W32# (wordToWord32# (int2Word# sz)))
+#else
csizeOfSize (CountOf (I# sz)) = CSize (W32# (int2Word# sz))
+#endif
#else
#if __GLASGOW_HASKELL__ >= 904
csizeOfSize (CountOf (I# sz)) = CSize (W64# (wordToWord64# (int2Word# sz)))
-
#else
csizeOfSize (CountOf (I# sz)) = CSize (W64# (int2Word# sz))
-
#endif
#endif
csizeOfOffset :: Offset8 -> CSize
#if WORD_SIZE_IN_BITS < 64
+#if __GLASGOW_HASKELL__ >= 904
+csizeOfOffset (Offset (I# sz)) = CSize (W32# (wordToWord32# (int2Word# sz)))
+#else
csizeOfOffset (Offset (I# sz)) = CSize (W32# (int2Word# sz))
+#endif
#else
#if __GLASGOW_HASKELL__ >= 904
csizeOfOffset (Offset (I# sz)) = CSize (W64# (wordToWord64# (int2Word# sz)))
@@ -250,7 +260,11 @@ csizeOfOffset (Offset (I# sz)) = CSize (W64# (int2Word# sz))
sizeOfCSSize :: CSsize -> CountOf Word8
sizeOfCSSize (CSsize (-1)) = error "invalid size: CSSize is -1"
#if WORD_SIZE_IN_BITS < 64
+#if __GLASGOW_HASKELL__ >= 904
+sizeOfCSSize (CSsize (I32# sz)) = CountOf (I# (int32ToInt# sz))
+#else
sizeOfCSSize (CSsize (I32# sz)) = CountOf (I# sz)
+#endif
#else
#if __GLASGOW_HASKELL__ >= 904
sizeOfCSSize (CSsize (I64# sz)) = CountOf (I# (int64ToInt# sz))
@@ -261,7 +275,11 @@ sizeOfCSSize (CSsize (I64# sz)) = CountOf (I# sz)
sizeOfCSize :: CSize -> CountOf Word8
#if WORD_SIZE_IN_BITS < 64
+#if __GLASGOW_HASKELL__ >= 904
+sizeOfCSize (CSize (W32# sz)) = CountOf (I# (word2Int# (word32ToWord# sz)))
+#else
sizeOfCSize (CSize (W32# sz)) = CountOf (I# (word2Int# sz))
+#endif
#else
#if __GLASGOW_HASKELL__ >= 904
sizeOfCSize (CSize (W64# sz)) = CountOf (I# (word2Int# (word64ToWord# sz)))

View File

@@ -0,0 +1,12 @@
diff --git a/direct-sqlcipher.cabal b/direct-sqlcipher.cabal
index 728ba3e..c63745e 100644
--- a/direct-sqlcipher.cabal
+++ b/direct-sqlcipher.cabal
@@ -84,6 +84,8 @@ library
cc-options: -DSQLITE_TEMP_STORE=2
-DSQLITE_HAS_CODEC
+ extra-libraries: ws2_32
+
if !os(windows) && !os(android)
extra-libraries: pthread

View File

@@ -0,0 +1,36 @@
From 2738929ce15b4c8704bbbac24a08539b5d4bf30e Mon Sep 17 00:00:00 2001
From: sternenseemann <sternenseemann@systemli.org>
Date: Mon, 14 Aug 2023 10:51:30 +0200
Subject: [PATCH] Data.Memory.Internal.CompatPrim64: fix 32 bit with GHC >= 9.4
Since 9.4, GHC.Prim exports Word64# operations like timesWord64# even on
i686 whereas GHC.IntWord64 no longer exists. Therefore, we can just use
the ready made solution.
Closes #98, as it should be the better solution.
---
Data/Memory/Internal/CompatPrim64.hs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Data/Memory/Internal/CompatPrim64.hs b/Data/Memory/Internal/CompatPrim64.hs
index b9eef8a..a134c88 100644
--- a/Data/Memory/Internal/CompatPrim64.hs
+++ b/Data/Memory/Internal/CompatPrim64.hs
@@ -150,6 +150,7 @@ w64# :: Word# -> Word# -> Word# -> Word64#
w64# w _ _ = w
#elif WORD_SIZE_IN_BITS == 32
+#if __GLASGOW_HASKELL__ < 904
import GHC.IntWord64
import GHC.Prim (Word#)
@@ -158,6 +159,9 @@ timesWord64# a b =
let !ai = word64ToInt64# a
!bi = word64ToInt64# b
in int64ToWord64# (timesInt64# ai bi)
+#else
+import GHC.Prim
+#endif
w64# :: Word# -> Word# -> Word# -> Word64#
w64# _ hw lw =

View File

@@ -197,6 +197,7 @@ library
, network >=3.1.2.7 && <3.2
, network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, record-hasfield ==1.0.*
, simple-logger ==0.1.*
@@ -216,13 +217,11 @@ library
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
@@ -257,6 +256,7 @@ executable simplex-bot
, network >=3.1.2.7 && <3.2
, network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, record-hasfield ==1.0.*
, simple-logger ==0.1.*
@@ -277,13 +277,11 @@ executable simplex-bot
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
@@ -318,6 +316,7 @@ executable simplex-bot-advanced
, network >=3.1.2.7 && <3.2
, network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, record-hasfield ==1.0.*
, simple-logger ==0.1.*
@@ -338,13 +337,11 @@ executable simplex-bot-advanced
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
@@ -381,6 +378,7 @@ executable simplex-broadcast-bot
, network >=3.1.2.7 && <3.2
, network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, record-hasfield ==1.0.*
, simple-logger ==0.1.*
@@ -401,13 +399,11 @@ executable simplex-broadcast-bot
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
@@ -443,6 +439,7 @@ executable simplex-chat
, network ==3.1.*
, network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, record-hasfield ==1.0.*
, simple-logger ==0.1.*
@@ -464,13 +461,11 @@ executable simplex-chat
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
@@ -510,6 +505,7 @@ executable simplex-directory-service
, network >=3.1.2.7 && <3.2
, network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, record-hasfield ==1.0.*
, simple-logger ==0.1.*
@@ -530,13 +526,11 @@ executable simplex-directory-service
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
@@ -598,6 +592,7 @@ test-suite simplex-chat-test
, exceptions ==0.10.*
, filepath ==1.4.*
, generic-random ==1.5.*
, hspec ==2.11.*
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
, memory ==0.18.*
@@ -605,6 +600,7 @@ test-suite simplex-chat-test
, network ==3.1.*
, network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, record-hasfield ==1.0.*
, silently ==1.2.*
@@ -626,18 +622,10 @@ test-suite simplex-chat-test
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
if impl(ghc >= 9.6.2)
build-depends:
hspec ==2.11.*
if impl(ghc < 9.6.2)
build-depends:
hspec ==2.7.*

View File

@@ -1035,6 +1035,7 @@ processChatCommand' vr = \case
withStore' $ \db -> deleteGroup db user gInfo
let contactIds = mapMaybe memberContactId members
deleteAgentConnectionsAsync user . concat =<< mapM deleteUnusedContact contactIds
updateCIGroupInvitationStatus user gInfo CIGISRejected `catchChatError` \_ -> pure ()
pure $ CRGroupDeletedUser user gInfo
where
deleteUnusedContact :: ContactId -> m [ConnId]
@@ -1686,17 +1687,9 @@ processChatCommand' vr = \case
createMemberConnection db userId fromMember agentConnId (fromJVersionRange peerChatVRange) subMode
updateGroupMemberStatus db userId fromMember GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
updateCIGroupInvitationStatus user
updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` \_ -> pure ()
pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing
Nothing -> throwChatError $ CEContactNotActive ct
where
updateCIGroupInvitationStatus user = do
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db vr user groupId
case (cInfo, content) of
(DirectChat ct, CIRcvGroupInvitation ciGroupInv memRole) -> do
let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = CIGISAccepted} memRole
updateDirectChatItemView user ct itemId aciContent False Nothing
_ -> pure () -- prohibited
APIMemberRole groupId memberId memRole -> withUser $ \user -> do
Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId
if memberId == groupMemberId' membership
@@ -2512,6 +2505,14 @@ processChatCommand' vr = \case
cReqHashes :: (ConnReqUriHash, ConnReqUriHash)
cReqHashes = bimap hash hash cReqSchemas
hash = ConnReqUriHash . C.sha256Hash . strEncode
updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db vr user groupId
case (cInfo, content) of
(DirectChat ct, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole)
| status == CIGISPending -> do
let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = newStatus} memRole
updateDirectChatItemView user ct itemId aciContent False Nothing
_ -> pure () -- prohibited
toggleNtf :: ChatMonad m => User -> GroupMember -> Bool -> m ()
toggleNtf user m ntfOn =

View File

@@ -20,6 +20,7 @@ import qualified Data.ByteArray as BA
import qualified Data.ByteString.Base64.URL as U
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Functor (($>))
import Data.List (find)
import qualified Data.List.NonEmpty as L
@@ -94,6 +95,8 @@ foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CStrin
foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString
foreign export ccall "chat_json_length" cChatJsonLength :: CString -> IO CInt
foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString
foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
@@ -176,6 +179,10 @@ cChatPasswordHash cPwd cSalt = do
cChatValidName :: CString -> IO CString
cChatValidName cName = newCString . mkValidName =<< peekCString cName
-- | returns length of JSON encoded string
cChatJsonLength :: CString -> IO CInt
cChatJsonLength s = fromIntegral . subtract 2 . LB.length . J.encode . safeDecodeUtf8 <$> B.packCString s
mobileChatOpts :: String -> ChatOpts
mobileChatOpts dbFilePrefix =
ChatOpts

View File

@@ -68,6 +68,8 @@ mobileTests = do
it "no exception on missing file" testMissingFileEncryptionCApi
describe "validate name" $ do
it "should convert invalid name to a valid name" testValidNameCApi
describe "JSON length" $ do
it "should compute length of JSON encoded string" testChatJsonLengthCApi
noActiveUser :: LB.ByteString
noActiveUser =
@@ -356,6 +358,13 @@ testValidNameCApi _ = do
cName2 <- cChatValidName =<< newCString " @'Джон' Доу 👍 "
peekCString cName2 `shouldReturn` goodName
testChatJsonLengthCApi :: FilePath -> IO ()
testChatJsonLengthCApi _ = do
cInt1 <- cChatJsonLength =<< newCString "Hello!"
cInt1 `shouldBe` 6
cInt2 <- cChatJsonLength =<< newCString "こんにちは!"
cInt2 `shouldBe` 18
jDecode :: FromJSON a => String -> IO (Maybe a)
jDecode = pure . J.decode . LB.pack