on master help sign in
Refactor conversations UI with ListBox layout and add small button components
5aa2735iko
client/App.fs+97−45
@@ -17,6 +17,8 @@ module App =
17 open ApiClient
17 open ApiClient
18 open Buttons
18 open Buttons
19 open TextFields
19 open TextFields
20+ open Styles
21+ open Avalonia.Layout
20
22
21 // ── Domain Types ──
23 // ── Domain Types ──
22
24
@@ -88,6 +90,7 @@ module App =
88 | DoSaveProfile
90 | DoSaveProfile
89 | StartFlush
91 | StartFlush
90 | FlushResult of sent: bool
92 | FlushResult of sent: bool
93+ | Search of string
91
94
92 // ── CmdMsg ──
95 // ── CmdMsg ──
93
96
@@ -160,13 +163,13 @@ module App =
160 Nickname = nickname
163 Nickname = nickname
161 DisplayName = ""
164 DisplayName = ""
162 Bio = ""
165 Bio = ""
163− Photo = [||] }
166+ Photo = None }
164
167
165 let private withProfile (p: Profile) (c: Contact) =
168 let private withProfile (p: Profile) (c: Contact) =
166 { c with
169 { c with
167 DisplayName = p.DisplayName
170 DisplayName = p.DisplayName
168 Bio = p.Bio
171 Bio = p.Bio
169− Photo = p.Photo |> Option.defaultValue [||] }
172+ Photo = p.Photo }
170
173
171 let private applyEvent (m: Model) (evt: IncomingEvent) =
174 let private applyEvent (m: Model) (evt: IncomingEvent) =
172 match evt.Envelope with
175 match evt.Envelope with
@@ -836,48 +839,95 @@ module App =
836 )
839 )
837
840
838 let private viewConversations model =
841 let private viewConversations model =
839− ScrollViewer(
842+ let contacts = model.Contacts.Values |> Seq.toList
840− (VStack(8.) {
841− HStack(8.) {
842− TextBlock("Conversations").fontSize (24.)
843−
844− Button("+", SetPage(AddContact("", "")))
845− Button("Settings", SetPage Settings)
846− }
847−
848− let pubPrefix =
849− match model.Auth with
850− | Identified(id, _) -> id.PubKeyHex.[..7]
851− | _ -> "?"
852−
853− let pollInfo =
854− if model.PollStatus <> "" then
855− $" | {model.PollStatus}"
856− else
857− ""
858−
859− TextBlock($"{connStatusLabel model.Auth} [{pubPrefix}]{pollInfo}")
860− .fontSize(10.)
861− .foreground (SolidColorBrush(Colors.DimGray))
862
843
863− viewErrorBanner model.Error
844+ (Grid([], [ Dimension.Star; Dimension.Auto ]) {
864
845
865− if model.Contacts.IsEmpty then
846+ if contacts.IsEmpty then
866− TextBlock("No contacts yet. Tap + to add one.")
847+ TextBlock("No contacts yet. Tap + to add one.")
867− .centerText()
848+ .centerText()
868− .foreground (SolidColorBrush(Colors.Gray))
849+ .foreground(SolidColorBrush(Colors.Gray))
850+ .gridRow (1)
851+
852+ if not contacts.IsEmpty then
853+ ListBox(
854+ contacts,
855+ fun c ->
856+ let name = contactName model.Contacts c.Pubkey
857+
858+ let preview =
859+ messagesFor c.Pubkey model.Messages
860+ |> List.tryLast
861+ |> Option.map (fun m -> if m.Body.Length > 40 then m.Body.[..39] + "..." else m.Body)
862+ |> Option.defaultValue ""
863+
864+ Border(
865+ (HStack(8.) {
866+
867+ Grid() {
868+ (match c.Photo with
869+ | None -> Image("avares://Skrepka/Assets/Images/user.png", Stretch.Uniform)
870+ | Some i ->
871+ Image(
872+ new Avalonia.Media.Imaging.Bitmap(new System.IO.MemoryStream(i)),
873+ Stretch.UniformToFill
874+ ))
875+ .width(16.)
876+ .height(16.)
877+ .margin(8.)
878+ .clipToBounds (true)
879+
880+ Rectangle().stroke(Colors.Black).strokeThickness (4.)
881+ }
882+
883+ VStack(2.) {
884+ TextBlock(name)
885+ .fontFamily(Constants.fontFamily)
886+ .fontWeight(FontWeight.Bold)
887+ .fontSize (16.)
888+
889+ if preview <> "" then
890+ TextBlock(preview).fontSize(12.).fontWeight (FontWeight.Bold)
891+ }
892+
893+ })
894+ .margin (8.)
895+ )
896+ .borderThickness(Avalonia.Thickness(0., 0., 0., 4.))
897+ .borderBrush(SolidColorBrush(Colors.Black))
898+ .background(Colors.Transparent)
899+ .onTapped (fun _ -> SetPage(Chat(c.Pubkey, "")))
900+ )
901+ .styles(noListBoxPadding ())
902+ .background(Colors.White)
903+ .gridRow (0)
904+
905+ Border(
906+
907+ (Grid([ Dimension.Auto; Dimension.Star; Dimension.Auto ], []) {
908+ // HStack(8.) {
909+ // TextBlock("Conversations").fontSize (24.)
910+
911+ // Button("+", SetPage(AddContact("", "")))
912+ // Button("Settings", SetPage Settings)
913+ // }
914+
915+ (smallImageButton None (SetPage Settings)).gridColumn (0)
916+
917+ (textField "" Search).margin(0.).margin(8.).gridColumn (1)
918+ (smallTextButton "+" (SetPage(AddContact("", "")))).gridColumn (2)
869
919
870− for c in model.Contacts.Values do
920+ })
871− let preview =
921+ .margin(20.)
872− messagesFor c.Pubkey model.Messages
922+ .horizontalAlignment (HorizontalAlignment.Stretch)
873− |> List.tryLast
923+ )
874− |> Option.map (fun m -> if m.Body.Length > 40 then m.Body.[..39] + "..." else m.Body)
924+ .borderThickness(Avalonia.Thickness(0., 4., 0., 0.))
875− |> Option.defaultValue ""
925+ .borderBrush(SolidColorBrush(Colors.Black))
926+ .background(Constants.accentColor)
927+ .gridRow (1)
876
928
877− Button($"{contactName model.Contacts c.Pubkey}\n{preview}", SetPage(Chat(c.Pubkey, "")))
929+ })
878− })
879− .margin (20.)
930+ .margin (0.)
880− )
881
931
882 let private viewChat model pk compose =
932 let private viewChat model pk compose =
883 let name = contactName model.Contacts pk
933 let name = contactName model.Contacts pk
@@ -959,15 +1009,17 @@ module App =
959 )
1009 )
960 .gridRow (0)
1010 .gridRow (0)
961
1011
962− (Grid() {
1012+ Border(
963− Rectangle().stroke(SolidColorBrush(Colors.Black)).strokeThickness(4.).fill (Constants.accentColor)
964
1013
965− HStack(8.) {
1014+ (HStack(8.) {
966 (button Secondary "back" (SetPage Settings)).margin (8.)
1015 (button Secondary "back" (SetPage Settings)).margin (8.)
967
1016
968 (button Secondary "save" DoSaveProfile).margin (8.)
1017 (button Secondary "save" DoSaveProfile).margin (8.)
969− }
1018+ })
1019+ .background (Constants.accentColor)
970− })
1020+ )
1021+ .borderThickness(Avalonia.Thickness(0., 4., 0., 0.))
1022+ .borderBrush(SolidColorBrush(Colors.Black))
971 .gridRow (1)
1023 .gridRow (1)
972 })
1024 })
973
1025
client/Buttons.fs+87
@@ -122,3 +122,90 @@ module Buttons =
122 .margin(16.)
122 .margin(16.)
123 .onTapped (fun _ -> msg)
123 .onTapped (fun _ -> msg)
124 }
124 }
125+
126+ let smallImageButton (img: byte[] option) (msg: 'msg) =
127+ let key =
128+ match img with
129+ | None -> "img-button"
130+ | Some i -> $"img-button-{hash i}"
131+
132+ Component(key) {
133+ let! pressed = Context.State(false)
134+ let offset = if pressed.Current then 2. else 4.
135+
136+ (Grid() {
137+ Rectangle()
138+ .fill(SolidColorBrush(Colors.Black))
139+ .renderTransform(translate offset offset)
140+ .transition (
141+ TransformOperationsTransition(
142+ Avalonia.Visual.RenderTransformProperty,
143+ TimeSpan.FromMilliseconds Constants.animationDuration
144+ )
145+ .easing (Constants.easing)
146+ )
147+
148+ Rectangle().fill (SolidColorBrush(Colors.White))
149+
150+ match img with
151+ | None ->
152+ ViewBox(
153+ Image("avares://Skrepka/Assets/Images/user.png", Stretch.Uniform)
154+ .width(32.)
155+ .height(32.)
156+ .margin (8.)
157+ )
158+
159+ | Some i ->
160+ Image(new Avalonia.Media.Imaging.Bitmap(new System.IO.MemoryStream(i)), Stretch.UniformToFill)
161+ .width(32.)
162+ .height(32.)
163+ .clipToBounds (true)
164+
165+ Rectangle().stroke(Colors.Black).strokeThickness (4.)
166+ })
167+ .background(Brushes.Transparent)
168+ .onPointerPressed(fun _ -> pressed.Set true)
169+ .onPointerReleased(fun _ -> pressed.Set false)
170+ .onPointerExited(fun _ -> pressed.Set false)
171+ .margin(8.)
172+ .onTapped (fun _ -> msg)
173+ }
174+
175+ let smallTextButton (text: string) (msg: 'msg) =
176+ Component($"small-button-{text}") {
177+ let! pressed = Context.State(false)
178+ let offset = if pressed.Current then 2. else 4.
179+
180+ (Grid() {
181+ Rectangle()
182+ .fill(SolidColorBrush(Colors.Black))
183+ .renderTransform(translate offset offset)
184+ .transition (
185+ TransformOperationsTransition(
186+ Avalonia.Visual.RenderTransformProperty,
187+ TimeSpan.FromMilliseconds Constants.animationDuration
188+ )
189+ .easing (Constants.easing)
190+ )
191+
192+ Rectangle().fill (SolidColorBrush(Colors.White))
193+
194+ TextBlock(text)
195+ .fontFamily(Constants.fontFamily)
196+ .fontWeight(FontWeight.Bold)
197+ .fontSize(28.)
198+ .textAlignment(TextAlignment.Center)
199+ .center ()
200+
201+ Rectangle().stroke(Colors.Black).strokeThickness (4.)
202+ })
203+ .width(48.)
204+ .height(48.)
205+ .background(Brushes.Transparent)
206+ .onPointerPressed(fun _ -> pressed.Set true)
207+ .onPointerReleased(fun _ -> pressed.Set false)
208+ .onPointerExited(fun _ -> pressed.Set false)
209+ .margin(8.)
210+ .onTapped (fun _ -> msg)
211+ }
client/Skrepka.fsproj+1
@@ -29,6 +29,7 @@
29
29
30 <ItemGroup>
30 <ItemGroup>
31 <Compile Include="Constants.fs" />
31 <Compile Include="Constants.fs" />
32+ <Compile Include="Styles.fs" />
32 <Compile Include="Crypto.fs" />
33 <Compile Include="Crypto.fs" />
33 <Compile Include="ApiClient.fs" />
34 <Compile Include="ApiClient.fs" />
34 <Compile Include="Phonemic.fs" />
35 <Compile Include="Phonemic.fs" />
client/Store.fs+99−31
@@ -13,7 +13,7 @@ module Store =
13 Nickname: string
13 Nickname: string
14 DisplayName: string
14 DisplayName: string
15 Bio: string
15 Bio: string
16− Photo: byte[] }
16+ Photo: byte[] option }
17
17
18 type Profile =
18 type Profile =
19 { DisplayName: string
19 { DisplayName: string
@@ -111,20 +111,48 @@ module Store =
111
111
112 type private ByteArrayOptionHandler() =
112 type private ByteArrayOptionHandler() =
113 inherit SqlMapper.TypeHandler<byte[] option>()
113 inherit SqlMapper.TypeHandler<byte[] option>()
114+
114 override _.SetValue(p, value) =
115 override _.SetValue(p, value) =
116+ p.Value <-
117+ match value with
118+ | Some v -> box v
115− p.Value <- match value with Some v -> box v | None -> box DBNull.Value
119+ | None -> box DBNull.Value
120+
116 override _.Parse(value) =
121 override _.Parse(value) =
117− if value = null || value :? DBNull then None else Some(value :?> byte[])
122+ if value = null || value :? DBNull then
123+ None
124+ else
125+ Some(value :?> byte[])
118
126
119 do
127 do
120 OptionTypes.register ()
128 OptionTypes.register ()
121 SqlMapper.AddTypeHandler(ByteArrayOptionHandler())
129 SqlMapper.AddTypeHandler(ByteArrayOptionHandler())
122 use conn = openConn ()
130 use conn = openConn ()
131+
132+ conn.Execute(
123− conn.Execute("CREATE TABLE IF NOT EXISTS profile (Id TEXT PRIMARY KEY, DisplayName TEXT NOT NULL DEFAULT '', Bio TEXT NOT NULL DEFAULT '', Photo BLOB)") |> ignore
133+ "CREATE TABLE IF NOT EXISTS profile (Id TEXT PRIMARY KEY, DisplayName TEXT NOT NULL DEFAULT '', Bio TEXT NOT NULL DEFAULT '', Photo BLOB)"
134+ )
135+ |> ignore
136+
137+ conn.Execute(
124− conn.Execute("CREATE TABLE IF NOT EXISTS contacts (Pubkey TEXT PRIMARY KEY, Nickname TEXT NOT NULL DEFAULT '', DisplayName TEXT NOT NULL DEFAULT '', Bio TEXT NOT NULL DEFAULT '', Photo BLOB)") |> ignore
138+ "CREATE TABLE IF NOT EXISTS contacts (Pubkey TEXT PRIMARY KEY, Nickname TEXT NOT NULL DEFAULT '', DisplayName TEXT NOT NULL DEFAULT '', Bio TEXT NOT NULL DEFAULT '', Photo BLOB)"
139+ )
140+ |> ignore
141+
142+ conn.Execute(
125− conn.Execute("CREATE TABLE IF NOT EXISTS messages (Id TEXT PRIMARY KEY, ConversationId TEXT NOT NULL, Body TEXT NOT NULL DEFAULT '', TimestampUnix INTEGER NOT NULL, IsOutgoing INTEGER NOT NULL DEFAULT 0, Status INTEGER NOT NULL DEFAULT 0)") |> ignore
143+ "CREATE TABLE IF NOT EXISTS messages (Id TEXT PRIMARY KEY, ConversationId TEXT NOT NULL, Body TEXT NOT NULL DEFAULT '', TimestampUnix INTEGER NOT NULL, IsOutgoing INTEGER NOT NULL DEFAULT 0, Status INTEGER NOT NULL DEFAULT 0)"
144+ )
145+ |> ignore
146+
147+ conn.Execute(
126− conn.Execute("CREATE TABLE IF NOT EXISTS settings (Id TEXT PRIMARY KEY, ServerUrl TEXT NOT NULL DEFAULT '', PollCursor INTEGER NOT NULL DEFAULT 0)") |> ignore
148+ "CREATE TABLE IF NOT EXISTS settings (Id TEXT PRIMARY KEY, ServerUrl TEXT NOT NULL DEFAULT '', PollCursor INTEGER NOT NULL DEFAULT 0)"
149+ )
150+ |> ignore
151+
152+ conn.Execute(
127− conn.Execute("CREATE TABLE IF NOT EXISTS outbox (Id INTEGER PRIMARY KEY AUTOINCREMENT, RecipientHex TEXT NOT NULL, EnvelopeJson TEXT NOT NULL, CreatedAt INTEGER NOT NULL)") |> ignore
153+ "CREATE TABLE IF NOT EXISTS outbox (Id INTEGER PRIMARY KEY AUTOINCREMENT, RecipientHex TEXT NOT NULL, EnvelopeJson TEXT NOT NULL, CreatedAt INTEGER NOT NULL)"
154+ )
155+ |> ignore
128
156
129 let private identityPath = Path.Combine(appDataDir, "identity.key")
157 let private identityPath = Path.Combine(appDataDir, "identity.key")
130
158
@@ -153,12 +181,18 @@ module Store =
153 try
181 try
154 use conn = openConn ()
182 use conn = openConn ()
155
183
184+ select {
156− select { for p in profileTable do where (p.Id = "me") }
185+ for p in profileTable do
186+ where (p.Id = "me")
187+ }
157 |> conn.SelectAsync<ProfileRow>
188 |> conn.SelectAsync<ProfileRow>
158 |> Async.AwaitTask
189 |> Async.AwaitTask
159 |> Async.RunSynchronously
190 |> Async.RunSynchronously
160 |> Seq.tryHead
191 |> Seq.tryHead
192+ |> Option.map (fun p ->
161− |> Option.map (fun p -> { DisplayName = p.DisplayName; Bio = p.Bio; Photo = p.Photo })
193+ { DisplayName = p.DisplayName
194+ Bio = p.Bio
195+ Photo = p.Photo })
162 with _ ->
196 with _ ->
163 None
197 None
164
198
@@ -168,8 +202,11 @@ module Store =
168
202
169 conn.Execute(
203 conn.Execute(
170 "INSERT OR REPLACE INTO profile (Id, DisplayName, Bio, Photo) VALUES (@Id, @DisplayName, @Bio, @Photo)",
204 "INSERT OR REPLACE INTO profile (Id, DisplayName, Bio, Photo) VALUES (@Id, @DisplayName, @Bio, @Photo)",
205+ {| Id = "me"
171− {| Id = "me"; DisplayName = profile.DisplayName; Bio = profile.Bio
206+ DisplayName = profile.DisplayName
207+ Bio = profile.Bio
172− Photo = profile.Photo |> Option.defaultValue null |})
208+ Photo = profile.Photo |> Option.defaultValue null |}
209+ )
173 |> ignore
210 |> ignore
174 with _ ->
211 with _ ->
175 ()
212 ()
@@ -179,7 +216,10 @@ module Store =
179 use conn = openConn ()
216 use conn = openConn ()
180
217
181 let settings =
218 let settings =
219+ select {
182− select { for s in settingsTable do where (s.Id = "settings") }
220+ for s in settingsTable do
221+ where (s.Id = "settings")
222+ }
183 |> conn.SelectAsync<SettingsRow>
223 |> conn.SelectAsync<SettingsRow>
184 |> Async.AwaitTask
224 |> Async.AwaitTask
185 |> Async.RunSynchronously
225 |> Async.RunSynchronously
@@ -188,7 +228,10 @@ module Store =
188 settings
228 settings
189 |> Option.map (fun s ->
229 |> Option.map (fun s ->
190 let contacts =
230 let contacts =
231+ select {
191− select { for c in contactTable do selectAll }
232+ for c in contactTable do
233+ selectAll
234+ }
192 |> conn.SelectAsync<ContactRow>
235 |> conn.SelectAsync<ContactRow>
193 |> Async.AwaitTask
236 |> Async.AwaitTask
194 |> Async.RunSynchronously
237 |> Async.RunSynchronously
@@ -198,13 +241,16 @@ module Store =
198 Nickname = c.Nickname
241 Nickname = c.Nickname
199 DisplayName = c.DisplayName
242 DisplayName = c.DisplayName
200 Bio = c.Bio
243 Bio = c.Bio
201− Photo = c.Photo |> Option.defaultValue [||] }
244+ Photo = c.Photo }
202
245
203 c.Pubkey, contact)
246 c.Pubkey, contact)
204 |> Map.ofSeq
247 |> Map.ofSeq
205
248
206 let messages =
249 let messages =
250+ select {
207− select { for m in messageTable do selectAll }
251+ for m in messageTable do
252+ selectAll
253+ }
208 |> conn.SelectAsync<MessageRow>
254 |> conn.SelectAsync<MessageRow>
209 |> Async.AwaitTask
255 |> Async.AwaitTask
210 |> Async.RunSynchronously
256 |> Async.RunSynchronously
@@ -217,8 +263,15 @@ module Store =
217 Body = m.Body
263 Body = m.Body
218 Timestamp = DateTimeOffset.FromUnixTimeMilliseconds m.TimestampUnix
264 Timestamp = DateTimeOffset.FromUnixTimeMilliseconds m.TimestampUnix
219 Direction =
265 Direction =
220− if not m.IsOutgoing then Incoming
266+ if not m.IsOutgoing then
267+ Incoming
268+ else
269+ Outgoing(
270+ if m.Status = 1 then
221− else Outgoing(if m.Status = 1 then DeliveryStatus.Delivered else DeliveryStatus.Sent) })
271+ DeliveryStatus.Delivered
272+ else
273+ DeliveryStatus.Sent
274+ ) })
222 |> Seq.sortBy _.Timestamp
275 |> Seq.sortBy _.Timestamp
223 |> Seq.toList)
276 |> Seq.toList)
224 |> Map.ofSeq
277 |> Map.ofSeq
@@ -237,8 +290,12 @@ module Store =
237 for c in data.Contacts.Values do
290 for c in data.Contacts.Values do
238 conn.Execute(
291 conn.Execute(
239 "INSERT OR REPLACE INTO contacts (Pubkey, Nickname, DisplayName, Bio, Photo) VALUES (@Pubkey, @Nickname, @DisplayName, @Bio, @Photo)",
292 "INSERT OR REPLACE INTO contacts (Pubkey, Nickname, DisplayName, Bio, Photo) VALUES (@Pubkey, @Nickname, @DisplayName, @Bio, @Photo)",
293+ {| Pubkey = c.Pubkey
294+ Nickname = c.Nickname
240− {| Pubkey = c.Pubkey; Nickname = c.Nickname; DisplayName = c.DisplayName; Bio = c.Bio
295+ DisplayName = c.DisplayName
241− Photo = if c.Photo = null || c.Photo.Length = 0 then null else c.Photo |})
296+ Bio = c.Bio
297+ Photo = c.Photo |}
298+ )
242 |> ignore
299 |> ignore
243
300
244 data.Messages
301 data.Messages
@@ -252,28 +309,37 @@ module Store =
252
309
253 conn.Execute(
310 conn.Execute(
254 "INSERT OR REPLACE INTO messages (Id, ConversationId, Body, TimestampUnix, IsOutgoing, Status) VALUES (@Id, @ConversationId, @Body, @TimestampUnix, @IsOutgoing, @Status)",
311 "INSERT OR REPLACE INTO messages (Id, ConversationId, Body, TimestampUnix, IsOutgoing, Status) VALUES (@Id, @ConversationId, @Body, @TimestampUnix, @IsOutgoing, @Status)",
312+ {| Id = m.Id
255− {| Id = m.Id; ConversationId = convId; Body = m.Body
313+ ConversationId = convId
314+ Body = m.Body
256 TimestampUnix = m.Timestamp.ToUnixTimeMilliseconds()
315 TimestampUnix = m.Timestamp.ToUnixTimeMilliseconds()
257− IsOutgoing = isOutgoing; Status = status |})
316+ IsOutgoing = isOutgoing
317+ Status = status |}
318+ )
258 |> ignore)
319 |> ignore)
259
320
260 conn.Execute(
321 conn.Execute(
261 "INSERT OR REPLACE INTO settings (Id, ServerUrl, PollCursor) VALUES (@Id, @ServerUrl, @PollCursor)",
322 "INSERT OR REPLACE INTO settings (Id, ServerUrl, PollCursor) VALUES (@Id, @ServerUrl, @PollCursor)",
323+ {| Id = "settings"
324+ ServerUrl = data.ServerUrl
262− {| Id = "settings"; ServerUrl = data.ServerUrl; PollCursor = data.PollCursor |})
325+ PollCursor = data.PollCursor |}
326+ )
263 |> ignore
327 |> ignore
264 with _ ->
328 with _ ->
265 ()
329 ()
266
330
267 let enqueueOutbox (recipientHex: string) (envelopeJson: string) =
331 let enqueueOutbox (recipientHex: string) (envelopeJson: string) =
268 use conn = openConn ()
332 use conn = openConn ()
333+
269 insert {
334 insert {
270 for o in outboxTable do
335 for o in outboxTable do
271− value
336+ value
272− { Id = 0L
337+ { Id = 0L
273− RecipientHex = recipientHex
338+ RecipientHex = recipientHex
274− EnvelopeJson = envelopeJson
339+ EnvelopeJson = envelopeJson
275− CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }
340+ CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }
341+
276− excludeColumn o.Id
342+ excludeColumn o.Id
277 }
343 }
278 |> conn.InsertAsync
344 |> conn.InsertAsync
279 |> Async.AwaitTask
345 |> Async.AwaitTask
@@ -282,9 +348,10 @@ module Store =
282
348
283 let peekOutbox () : OutboxRow option =
349 let peekOutbox () : OutboxRow option =
284 use conn = openConn ()
350 use conn = openConn ()
351+
285 select {
352 select {
286 for o in outboxTable do
353 for o in outboxTable do
287− orderBy o.Id
354+ orderBy o.Id
288 }
355 }
289 |> conn.SelectAsync<OutboxRow>
356 |> conn.SelectAsync<OutboxRow>
290 |> Async.AwaitTask
357 |> Async.AwaitTask
@@ -293,9 +360,10 @@ module Store =
293
360
294 let dequeueOutbox (id: int64) =
361 let dequeueOutbox (id: int64) =
295 use conn = openConn ()
362 use conn = openConn ()
363+
296 delete {
364 delete {
297 for o in outboxTable do
365 for o in outboxTable do
298− where (o.Id = id)
366+ where (o.Id = id)
299 }
367 }
300 |> conn.DeleteAsync
368 |> conn.DeleteAsync
301 |> Async.AwaitTask
369 |> Async.AwaitTask
client/Styles.fsnew file+28
@@ -0,0 +1,28 @@
1+namespace Skrepka
2+
3+
4+module Styles =
5+ open Avalonia
6+ open Avalonia.Controls
7+ open Avalonia.Controls.Presenters
8+ open Avalonia.Styling
9+
10+ let style (selector: Selector -> Selector) (setters: Setter list) =
11+ let s = Style(selector)
12+
13+ for setter in setters do
14+ s.Setters.Add(setter)
15+
16+ s :> IStyle
17+
18+ let noListBoxPadding () =
19+ [ style
20+ _.OfType<ListBoxItem>()
21+ [ Setter(ListBoxItem.PaddingProperty, Thickness(0.))
22+ Setter(ListBoxItem.MinHeightProperty, box 0.) ]
23+ style
24+ (_.OfType<ListBoxItem>()
25+ .Template()
26+ .OfType<ContentPresenter>()
27+ .Name("PART_ContentPresenter"))
28+ [ Setter(ContentPresenter.MarginProperty, Thickness(0.)) ] ]