Compare commits
618 Commits
6377856ae0
...
3.5.0+138
| Author | SHA1 | Date | |
|---|---|---|---|
|
c16d8a5912
|
|||
|
cb9eca0424
|
|||
|
d7858bab67
|
|||
|
5ce590029b
|
|||
|
4d92dec45c
|
|||
|
0a4e797eec
|
|||
|
38dffa414f
|
|||
|
eea56a742e
|
|||
|
56b7ee1d69
|
|||
|
3b564f7e7f
|
|||
|
93d2670063
|
|||
|
788165ac5b
|
|||
|
ec71125fa9
|
|||
|
adb231278c
|
|||
|
b3ae4ab36f
|
|||
|
3670fe0f10
|
|||
|
bb1a5155ed
|
|||
|
eb90dbbc5a
|
|||
|
a73d9f8ec0
|
|||
|
4c8f2e3251
|
|||
|
bc9d2ab8ce
|
|||
|
8bc01f1b97
|
|||
|
200cf3ec80
|
|||
|
d910d837eb
|
|||
|
56d1f14206
|
|||
|
a7c8a8d2ee
|
|||
|
411c71dae0
|
|||
|
a8430604f9
|
|||
|
fe37d219b7
|
|||
|
bc1ebc799a
|
|||
|
37940ef12a
|
|||
|
5d0469e187
|
|||
|
7ad7ab53a6
|
|||
|
6b0343d3dc
|
|||
|
f541580281
|
|||
|
6e7eedc026
|
|||
|
5d5bda7925
|
|||
|
48e66580c3
|
|||
|
836449e3f4
|
|||
|
804dd029b1
|
|||
|
e13928b03f
|
|||
|
5c14236603
|
|||
|
738ed357bf
|
|||
|
0876ab9b74
|
|||
|
7071399cd8
|
|||
|
af23df6e48
|
|||
|
e7e7cc424b
|
|||
|
56ad8f60ea
|
|||
|
026dd3eb01
|
|||
|
72baf0ca5c
|
|||
|
82cb8c7ff9
|
|||
|
a266177628
|
|||
|
2474c7f97c
|
|||
|
1716afd66c
|
|||
|
78a3cd6dd2
|
|||
|
d655840e85
|
|||
|
2e3e988125
|
|||
|
2a94ed5171
|
|||
|
0948810993
|
|||
|
689965c582
|
|||
|
ac82fdb8c8
|
|||
|
d94baab877
|
|||
|
0a179acb13
|
|||
|
33686b83e3
|
|||
|
09abe79f6a
|
|||
|
b0b227f36b
|
|||
|
62a45317a9
|
|||
|
f727882b93
|
|||
|
ba6d6ef97a
|
|||
|
c904826c49
|
|||
|
595aa45378
|
|||
|
a481b1b82f
|
|||
|
2df31e4244
|
|||
|
9c1eb8e5bc
|
|||
|
4d095aa333
|
|||
|
fb62ce7735
|
|||
|
b258df56c9
|
|||
|
2bf54099f9
|
|||
|
eb89d9223a
|
|||
|
87a54625aa
|
|||
|
30b2c0a0b4
|
|||
|
59c34ada40
|
|||
|
67a522753e
|
|||
|
e6338e8a5a
|
|||
|
cb7eef943c
|
|||
|
7a56e7882e
|
|||
|
b0085c2ab0
|
|||
|
d3f990691e
|
|||
|
46a773cfe9
|
|||
|
f5fb5d8a98
|
|||
|
4d87ca7cca
|
|||
|
e16a04bd5a
|
|||
|
d68b39f80f
|
|||
|
b7360f1f91
|
|||
|
5f094aca4b
|
|||
|
6010c17900
|
|||
|
2ee6b3514c
|
|||
|
8c83ee9b88
|
|||
|
18c81503f1
|
|||
|
53137aed3f
|
|||
|
b2aa8b8ec1
|
|||
|
b13a4f5bcf
|
|||
|
8fe703ef6d
|
|||
|
2297fb3c47
|
|||
|
580663dcda
|
|||
|
de20803119
|
|||
|
fb51d2076f
|
|||
|
d8485954fa
|
|||
|
d7746d14e4
|
|||
|
648d5225f6
|
|||
|
9d4d0f2e48
|
|||
|
fe386163f4
|
|||
|
ac2cee10e5
|
|||
|
9c370647dd
|
|||
|
7516e197fe
|
|||
|
71c372ab6c
|
|||
|
25f23f7f93
|
|||
|
51853698b9
|
|||
|
39ed5393ab
|
|||
|
782b3f1b08
|
|||
|
3ef2f13dd3
|
|||
|
36b0f55a47
|
|||
|
bc7a6e865e
|
|||
|
2ff60fc4ff
|
|||
|
ea93aa144e
|
|||
|
e4cd0c99df
|
|||
|
dff84dde58
|
|||
|
16c7b7e764
|
|||
|
240509ceff
|
|||
|
91da9768c1
|
|||
|
60b8e2bcad
|
|||
|
504e4d55ad
|
|||
|
38a15bb62a
|
|||
|
9d03faf594
|
|||
|
fd79c11d18
|
|||
|
c4ac256896
|
|||
|
c1fc8ea3fe
|
|||
|
29574ada88
|
|||
|
7369f5d88c
|
|||
|
5b3c138ebe
|
|||
|
562bdf62e9
|
|||
|
a73672925e
|
|||
|
c585522c35
|
|||
|
6aba84e506
|
|||
|
c6f104afc7
|
|||
|
4181fd0090
|
|||
|
84bca9601a
|
|||
|
31b83b2d27
|
|||
|
dfcb089c69
|
|||
|
fe365e8c6d
|
|||
|
b5262137ad
|
|||
|
11e93314c7
|
|||
|
c8658bc0ca
|
|||
|
b2f689693b
|
|||
|
33ec0b1d9a
|
|||
|
f698385494
|
|||
|
6ecdf89d20
|
|||
|
683f686540
|
|||
|
6a115ab1cc
|
|||
|
d05283d3b1
|
|||
|
b9653e7264
|
|||
|
87d1c8b320
|
|||
|
22eb54b61f
|
|||
|
a90ad2debc
|
|||
|
2f00bf660d
|
|||
|
322a93324c
|
|||
|
c3a3be0807
|
|||
|
27c7c8f039
|
|||
|
a7960da362
|
|||
|
64ab30b0a9
|
|||
|
935e6d5833
|
|||
|
938b128b1e
|
|||
|
c9764daa20
|
|||
|
7bc44e8f06
|
|||
|
4a7ff96a8b
|
|||
|
e759d5f46c
|
|||
|
f5ca6a37bf
|
|||
|
5fc8859f3b
|
|||
|
e30e7adbe2
|
|||
|
68be4db160
|
|||
|
aa91e376ca
|
|||
|
caffb85588
|
|||
|
521b192205
|
|||
|
77ac0428ea
|
|||
|
88c8227c66
|
|||
|
b20d8350a8
|
|||
|
98b27bed0e
|
|||
|
3a7d8b1a0d
|
|||
|
b4801d6af6
|
|||
|
aab5b957af
|
|||
|
43d706a184
|
|||
|
98df275f88
|
|||
|
5663df6ef1
|
|||
|
e996a0c95f
|
|||
|
a090e93f57
|
|||
|
c69034c071
|
|||
|
369ea6cf5b
|
|||
|
2e371b5296
|
|||
|
2e9d61bcfa
|
|||
|
9c2b5b0dfa
|
|||
|
3b40f515b3
|
|||
|
5ee61dbef2
|
|||
|
b151ef6686
|
|||
|
ff934d0f08
|
|||
|
abe5ded896
|
|||
|
f1d72a5215
|
|||
|
864cbe73b7
|
|||
|
108a6da074
|
|||
|
f9a09599c9
|
|||
|
9067dadd3e
|
|||
|
09f8df1e78
|
|||
|
2c5f246c55
|
|||
|
a66c6ea654
|
|||
|
3ad4bb4518
|
|||
|
53f0dcb825
|
|||
|
557f5a2389
|
|||
|
78f14f890f
|
|||
|
77b2effb34
|
|||
|
f02b4abf65
|
|||
|
3f37c4f761
|
|||
|
5deb910fa4
|
|||
|
f50a19f573
|
|||
|
98c8a356e8
|
|||
|
d0c16ea08f
|
|||
|
f2c1b2a531
|
|||
|
3061f0c5a9
|
|||
|
98f7f33c65
|
|||
|
d9af5d32fd
|
|||
|
f2031697ec
|
|||
|
9b85b7573c
|
|||
|
4fb739b33b
|
|||
|
c03ba3bc3a
|
|||
|
fc65440420
|
|||
|
7b85533184
|
|||
|
77d9eb60c6
|
|||
|
4d8953cd22
|
|||
|
fafa460fe8
|
|||
|
faf3a677d4
|
|||
|
0f644a0234
|
|||
|
18d16fdd57
|
|||
|
18e890d63c
|
|||
|
9c5e50c16a
|
|||
|
96a2c8182e
|
|||
|
56b27c3e82
|
|||
|
ad4bf94195
|
|||
|
b77a832d8a
|
|||
|
5e61805db7
|
|||
|
35b96b0bd2
|
|||
|
c8ad791ff3
|
|||
|
1e908502dc
|
|||
|
715ce1a368
|
|||
|
548c9963ee
|
|||
|
db5199438a
|
|||
|
4409a6fb1e
|
|||
|
26a24b0e41
|
|||
|
9b948d259b
|
|||
|
1f713b5b2b
|
|||
|
f92cfafda4
|
|||
|
fa208b44d7
|
|||
|
94adecafbb
|
|||
|
0303ef4a93
|
|||
|
c2b18ce10b
|
|||
|
0767bb53ce
|
|||
|
b233f9a410
|
|||
|
256024fb46
|
|||
|
4a80aaf24d
|
|||
|
aafd160c44
|
|||
|
4a800725e3
|
|||
|
24791b3293
|
|||
|
3ac263d483
|
|||
|
2445d8adf8
|
|||
|
d4f95bbbf4
|
|||
|
943e4b7b5c
|
|||
|
7edc02a1d3
|
|||
|
3f9881e943
|
|||
|
50c25e919c
|
|||
|
99fb08dd55
|
|||
|
e43bc6b8a8
|
|||
|
c247cdf81c
|
|||
|
3ffa730505
|
|||
|
1cc34d3073
|
|||
|
96a919cc4e
|
|||
|
e7e3bfcadf
|
|||
|
a8617a5040
|
|||
|
d94f8d004f
|
|||
|
d93b066979
|
|||
|
320664a547
|
|||
|
98f4698d5b
|
|||
|
82397dd087
|
|||
|
4ec10ceb47
|
|||
|
4b03b45a0d
|
|||
|
7a72d32649
|
|||
|
5152dd13ea
|
|||
|
fd377aa7af
|
|||
|
67044148f1
|
|||
|
92bc43e4df
|
|||
|
a1a7b34c86
|
|||
|
40c0e052cf
|
|||
|
9a75228e38
|
|||
|
a9fd75cc45
|
|||
|
a713b30d93
|
|||
|
e516f0a862
|
|||
|
429b966c4b
|
|||
|
f14da0d3a2
|
|||
|
d201182bd2
|
|||
|
6f6422c15e
|
|||
|
9f6ae639ee
|
|||
|
35f4d7d885
|
|||
|
a9c8f49797
|
|||
|
5e9341a19c
|
|||
|
645a6dca93
|
|||
|
ea8e7ead2d
|
|||
|
5f2f083d72
|
|||
|
5cf40e27de
|
|||
|
1ab7295918
|
|||
|
07f191171c
|
|||
|
4a5dac248e
|
|||
|
3b983a6444
|
|||
|
4607b77355
|
|||
|
7957e4894a
|
|||
|
f94f80c375
|
|||
|
74fa2215a6
|
|||
|
0d11435feb
|
|||
|
e22598b0a6
|
|||
|
84cfe643f5
|
|||
|
05ac04e9a2
|
|||
|
66f283d6e8
|
|||
|
c779c7523c
|
|||
|
ac7cb29afe
|
|||
|
935aa77223
|
|||
|
24e5b3b824
|
|||
|
0391893b32
|
|||
|
b8d24876c8
|
|||
|
0493661f9a
|
|||
|
b40afde00f
|
|||
|
78a4022531
|
|||
|
8a291c80b7
|
|||
|
1395d65b76
|
|||
|
eb4942e0ed
|
|||
|
f254cfa81e
|
|||
|
4927795260
|
|||
|
e4019dadc8
|
|||
|
5e7d77e1a1
|
|||
|
bfcbed035c
|
|||
|
5ebefae961
|
|||
|
d4758674bb
|
|||
|
f5f1ddc0ea
|
|||
|
2720b59485
|
|||
|
29b1ac7fce
|
|||
|
83ca5551ad
|
|||
| 611cb024a9 | |||
|
74fb56891d
|
|||
|
ac4fa5eb85
|
|||
|
8857718709
|
|||
|
dd17b2b9c1
|
|||
|
848439f664
|
|||
|
f83117424d
|
|||
|
8c19c32c76
|
|||
|
d62b2bed80
|
|||
|
5a23eb1768
|
|||
|
5f6e4763d3
|
|||
|
580c36fb89
|
|||
|
6c25af3b30
|
|||
|
a1da72d447
|
|||
|
ab4120cc22
|
|||
|
52eff0fa25
|
|||
|
beeb28abf2
|
|||
|
c0ab3837ac
|
|||
|
59d38c0d8d
|
|||
|
bd2247ce86
|
|||
|
da2d3f7f17
|
|||
|
7497b77384
|
|||
|
f542d9fa97
|
|||
|
e70439870e
|
|||
|
d764b042fe
|
|||
|
a76b97d1d2
|
|||
|
cfbe6e580b
|
|||
|
f08b9e057f
|
|||
|
0509f37c96
|
|||
|
a7dc9ac6fa
|
|||
|
caf2f5f1f6
|
|||
|
12b79af3a2
|
|||
|
88f149584e
|
|||
|
877001b802
|
|||
| fec28f6223 | |||
| 85005ff9c3 | |||
| e3c92a3c55 | |||
| 9e9fbc5d6a | |||
| 8d1d836b52 | |||
| bc60ce5d42 | |||
| c093123e3a | |||
| 3de73538c7 | |||
| ba8d5cee09 | |||
|
5ee2e70442
|
|||
|
53a3a32907
|
|||
|
9a628779d9
|
|||
|
b60bd63d0c
|
|||
|
01cc71fd47
|
|||
|
a2b0cd0b6a
|
|||
|
7f971bcee3
|
|||
|
7de98a1731
|
|||
|
b52eb95b14
|
|||
|
b3ef7d6ad0
|
|||
|
d28c11940d
|
|||
|
504322c2dd
|
|||
|
a07ec3ca36
|
|||
| d96691e920 | |||
|
6273b2d917
|
|||
|
ab90d244b5
|
|||
|
dc6af6d9e5
|
|||
|
0ca801d963
|
|||
|
3edcdd72af
|
|||
|
402bb3fe04
|
|||
|
8ba55eb1be
|
|||
|
983ae2a1fc
|
|||
|
6fc94001b3
|
|||
|
44dbcfdc94
|
|||
|
b57caf56db
|
|||
|
dbcd1b6d36
|
|||
|
a8055de910
|
|||
|
49b15e7674
|
|||
|
e2369c40db
|
|||
|
44c5d91620
|
|||
|
7a5a2407b7
|
|||
|
234434f102
|
|||
|
9c3b228d02
|
|||
|
82682cae9a
|
|||
|
fcbd5fe680
|
|||
|
ad91b17af7
|
|||
|
24fa637329
|
|||
|
926ae5402f
|
|||
|
1a37d384e6
|
|||
|
d4cf598f69
|
|||
|
0106c08891
|
|||
|
9697def808
|
|||
|
6572875229
|
|||
|
66590b9079
|
|||
|
08b9604b55
|
|||
|
0602bbd277
|
|||
|
76e7ba7898
|
|||
|
6e6616b236
|
|||
|
071d51b25e
|
|||
|
a958362461
|
|||
|
6749bb00fe
|
|||
|
11fb20c673
|
|||
|
a7990f83db
|
|||
|
5f4cdf7937
|
|||
|
3330ca14dd
|
|||
|
1719b1c8fe
|
|||
|
3c2c51bfaf
|
|||
|
239d6750ff
|
|||
|
8b0c91977a
|
|||
|
f74cca8464
|
|||
|
08091d51bf
|
|||
|
481190811b
|
|||
|
4b32b65d1c
|
|||
|
50ac7109bb
|
|||
|
62da279c71
|
|||
|
fde6dbf891
|
|||
|
613bf4fb42
|
|||
|
00ae586016
|
|||
|
ea0d132dce
|
|||
|
aa2df1e847
|
|||
|
50672795f3
|
|||
|
383de9568d
|
|||
|
01fa228e45
|
|||
|
1e71ad33a6
|
|||
|
92c0260ecd
|
|||
|
0a161ad255
|
|||
|
c003f27b9a
|
|||
|
19db8309c4
|
|||
|
aa72ce08e8
|
|||
|
4639b00b86
|
|||
|
cc5460ea55
|
|||
|
eafac811e6
|
|||
|
e3be691596
|
|||
|
aa180a1358
|
|||
|
c2707b8af1
|
|||
|
62fd0500f3
|
|||
|
eeae865cc8
|
|||
|
cdf1413fe0
|
|||
|
327b4c04f1
|
|||
|
bd903ce29c
|
|||
|
1b8ecb15ce
|
|||
|
d4e380a97a
|
|||
|
126048b4fa
|
|||
|
8bec18813d
|
|||
|
1ae81794b1
|
|||
|
2a7d12de48
|
|||
|
64c60ead48
|
|||
| 001549b190 | |||
| 4595865ad3 | |||
|
|
1834643167 | ||
|
|
0e816eaa3e | ||
|
|
7c1f24b824 | ||
|
c6594ea2ce
|
|||
|
3bec6e683e
|
|||
|
83e92e2eed
|
|||
|
|
b7d44d96ba | ||
|
a83b929d42
|
|||
|
9423affa75
|
|||
|
cda23db609
|
|||
|
61074bc5a3
|
|||
|
5feafa9255
|
|||
|
e604577c1f
|
|||
|
af0ddd1273
|
|||
|
8a6bb34808
|
|||
|
4ef8445c77
|
|||
|
ec39ad6ca3
|
|||
|
eabb3154f1
|
|||
|
910bf20eef
|
|||
|
5efa9b2ae8
|
|||
|
dd3e39e891
|
|||
|
b6896ded23
|
|||
|
f28a73ff9c
|
|||
|
a014b64235
|
|||
|
7e0e7c20d7
|
|||
|
389fa515ba
|
|||
|
681ead02eb
|
|||
|
8d1c145b0b
|
|||
|
51b4754182
|
|||
|
8a2b321701
|
|||
|
f685a7a249
|
|||
|
76009147e9
|
|||
|
ce12f28e56
|
|||
|
3604373a1e
|
|||
|
9704a4c2c7
|
|||
|
67def56ad1
|
|||
|
1be33916af
|
|||
|
e8ff1bfd22
|
|||
|
3ae56f3d89
|
|||
|
707143e998
|
|||
|
1fd34eb2a3
|
|||
|
d7ca41e946
|
|||
|
ad9fb0719a
|
|||
|
e2d315afd4
|
|||
|
6124dbfd79
|
|||
|
5327f04ec0
|
|||
|
41c56a2319
|
|||
|
f9d033542e
|
|||
|
91784e65e6
|
|||
|
9d39c6a825
|
|||
|
537e49f1a4
|
|||
|
75bbd4df71
|
|||
|
6ef4580d93
|
|||
|
6ffd498761
|
|||
|
27157e7cc1
|
|||
|
bbb07d574a
|
|||
|
c660a419e2
|
|||
|
c3f61467c8
|
|||
|
9bc47df452
|
|||
|
9ef8ca4d45
|
|||
|
b55cbd08d1
|
|||
|
8c6bd0feaa
|
|||
|
7dd4b20628
|
|||
|
fec0cb7640
|
|||
|
75deb04a2b
|
|||
|
7c7ed21a96
|
|||
|
a201f20793
|
|||
|
598c51bc1a
|
|||
|
e1ea61c5f1
|
|||
|
ac424bde36
|
|||
|
b43b70df3f
|
|||
|
4321aa621a
|
|||
|
d5d275fb43
|
|||
|
6bb3307144
|
|||
|
391604d4a2
|
|||
|
1d9361c12f
|
|||
|
a129b9cdd0
|
|||
|
3bf815ac61
|
|||
|
77bae4d6fd
|
|||
|
0a301c4c9b
|
|||
|
27b390a51c
|
|||
|
018386d14e
|
|||
|
3825d7c6c7
|
|||
|
bf930291e4
|
|||
|
a8c4988790
|
|||
|
28dd204b1a
|
|||
|
3cbc1a59a7
|
|||
|
277e9ae3d1
|
|||
|
27b3ca25b7
|
|||
|
f871cd3b62
|
|||
|
a8a59ee30c
|
|||
|
2cd1416a13
|
|||
|
6be7dfbc61
|
|||
|
1abbd85614
|
|||
|
31ac5ad07c
|
|||
|
ae2ba495e9
|
|||
|
637aa44548
|
|||
|
44dbfc36d9
|
|||
|
5dbe7371cb
|
|||
|
6c91093198
|
|||
|
3f640b7898
|
|||
|
7db164fda6
|
|||
|
6df1d96cc9
|
|||
|
122a796f8c
|
|||
|
fbc7812a16
|
|||
|
0b1a23e81a
|
|||
|
c87e6cfe07
|
|||
|
53d51b8a0e
|
|||
|
337ae39e08
|
|||
|
8fe3a664a6
|
|||
|
3bfc0b8181
|
|||
| ac2951479b | |||
| 2bfd13d843 | |||
| 28db6f9f01 | |||
|
a4f7b8415d
|
|||
|
2255d3d591
|
|||
|
97792ae734
|
|||
|
a5d13250cc
|
|||
|
de9e235d0c
|
|||
|
56fb5451cd
|
|||
|
870de961f5
|
|||
|
22bf6d1c33
|
|||
|
5b62f89531
|
|||
|
b1326d8f04
|
|||
|
fffca4a78c
|
|||
|
42bd7f97cb
|
@@ -62,3 +62,9 @@ If you want to build the release version, use the flutter build command. Learn m
|
|||||||
```bash
|
```bash
|
||||||
flutter build <platform>
|
flutter build <platform>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
|
||||||
|
Due to the issues with the flutter build tools, [see](https://github.com/flutter/flutter/issues/160622).
|
||||||
|
|
||||||
|
Since there is a watchOS app for iOS, you're unable to use the flutter cli to run iOS app. Use xcode instead.
|
||||||
@@ -75,3 +75,4 @@ dependencies {
|
|||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="29" />
|
android:maxSdkVersion="29" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||||
@@ -43,6 +45,16 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- App protocol -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
|
||||||
|
<data android:scheme="solian" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Deeplinking -->
|
<!-- Deeplinking -->
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -51,6 +63,12 @@
|
|||||||
<data android:scheme="http" android:host="solian.app" />
|
<data android:scheme="http" android:host="solian.app" />
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="solian" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Share Intent Filters -->
|
<!-- Share Intent Filters -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -143,4 +161,4 @@
|
|||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -158,14 +158,12 @@
|
|||||||
"checkIn": "Check In",
|
"checkIn": "Check In",
|
||||||
"checkInNone": "Not checked-in yet",
|
"checkInNone": "Not checked-in yet",
|
||||||
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
||||||
"checkInResultLevel0": "Wrost Luck",
|
"checkInResultLevel0": "Worst Luck",
|
||||||
"checkInResultLevel1": "Bad Luck",
|
"checkInResultLevel1": "Bad Luck",
|
||||||
"checkInResultLevel2": "A Normal Day",
|
"checkInResultLevel2": "A Normal Day",
|
||||||
"checkInResultLevel3": "Good Luck",
|
"checkInResultLevel3": "Good Luck",
|
||||||
"checkInResultLevel4": "Best Luck",
|
"checkInResultLevel4": "Best Luck",
|
||||||
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
||||||
"eventCalander": "Event Calander",
|
|
||||||
"eventCalanderEmpty": "No events on that day.",
|
|
||||||
"fortuneGraph": "Fortune Trend",
|
"fortuneGraph": "Fortune Trend",
|
||||||
"noFortuneData": "No fortune data available for this month.",
|
"noFortuneData": "No fortune data available for this month.",
|
||||||
"creatorHub": "Creator Hub",
|
"creatorHub": "Creator Hub",
|
||||||
@@ -257,6 +255,24 @@
|
|||||||
"walletCurrencyShortGolds": "NSD",
|
"walletCurrencyShortGolds": "NSD",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
"creatorHubUnselectedHint": "Pick / create a publisher to get started.",
|
"creatorHubUnselectedHint": "Pick / create a publisher to get started.",
|
||||||
|
"publisherFediverse": "Fediverse Actor",
|
||||||
|
"publisherFediverseDescription": "Configure your publisher's ActivityPub actor for federated social networking",
|
||||||
|
"publisherFediverseEnabled": "Enabled",
|
||||||
|
"publisherFediverseDisabled": "Disabled",
|
||||||
|
"publisherFediverseNotConfigured": "Not configured",
|
||||||
|
"publisherFediverseEnableHint": "Enable your publisher to interact with fediverse",
|
||||||
|
"publisherFediverseDisableHint": "Disable your publisher's fediverse actor",
|
||||||
|
"publisherFediverseEnableConfirm": "Enable fediverse actor?",
|
||||||
|
"publisherFediverseDisableConfirm": "Disable fediverse actor?",
|
||||||
|
"publisherFediverseEnabledSuccess": "Fediverse actor enabled successfully",
|
||||||
|
"publisherFediverseDisabledSuccess": "Fediverse actor disabled successfully",
|
||||||
|
"publisherFediverseFailedToEnable": "Failed to enable fediverse actor",
|
||||||
|
"publisherFediverseFailedToDisable": "Failed to disable fediverse actor",
|
||||||
|
"publisherFediverseWhatIs": "What is Fediverse?",
|
||||||
|
"publisherFediverseAbout": "The fediverse is a federated network of social platforms. Enabling this feature allows your publisher to interact with users across different ActivityPub-compatible services like Mastodon, PeerTube, and more.",
|
||||||
|
"publisherFediverseActorUri": "Actor URI",
|
||||||
|
"publisherFediverseFollowerCount": "Followers",
|
||||||
|
"publisherFediverseNoFollowers": "No followers yet",
|
||||||
"relationships": "Relationships",
|
"relationships": "Relationships",
|
||||||
"addFriend": "Send a Friend Request",
|
"addFriend": "Send a Friend Request",
|
||||||
"addFriendShort": "Add as Friend",
|
"addFriendShort": "Add as Friend",
|
||||||
@@ -316,7 +332,6 @@
|
|||||||
"settingsAutoTranslate": "Auto Translate",
|
"settingsAutoTranslate": "Auto Translate",
|
||||||
"settingsHideBottomNav": "Hide Bottom Navigation",
|
"settingsHideBottomNav": "Hide Bottom Navigation",
|
||||||
"settingsSoundEffects": "Sound Effects",
|
"settingsSoundEffects": "Sound Effects",
|
||||||
"settingsAprilFoolFeatures": "April Fool Features",
|
|
||||||
"settingsEnterToSend": "Enter to Send",
|
"settingsEnterToSend": "Enter to Send",
|
||||||
"settingsTransparentAppBar": "Transparent App Bar",
|
"settingsTransparentAppBar": "Transparent App Bar",
|
||||||
"settingsCustomFonts": "Custom Fonts",
|
"settingsCustomFonts": "Custom Fonts",
|
||||||
@@ -391,10 +406,6 @@
|
|||||||
"other": "{} are typing..."
|
"other": "{} are typing..."
|
||||||
},
|
},
|
||||||
"settingsAppearance": "Appearance",
|
"settingsAppearance": "Appearance",
|
||||||
"settingsThemeMode": "Theme Mode",
|
|
||||||
"settingsThemeModeSystem": "System",
|
|
||||||
"settingsThemeModeLight": "Light",
|
|
||||||
"settingsThemeModeDark": "Dark",
|
|
||||||
"settingsServer": "Server",
|
"settingsServer": "Server",
|
||||||
"settingsBehavior": "Behavior",
|
"settingsBehavior": "Behavior",
|
||||||
"settingsDesktop": "Desktop",
|
"settingsDesktop": "Desktop",
|
||||||
@@ -688,9 +699,9 @@
|
|||||||
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
|
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
|
||||||
"postVisibility": "Post Visibility",
|
"postVisibility": "Post Visibility",
|
||||||
"currentMembershipMember": "A member of Stellar Program · {}",
|
"currentMembershipMember": "A member of Stellar Program · {}",
|
||||||
"membershipPriceStellar": "1200 NSP per month, level 3+ required",
|
"membershipPriceStellar": "1200 NSP per month, level 20+ required",
|
||||||
"membershipPriceNova": "2400 NSP per month, level 6+ required",
|
"membershipPriceNova": "2400 NSP per month, level 40+ required",
|
||||||
"membershipPriceSupernova": "3600 NSP per month, level 9+ required",
|
"membershipPriceSupernova": "3600 NSP per month, level 60+ required",
|
||||||
"sharePostPhoto": "Share Post as Photo",
|
"sharePostPhoto": "Share Post as Photo",
|
||||||
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
|
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
|
||||||
"abuseReports": "Abuse Reports",
|
"abuseReports": "Abuse Reports",
|
||||||
@@ -711,7 +722,7 @@
|
|||||||
"aboutScreenDeveloperSectionTitle": "Developer",
|
"aboutScreenDeveloperSectionTitle": "Developer",
|
||||||
"aboutScreenContactUsTitle": "Contact Us",
|
"aboutScreenContactUsTitle": "Contact Us",
|
||||||
"aboutScreenLicenseTitle": "License",
|
"aboutScreenLicenseTitle": "License",
|
||||||
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
|
"aboutScreenLicenseContent": "AGPLv3",
|
||||||
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
|
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
|
||||||
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
|
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
|
||||||
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
|
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
|
||||||
@@ -756,21 +767,6 @@
|
|||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"markAsSensitive": "Mark as Sensitive",
|
"markAsSensitive": "Mark as Sensitive",
|
||||||
"fileName": "File name",
|
"fileName": "File name",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "Language",
|
|
||||||
"sexualContent": "Sexual Content",
|
|
||||||
"violence": "Violence",
|
|
||||||
"profanity": "Profanity",
|
|
||||||
"hateSpeech": "Hate Speech",
|
|
||||||
"racism": "Racism",
|
|
||||||
"adultContent": "Adult Content",
|
|
||||||
"drugAbuse": "Drug Abuse",
|
|
||||||
"alcoholAbuse": "Alcohol Abuse",
|
|
||||||
"gambling": "Gambling",
|
|
||||||
"selfHarm": "Self-harm",
|
|
||||||
"childAbuse": "Child Abuse",
|
|
||||||
"other": "Other"
|
|
||||||
},
|
|
||||||
"poll": "Poll",
|
"poll": "Poll",
|
||||||
"pollsRecent": "Recent Polls",
|
"pollsRecent": "Recent Polls",
|
||||||
"pollCreateNew": "Create New",
|
"pollCreateNew": "Create New",
|
||||||
@@ -1040,7 +1036,6 @@
|
|||||||
"authDeviceChallenges": "Device Usage",
|
"authDeviceChallenges": "Device Usage",
|
||||||
"authDeviceHint": "Swipe left to edit label, swipe right to logout device.",
|
"authDeviceHint": "Swipe left to edit label, swipe right to logout device.",
|
||||||
"settingsMessageDisplayStyle": "Message Display Style",
|
"settingsMessageDisplayStyle": "Message Display Style",
|
||||||
"settingsWindowOpacity": "Window Opacity",
|
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"manual": "Manual",
|
"manual": "Manual",
|
||||||
"iframeCode": "Iframe Code",
|
"iframeCode": "Iframe Code",
|
||||||
@@ -1081,9 +1076,510 @@
|
|||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
|
"deleteMessage": "Delete Message",
|
||||||
|
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
|
||||||
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactions": "Custom Reactions",
|
||||||
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
|
"reactionAttitude": "Reaction Attitude",
|
||||||
|
"addReaction": "Add Reaction",
|
||||||
|
"eventCalendar": "Event Calendar",
|
||||||
|
"eventCalendarEmpty": "No events on that day.",
|
||||||
|
"walletStats": "Wallet Statistics",
|
||||||
|
"totalTransactions": "Total Transactions",
|
||||||
|
"totalOrders": "Total Orders",
|
||||||
|
"totalIncome": "Total Income",
|
||||||
|
"totalOutgoing": "Total Outgoing",
|
||||||
|
"netBalance": "Net Balance",
|
||||||
|
"messageUpdateLinks": "Server generated links previews",
|
||||||
|
"messageUpdateEdited": "Edited a message",
|
||||||
|
"settingsCardBackgroundOpacity": "Card Background Opacity",
|
||||||
|
"settingsThemeMode": "Theme Mode",
|
||||||
|
"settingsThemeModeSystem": "System",
|
||||||
|
"settingsThemeModeLight": "Light",
|
||||||
|
"settingsThemeModeDark": "Dark",
|
||||||
|
"enterPin": "Enter your PIN code",
|
||||||
|
"chatReplyingTo": "Replying to {}",
|
||||||
|
"chatForwarding": "Forwarding message",
|
||||||
|
"chatEditing": "Editing message",
|
||||||
|
"chatNoContent": "No content",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "Language",
|
||||||
|
"sexualContent": "Sexual Content",
|
||||||
|
"violence": "Violence",
|
||||||
|
"profanity": "Profanity",
|
||||||
|
"hateSpeech": "Hate Speech",
|
||||||
|
"racism": "Racism",
|
||||||
|
"adultContent": "Adult Content",
|
||||||
|
"drugAbuse": "Drug Abuse",
|
||||||
|
"alcoholAbuse": "Alcohol Abuse",
|
||||||
|
"gambling": "Gambling",
|
||||||
|
"selfHarm": "Self-harm",
|
||||||
|
"childAbuse": "Child Abuse",
|
||||||
|
"other": "Other"
|
||||||
|
},
|
||||||
|
"Searching...": "Searching...",
|
||||||
|
"searchError": "Search failed. Please try again.",
|
||||||
|
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
||||||
|
"settingsWindowOpacity": "Window Opacity",
|
||||||
|
"messageContent": "Message Content",
|
||||||
"updateAvailable": "Update available",
|
"updateAvailable": "Update available",
|
||||||
"noChangelogProvided": "No changelog provided.",
|
"noChangelogProvided": "No changelog provided.",
|
||||||
"useSecondarySourceForDownload": "Use secondary source for download",
|
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||||
"installUpdate": "Install update",
|
"installUpdate": "Install update",
|
||||||
"openReleasePage": "Open release page"
|
"openReleasePage": "Open release page",
|
||||||
}
|
"postCompose": "Compose Post",
|
||||||
|
"postPublish": "Publish Post",
|
||||||
|
"restoreDraftTitle": "Restore Draft",
|
||||||
|
"restoreDraftMessage": "A draft was found. Do you want to restore it?",
|
||||||
|
"draft": "Draft",
|
||||||
|
"purchaseGift": "Purchase Gift",
|
||||||
|
"selectRecipient": "Select Recipient",
|
||||||
|
"changeRecipient": "Change Recipient",
|
||||||
|
"addMessage": "Add Message",
|
||||||
|
"skipRecipient": "Skip Recipient",
|
||||||
|
"giftSubscriptions": "Gift Subscriptions",
|
||||||
|
"purchaseAGift": "Purchase a Gift",
|
||||||
|
"redeemAGift": "Redeem a Gift",
|
||||||
|
"giftHistory": "Gift History",
|
||||||
|
"sentGifts": "Sent Gifts",
|
||||||
|
"receivedGifts": "Received Gifts",
|
||||||
|
"noSentGifts": "No sent gifts",
|
||||||
|
"noReceivedGifts": "No received gifts",
|
||||||
|
"stellarGift": "Stellar Gift",
|
||||||
|
"novaGift": "Nova Gift",
|
||||||
|
"supernovaGift": "Supernova Gift",
|
||||||
|
"sameAsMembership": "Same as membership",
|
||||||
|
"enterGiftCodeToRedeem": "Enter gift code to redeem",
|
||||||
|
"enterGiftCode": "Enter gift code",
|
||||||
|
"giftPurchased": "Gift Purchased!",
|
||||||
|
"shareCodeWithRecipient": "Share this code with the recipient to redeem the gift.",
|
||||||
|
"openGiftAnyoneCanRedeem": "This is an open gift that anyone can redeem.",
|
||||||
|
"ok": "OK",
|
||||||
|
"selectedRecipient": "Selected recipient",
|
||||||
|
"noRecipientSelected": "No recipient selected",
|
||||||
|
"thisWillBeAnOpenGift": "This will be an open gift",
|
||||||
|
"personalMessage": "Personal Message",
|
||||||
|
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
||||||
|
"giftStatusCreated": "Created",
|
||||||
|
"giftStatusSent": "Sent",
|
||||||
|
"giftStatusRedeemed": "Redeemed",
|
||||||
|
"giftStatusCancelled": "Cancelled",
|
||||||
|
"giftStatusExpired": "Expired",
|
||||||
|
"giftStatusUnknown": "Unknown",
|
||||||
|
"giftCodeCopiedToClipboard": "Gift code copied to clipboard",
|
||||||
|
"codeLabel": "Code: ",
|
||||||
|
"subscriptionLabel": "Subscription: ",
|
||||||
|
"toLabel": "To: ",
|
||||||
|
"fromLabel": "From: ",
|
||||||
|
"messageLabel": "Message: ",
|
||||||
|
"giftRedeemed": "Gift Redeemed!",
|
||||||
|
"giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.",
|
||||||
|
"cancelGift": "Cancel Gift",
|
||||||
|
"cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.",
|
||||||
|
"giftCancelledSuccessfully": "Gift cancelled successfully",
|
||||||
|
"createFund": "Create Fund",
|
||||||
|
"fundAmount": "Fund Amount",
|
||||||
|
"enterAmount": "Enter Amount",
|
||||||
|
"selectCurrency": "Select Currency",
|
||||||
|
"splitType": "Split Type",
|
||||||
|
"evenSplit": "Even Split",
|
||||||
|
"equalAmountEach": "Equal amount for each recipient",
|
||||||
|
"randomSplit": "Random Split",
|
||||||
|
"randomAmountEach": "Random amount for each recipient",
|
||||||
|
"recipientCount": "Recipient Count",
|
||||||
|
"numberOfRecipients": "Number of Recipients",
|
||||||
|
"addPersonalMessageForRecipients": "Add a personal message for recipients",
|
||||||
|
"invalidAmount": "Invalid amount",
|
||||||
|
"invalidRecipientCount": "Invalid recipient count",
|
||||||
|
"fundOverview": "Fund Overview",
|
||||||
|
"totalFundsSent": "Total Funds Sent",
|
||||||
|
"totalFundsReceived": "Total Funds Received",
|
||||||
|
"transactions": "Transactions",
|
||||||
|
"myFunds": "My Funds",
|
||||||
|
"availableFunds": "Available Funds",
|
||||||
|
"fundStatusCreated": "Created",
|
||||||
|
"fundStatusPartial": "Partially Claimed",
|
||||||
|
"fundStatusCompleted": "Fully Claimed",
|
||||||
|
"fundStatusExpired": "Expired",
|
||||||
|
"fundStatusUnknown": "Unknown",
|
||||||
|
"recipients": "Recipients",
|
||||||
|
"fundClaimedSuccessfully": "Fund claimed successfully!",
|
||||||
|
"claim": "Claim",
|
||||||
|
"noFundsCreated": "No funds created yet",
|
||||||
|
"createYourFirstFund": "Create your first fund to get started",
|
||||||
|
"noAvailableFunds": "No available funds",
|
||||||
|
"fundsWillAppearHere": "Funds you can claim will appear here",
|
||||||
|
"fundCreatedSuccessfully": "Fund created successfully!",
|
||||||
|
"selectRecipients": "Select Recipients",
|
||||||
|
"noRecipientsSelected": "No recipients selected",
|
||||||
|
"selectRecipientsToSendFund": "Select recipients to send the fund to",
|
||||||
|
"addRecipient": "Add Recipient",
|
||||||
|
"addMoreRecipients": "Add More Recipients",
|
||||||
|
"transactionDetails": "Transaction Details",
|
||||||
|
"remarks": "Remarks",
|
||||||
|
"payer": "Payer",
|
||||||
|
"payee": "Payee",
|
||||||
|
"transactionType": "Transaction Type",
|
||||||
|
"transfer": "Transfer",
|
||||||
|
"payment": "Payment",
|
||||||
|
"systemWallet": "System Wallet",
|
||||||
|
"date": "Date",
|
||||||
|
"createTransfer": "Create Transfer",
|
||||||
|
"transferAmount": "Transfer Amount",
|
||||||
|
"selectPayee": "Select Payee",
|
||||||
|
"selectedPayee": "Selected Payee",
|
||||||
|
"noPayeeSelected": "No payee selected",
|
||||||
|
"selectPayeeToTransfer": "Select payee to transfer to",
|
||||||
|
"addRemark": "Add Remark",
|
||||||
|
"transferRemark": "Transfer Remark",
|
||||||
|
"addRemarkForTransfer": "Add remark for transfer",
|
||||||
|
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
|
||||||
|
"transferCreatedSuccessfully": "Transfer created successfully!",
|
||||||
|
"postUpdate": "Update",
|
||||||
|
"fileMetadata": "File Metadata",
|
||||||
|
"resend": "Resend",
|
||||||
|
"fileInfoTitle": "File Information",
|
||||||
|
"download": "Download",
|
||||||
|
"info": "Info",
|
||||||
|
"noStickers": "No Stickers",
|
||||||
|
"noStickersInPack": "This pack does not contains stickers",
|
||||||
|
"noStickerPacks": "No Sticker Packs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"spoiler": "Spoiler",
|
||||||
|
"activityHeatmap": "Activity Heatmap",
|
||||||
|
"custom": "Custom",
|
||||||
|
"usernameColor": "Username Color",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"plain": "Plain",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"colorValue": "Color Value",
|
||||||
|
"gradientDirection": "Gradient Direction",
|
||||||
|
"gradientDirectionToRight": "To Right",
|
||||||
|
"gradientDirectionToLeft": "To Left",
|
||||||
|
"gradientDirectionToBottom": "To Bottom",
|
||||||
|
"gradientDirectionToTop": "To Top",
|
||||||
|
"gradientDirectionToBottomRight": "To Bottom Right",
|
||||||
|
"gradientDirectionToBottomLeft": "To Bottom Left",
|
||||||
|
"gradientDirectionToTopRight": "To Top Right",
|
||||||
|
"gradientDirectionToTopLeft": "To Top Left",
|
||||||
|
"gradientColors": "Gradient Colors",
|
||||||
|
"color": "Color",
|
||||||
|
"addColor": "Add Color",
|
||||||
|
"availableWithYourPlan": "Available with your plan",
|
||||||
|
"upgradeRequired": "Upgrade required",
|
||||||
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "Time zone not found",
|
||||||
|
"awardPoints": "Awarded {} points",
|
||||||
|
"postFeaturedOn": "Post featured on {}",
|
||||||
|
"messageSentAt": "Sent at {}",
|
||||||
|
"myTickets": "My Tickets",
|
||||||
|
"drawHistory": "Draw History",
|
||||||
|
"lottery": "Lottery",
|
||||||
|
"noLotteryTickets": "No lottery tickets yet",
|
||||||
|
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
|
||||||
|
"buyTicket": "Buy Ticket",
|
||||||
|
"ticketNumbers": "Numbers: {}, Special: {}",
|
||||||
|
"cost": "Cost",
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"prizeWon": "Prize Won",
|
||||||
|
"pending": "Pending",
|
||||||
|
"drawn": "Drawn",
|
||||||
|
"won": "Won",
|
||||||
|
"lost": "Lost",
|
||||||
|
"noDrawHistory": "No draw history yet",
|
||||||
|
"buyLotteryTicket": "Buy Lottery Ticket",
|
||||||
|
"selectNumbers": "Select Numbers",
|
||||||
|
"select5UniqueNumbers": "Select 5 unique numbers",
|
||||||
|
"selectSpecialNumber": "Select Special Number",
|
||||||
|
"selectMultiplier": "Select Multiplier",
|
||||||
|
"baseCost": "Base Cost",
|
||||||
|
"totalCost": "Total Cost",
|
||||||
|
"prizeStructure": "Prize Structure",
|
||||||
|
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
|
||||||
|
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
|
||||||
|
"winningNumbers": "Winning Numbers",
|
||||||
|
"specialNumber": "Special Number",
|
||||||
|
"totalTickets": "Total Tickets",
|
||||||
|
"totalWinners": "Total Winners",
|
||||||
|
"prizePool": "Prize Pool",
|
||||||
|
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
|
||||||
|
"purchase": "Purchase",
|
||||||
|
"multiplierLabel": "Multiplier",
|
||||||
|
"specialOnly": "Special Only",
|
||||||
|
"matches": "Matches",
|
||||||
|
"thoughtDefaultTopic": "Reflection",
|
||||||
|
"thoughtAiName": "SN-chan",
|
||||||
|
"thoughtUserName": "You",
|
||||||
|
"thoughtStreamingHint": "Sn-chan is thinking...",
|
||||||
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
|
"thoughtFunctionCall": "Use {}",
|
||||||
|
"aiThought": "AI Thought",
|
||||||
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
|
"activities": "Activities",
|
||||||
|
"presenceTypeGaming": "Playing",
|
||||||
|
"presenceTypeMusic": "Listening to Music",
|
||||||
|
"presenceTypeWorkout": "Working out",
|
||||||
|
"articleCompose": "Compose Article",
|
||||||
|
"backToHub": "Back to Hub",
|
||||||
|
"advancedFilters": "Advanced Filters",
|
||||||
|
"searchPosts": "Search Posts",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"fromDate": "From Date",
|
||||||
|
"toDate": "To Date",
|
||||||
|
"popularity": "Popularity",
|
||||||
|
"descendingOrder": "Descending Order",
|
||||||
|
"selectDate": "Select Date",
|
||||||
|
"pinnedPosts": "Pinned Posts",
|
||||||
|
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||||
|
"publicationSites": "Publication Sites",
|
||||||
|
"uploadTasks": "Upload Tasks",
|
||||||
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "{} responded",
|
||||||
|
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||||
|
"more": "More",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||||
|
"discard": "Discard",
|
||||||
|
"fund": "Fund",
|
||||||
|
"fundsRecent": "Recent Funds",
|
||||||
|
"fundCreateNew": "Create New",
|
||||||
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
|
"amountOfSplits": "Amount of Splits",
|
||||||
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share",
|
||||||
|
"affiliationSpell": "Affiliation Spell",
|
||||||
|
"affiliationSpellHint": "If you have an affiliation spell, enter it here.",
|
||||||
|
"friendsOnline": "Friends Online",
|
||||||
|
"createAccountAlmostThere": "Almost There",
|
||||||
|
"createAccountAlmostThereHint": "You're one step away from joining the Solar Network! Please solve the captcha puzzle shows next.",
|
||||||
|
"createAccountNotice": "Things you need to know before you create an account:",
|
||||||
|
"createAccountConfirmEmail": "After your account being created, you need go to your email inbox to active your account to get permission to use all features.",
|
||||||
|
"createAccountNoAltAccounts": "Multiple or alternative accounts are banned from the Solar Network, that will violates our terms of services.",
|
||||||
|
"createAccountAgreeTerms": "I've read these terms and agree to the terms of service.",
|
||||||
|
"createAccountProfile": "Create your profile",
|
||||||
|
"createAccountToS": "Review Terms & Conditions",
|
||||||
|
"updateYourProfileDescription": "Adjust how you looks on the Solar Network.",
|
||||||
|
"realmsDescription": "Manage realms you've joined.",
|
||||||
|
"exploreDescription": "Explore contents on the Solar Network.",
|
||||||
|
"accountDescription": "Information about your account.",
|
||||||
|
"chatDescription": "Group Chats and Direct Messages",
|
||||||
|
"connectionServerDown": "Unable to Connect",
|
||||||
|
"appSettingsDescription": "Customize your app.",
|
||||||
|
"accountSettingsDescription": "Manage your preferences on the Solar Network.",
|
||||||
|
"walletDescription": "Your source point wallet.",
|
||||||
|
"relationshipsDescription": "Friends and connections.",
|
||||||
|
"notificationsDescription": "See what's happended related to you recently.",
|
||||||
|
"settingsFestivalFeatures": "Festival Limited Features",
|
||||||
|
"categoriesAndTags": "Categories & Tags",
|
||||||
|
"webArticlesStandDescription": "Explore external sites articles.",
|
||||||
|
"aboutDescription": "Learn more about the Solar Network.",
|
||||||
|
"abuseReportsDescription": "View and manage abuse reports.",
|
||||||
|
"stickerMarketplaceDescription": "Browse and add sticker packs from the Solar Network marketplace.",
|
||||||
|
"webFeedsDescription": "Browse and subscribe to web feeds from the Solar Network.",
|
||||||
|
"discoverRealmsDescription": "Discover new realms and join them.",
|
||||||
|
"postShuffleDescription": "Shuffle posts to see the posts randomly.",
|
||||||
|
"levelingDescription": "See your leveling progress and history.",
|
||||||
|
"notableDayToday": "{} is today!",
|
||||||
|
"authSessionLogout": "Logout Session",
|
||||||
|
"authSessionLogoutHint": "Are you sure you want to logout this session? This will terminate this specific login session.",
|
||||||
|
"filesDescription": "Manage your files on the Solar Network Drive.",
|
||||||
|
"postComposeDescription": "Compose a new post",
|
||||||
|
"searchPostsDescription": "Search posts by title, content, or else.",
|
||||||
|
"accountActivationAlert": "Activate your account",
|
||||||
|
"accountActivationAlertHint": "Unactivated account may leads to various of permission issues, activate your account by clicking the link we sent to your email inbox.",
|
||||||
|
"accountActivationResendHint": "Didn't see it? Try click the button below to resend one. If you need to update your email while your account was unactivated, feel free to contact our customer service.",
|
||||||
|
"accountActivationResend": "Resend",
|
||||||
|
"ipAddress": "IP Address",
|
||||||
|
"noFurtherData": "No further data",
|
||||||
|
"searchAnything": "Search Anything...",
|
||||||
|
"tapToViewAllNotifications": "Tap to view all notifications",
|
||||||
|
"mostRecent": "Most Recent",
|
||||||
|
"noNotificationsYet": "No notifications yet",
|
||||||
|
"recentChats": "Recent Chats",
|
||||||
|
"noFeaturedPostsAvailable": "No featured posts available",
|
||||||
|
"searchChatsAndPages": "Search chats and pages...",
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"dashboardDescription": "All your data in one place.",
|
||||||
|
"postTagsCategories": "Post Tags and Categories",
|
||||||
|
"postTagsCategoriesDescription": "Browse posts by category and tags.",
|
||||||
|
"debugLogs": "Debug Logs",
|
||||||
|
"debugLogsDescription": "View debug logs for troubleshooting.",
|
||||||
|
"pinChatRoom": "Pin Chat Room",
|
||||||
|
"pinChatRoomDescription": "Pin this chat room to the top.",
|
||||||
|
"chatRoomPinned": "Chat room pinned successfully.",
|
||||||
|
"chatRoomUnpinned": "Chat room unpinned successfully.",
|
||||||
|
"pinnedChatRoom": "Pinned Rooms",
|
||||||
|
"settingsGroupedChatList": "Grouped Chat List",
|
||||||
|
"settingsNotifyWithHaptic": "Notification with Haptic Feedback",
|
||||||
|
"settingsDashSearchEngine": "Search Engine for web",
|
||||||
|
"settingsDashSearchEngineHelper": "Use %s as the placeholder for the query.",
|
||||||
|
"settingsDefaultScreen": "Default Screen",
|
||||||
|
"notableDayChristmas": "Christmas",
|
||||||
|
"notableDayNewYear": "New Year",
|
||||||
|
"noSubscription": "No subscriptions",
|
||||||
|
"embedLinks": {
|
||||||
|
"one": "{} link",
|
||||||
|
"other": "{} links"
|
||||||
|
},
|
||||||
|
"searchFediverse": "Search Fediverse",
|
||||||
|
"searchFediverseHint": "Search by address, e.g. {}",
|
||||||
|
"searchFediverseEmpty": "Search for users on other ActivityPub instances",
|
||||||
|
"searchFediverseNoResults": "No users found for this search",
|
||||||
|
"fediverseUsers": "Fediverse Users",
|
||||||
|
"following": "Following",
|
||||||
|
"followers": "Followers",
|
||||||
|
"follow": "Follow",
|
||||||
|
"unfollow": "Unfollow",
|
||||||
|
"followedUser": "Followed @{}",
|
||||||
|
"unfollowedUser": "Unfollowed @{}",
|
||||||
|
"followingEmpty": "You're not following anyone yet",
|
||||||
|
"followersEmpty": "No followers yet",
|
||||||
|
"followingEmptyHint": "Start by searching for users or explore other instances",
|
||||||
|
"fediversePost": "Fediverse Post",
|
||||||
|
"fediversePostDescribe": "Post from the Fediverse Network",
|
||||||
|
"settingsShowFediverseContent": "Show Fediverse Content"
|
||||||
|
}
|
||||||
@@ -158,14 +158,12 @@
|
|||||||
"checkIn": "Check In",
|
"checkIn": "Check In",
|
||||||
"checkInNone": "Not checked-in yet",
|
"checkInNone": "Not checked-in yet",
|
||||||
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
||||||
"checkInResultLevel0": "Wrost Luck",
|
"checkInResultLevel0": "Worst Luck",
|
||||||
"checkInResultLevel1": "Bad Luck",
|
"checkInResultLevel1": "Bad Luck",
|
||||||
"checkInResultLevel2": "A Normal Day",
|
"checkInResultLevel2": "A Normal Day",
|
||||||
"checkInResultLevel3": "Good Luck",
|
"checkInResultLevel3": "Good Luck",
|
||||||
"checkInResultLevel4": "Best Luck",
|
"checkInResultLevel4": "Best Luck",
|
||||||
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
||||||
"eventCalander": "Event Calander",
|
|
||||||
"eventCalanderEmpty": "No events on that day.",
|
|
||||||
"fortuneGraph": "Fortune Trend",
|
"fortuneGraph": "Fortune Trend",
|
||||||
"noFortuneData": "No fortune data available for this month.",
|
"noFortuneData": "No fortune data available for this month.",
|
||||||
"creatorHub": "Creator Hub",
|
"creatorHub": "Creator Hub",
|
||||||
@@ -316,7 +314,6 @@
|
|||||||
"settingsAutoTranslate": "Auto Translate",
|
"settingsAutoTranslate": "Auto Translate",
|
||||||
"settingsHideBottomNav": "Hide Bottom Navigation",
|
"settingsHideBottomNav": "Hide Bottom Navigation",
|
||||||
"settingsSoundEffects": "Sound Effects",
|
"settingsSoundEffects": "Sound Effects",
|
||||||
"settingsAprilFoolFeatures": "April Fool Features",
|
|
||||||
"settingsEnterToSend": "Enter to Send",
|
"settingsEnterToSend": "Enter to Send",
|
||||||
"settingsTransparentAppBar": "Transparent App Bar",
|
"settingsTransparentAppBar": "Transparent App Bar",
|
||||||
"settingsCustomFonts": "Custom Fonts",
|
"settingsCustomFonts": "Custom Fonts",
|
||||||
@@ -684,9 +681,9 @@
|
|||||||
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
|
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
|
||||||
"postVisibility": "Post Visibility",
|
"postVisibility": "Post Visibility",
|
||||||
"currentMembershipMember": "A member of Stellar Program · {}",
|
"currentMembershipMember": "A member of Stellar Program · {}",
|
||||||
"membershipPriceStellar": "1200 NSP per month, level 3+ required",
|
"membershipPriceStellar": "1200 NSP per month, level 20+ required",
|
||||||
"membershipPriceNova": "2400 NSP per month, level 6+ required",
|
"membershipPriceNova": "2400 NSP per month, level 40+ required",
|
||||||
"membershipPriceSupernova": "3600 NSP per month, level 9+ required",
|
"membershipPriceSupernova": "3600 NSP per month, level 60+ required",
|
||||||
"sharePostPhoto": "Share Post as Photo",
|
"sharePostPhoto": "Share Post as Photo",
|
||||||
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
|
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
|
||||||
"abuseReports": "Abuse Reports",
|
"abuseReports": "Abuse Reports",
|
||||||
@@ -707,7 +704,7 @@
|
|||||||
"aboutScreenDeveloperSectionTitle": "Developer",
|
"aboutScreenDeveloperSectionTitle": "Developer",
|
||||||
"aboutScreenContactUsTitle": "Contact Us",
|
"aboutScreenContactUsTitle": "Contact Us",
|
||||||
"aboutScreenLicenseTitle": "License",
|
"aboutScreenLicenseTitle": "License",
|
||||||
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
|
"aboutScreenLicenseContent": "AGPLv3",
|
||||||
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
|
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
|
||||||
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
|
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
|
||||||
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
|
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
|
||||||
@@ -752,21 +749,6 @@
|
|||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"markAsSensitive": "Mark as Sensitive",
|
"markAsSensitive": "Mark as Sensitive",
|
||||||
"fileName": "File name",
|
"fileName": "File name",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "Language",
|
|
||||||
"sexualContent": "Sexual Content",
|
|
||||||
"violence": "Violence",
|
|
||||||
"profanity": "Profanity",
|
|
||||||
"hateSpeech": "Hate Speech",
|
|
||||||
"racism": "Racism",
|
|
||||||
"adultContent": "Adult Content",
|
|
||||||
"drugAbuse": "Drug Abuse",
|
|
||||||
"alcoholAbuse": "Alcohol Abuse",
|
|
||||||
"gambling": "Gambling",
|
|
||||||
"selfHarm": "Self-harm",
|
|
||||||
"childAbuse": "Child Abuse",
|
|
||||||
"other": "Other"
|
|
||||||
},
|
|
||||||
"poll": "Poll",
|
"poll": "Poll",
|
||||||
"pollsRecent": "Recent Polls",
|
"pollsRecent": "Recent Polls",
|
||||||
"pollCreateNew": "Create New",
|
"pollCreateNew": "Create New",
|
||||||
@@ -1075,5 +1057,490 @@
|
|||||||
"deleteRecycledFiles": "Delete Recycled Files",
|
"deleteRecycledFiles": "Delete Recycled Files",
|
||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
"upload": "Upload"
|
"upload": "Upload",
|
||||||
|
"deleteMessage": "Delete Message",
|
||||||
|
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
|
||||||
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactions": "Custom Reactions",
|
||||||
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
|
"reactionAttitude": "Reaction Attitude",
|
||||||
|
"addReaction": "Add Reaction",
|
||||||
|
"eventCalendar": "Event Calendar",
|
||||||
|
"eventCalendarEmpty": "No events on that day.",
|
||||||
|
"walletStats": "Wallet Statistics",
|
||||||
|
"totalTransactions": "Total Transactions",
|
||||||
|
"totalOrders": "Total Orders",
|
||||||
|
"totalIncome": "Total Income",
|
||||||
|
"totalOutgoing": "Total Outgoing",
|
||||||
|
"netBalance": "Net Balance",
|
||||||
|
"messageUpdateLinks": "Server generated links previews",
|
||||||
|
"messageUpdateEdited": "Edited a message",
|
||||||
|
"settingsCardBackgroundOpacity": "Card Background Opacity",
|
||||||
|
"settingsThemeMode": "Theme Mode",
|
||||||
|
"settingsThemeModeSystem": "System",
|
||||||
|
"settingsThemeModeLight": "Light",
|
||||||
|
"settingsThemeModeDark": "Dark",
|
||||||
|
"enterPin": "Enter your PIN code",
|
||||||
|
"chatReplyingTo": "Replying to {}",
|
||||||
|
"chatForwarding": "Forwarding message",
|
||||||
|
"chatEditing": "Editing message",
|
||||||
|
"chatNoContent": "No content",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "Language",
|
||||||
|
"sexualContent": "Sexual Content",
|
||||||
|
"violence": "Violence",
|
||||||
|
"profanity": "Profanity",
|
||||||
|
"hateSpeech": "Hate Speech",
|
||||||
|
"racism": "Racism",
|
||||||
|
"adultContent": "Adult Content",
|
||||||
|
"drugAbuse": "Drug Abuse",
|
||||||
|
"alcoholAbuse": "Alcohol Abuse",
|
||||||
|
"gambling": "Gambling",
|
||||||
|
"selfHarm": "Self-harm",
|
||||||
|
"childAbuse": "Child Abuse",
|
||||||
|
"other": "Other"
|
||||||
|
},
|
||||||
|
"Searching...": "Searching...",
|
||||||
|
"searchError": "Search failed. Please try again.",
|
||||||
|
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
||||||
|
"settingsWindowOpacity": "Window Opacity",
|
||||||
|
"messageContent": "Message Content",
|
||||||
|
"updateAvailable": "Update available",
|
||||||
|
"noChangelogProvided": "No changelog provided.",
|
||||||
|
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||||
|
"installUpdate": "Install update",
|
||||||
|
"openReleasePage": "Open release page",
|
||||||
|
"postCompose": "Compose Post",
|
||||||
|
"postPublish": "Publish Post",
|
||||||
|
"restoreDraftTitle": "Restore Draft",
|
||||||
|
"restoreDraftMessage": "A draft was found. Do you want to restore it?",
|
||||||
|
"draft": "Draft",
|
||||||
|
"purchaseGift": "Purchase Gift",
|
||||||
|
"selectRecipient": "Select Recipient",
|
||||||
|
"changeRecipient": "Change Recipient",
|
||||||
|
"addMessage": "Add Message",
|
||||||
|
"skipRecipient": "Skip Recipient",
|
||||||
|
"giftSubscriptions": "Gift Subscriptions",
|
||||||
|
"purchaseAGift": "Purchase a Gift",
|
||||||
|
"redeemAGift": "Redeem a Gift",
|
||||||
|
"giftHistory": "Gift History",
|
||||||
|
"sentGifts": "Sent Gifts",
|
||||||
|
"receivedGifts": "Received Gifts",
|
||||||
|
"noSentGifts": "No sent gifts",
|
||||||
|
"noReceivedGifts": "No received gifts",
|
||||||
|
"stellarGift": "Stellar Gift",
|
||||||
|
"novaGift": "Nova Gift",
|
||||||
|
"supernovaGift": "Supernova Gift",
|
||||||
|
"sameAsMembership": "Same as membership",
|
||||||
|
"enterGiftCodeToRedeem": "Enter gift code to redeem",
|
||||||
|
"enterGiftCode": "Enter gift code",
|
||||||
|
"giftPurchased": "Gift Purchased!",
|
||||||
|
"shareCodeWithRecipient": "Share this code with the recipient to redeem the gift.",
|
||||||
|
"openGiftAnyoneCanRedeem": "This is an open gift that anyone can redeem.",
|
||||||
|
"ok": "OK",
|
||||||
|
"selectedRecipient": "Selected recipient",
|
||||||
|
"noRecipientSelected": "No recipient selected",
|
||||||
|
"thisWillBeAnOpenGift": "This will be an open gift",
|
||||||
|
"personalMessage": "Personal Message",
|
||||||
|
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
||||||
|
"giftStatusCreated": "Created",
|
||||||
|
"giftStatusSent": "Sent",
|
||||||
|
"giftStatusRedeemed": "Redeemed",
|
||||||
|
"giftStatusCancelled": "Cancelled",
|
||||||
|
"giftStatusExpired": "Expired",
|
||||||
|
"giftStatusUnknown": "Unknown",
|
||||||
|
"giftCodeCopiedToClipboard": "Gift code copied to clipboard",
|
||||||
|
"codeLabel": "Code: ",
|
||||||
|
"subscriptionLabel": "Subscription: ",
|
||||||
|
"toLabel": "To: ",
|
||||||
|
"fromLabel": "From: ",
|
||||||
|
"messageLabel": "Message: ",
|
||||||
|
"giftRedeemed": "Gift Redeemed!",
|
||||||
|
"giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.",
|
||||||
|
"cancelGift": "Cancel Gift",
|
||||||
|
"cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.",
|
||||||
|
"giftCancelledSuccessfully": "Gift cancelled successfully",
|
||||||
|
"createFund": "Create Fund",
|
||||||
|
"fundAmount": "Fund Amount",
|
||||||
|
"enterAmount": "Enter Amount",
|
||||||
|
"selectCurrency": "Select Currency",
|
||||||
|
"splitType": "Split Type",
|
||||||
|
"evenSplit": "Even Split",
|
||||||
|
"equalAmountEach": "Equal amount for each recipient",
|
||||||
|
"randomSplit": "Random Split",
|
||||||
|
"randomAmountEach": "Random amount for each recipient",
|
||||||
|
"recipientCount": "Recipient Count",
|
||||||
|
"numberOfRecipients": "Number of Recipients",
|
||||||
|
"addPersonalMessageForRecipients": "Add a personal message for recipients",
|
||||||
|
"invalidAmount": "Invalid amount",
|
||||||
|
"invalidRecipientCount": "Invalid recipient count",
|
||||||
|
"fundOverview": "Fund Overview",
|
||||||
|
"totalFundsSent": "Total Funds Sent",
|
||||||
|
"totalFundsReceived": "Total Funds Received",
|
||||||
|
"transactions": "Transactions",
|
||||||
|
"myFunds": "My Funds",
|
||||||
|
"availableFunds": "Available Funds",
|
||||||
|
"fundStatusCreated": "Created",
|
||||||
|
"fundStatusPartial": "Partially Claimed",
|
||||||
|
"fundStatusCompleted": "Fully Claimed",
|
||||||
|
"fundStatusExpired": "Expired",
|
||||||
|
"fundStatusUnknown": "Unknown",
|
||||||
|
"recipients": "Recipients",
|
||||||
|
"fundClaimedSuccessfully": "Fund claimed successfully!",
|
||||||
|
"claim": "Claim",
|
||||||
|
"noFundsCreated": "No funds created yet",
|
||||||
|
"createYourFirstFund": "Create your first fund to get started",
|
||||||
|
"noAvailableFunds": "No available funds",
|
||||||
|
"fundsWillAppearHere": "Funds you can claim will appear here",
|
||||||
|
"fundCreatedSuccessfully": "Fund created successfully!",
|
||||||
|
"selectRecipients": "Select Recipients",
|
||||||
|
"noRecipientsSelected": "No recipients selected",
|
||||||
|
"selectRecipientsToSendFund": "Select recipients to send the fund to",
|
||||||
|
"addRecipient": "Add Recipient",
|
||||||
|
"addMoreRecipients": "Add More Recipients",
|
||||||
|
"transactionDetails": "Transaction Details",
|
||||||
|
"remarks": "Remarks",
|
||||||
|
"payer": "Payer",
|
||||||
|
"payee": "Payee",
|
||||||
|
"transactionType": "Transaction Type",
|
||||||
|
"transfer": "Transfer",
|
||||||
|
"payment": "Payment",
|
||||||
|
"systemWallet": "System Wallet",
|
||||||
|
"date": "Date",
|
||||||
|
"createTransfer": "Create Transfer",
|
||||||
|
"transferAmount": "Transfer Amount",
|
||||||
|
"selectPayee": "Select Payee",
|
||||||
|
"selectedPayee": "Selected Payee",
|
||||||
|
"noPayeeSelected": "No payee selected",
|
||||||
|
"selectPayeeToTransfer": "Select payee to transfer to",
|
||||||
|
"addRemark": "Add Remark",
|
||||||
|
"transferRemark": "Transfer Remark",
|
||||||
|
"addRemarkForTransfer": "Add remark for transfer",
|
||||||
|
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
|
||||||
|
"transferCreatedSuccessfully": "Transfer created successfully!",
|
||||||
|
"postUpdate": "Update",
|
||||||
|
"fileMetadata": "File Metadata",
|
||||||
|
"resend": "Resend",
|
||||||
|
"fileInfoTitle": "File Information",
|
||||||
|
"download": "Download",
|
||||||
|
"info": "Info",
|
||||||
|
"noStickers": "No Stickers",
|
||||||
|
"noStickersInPack": "This pack does not contains stickers",
|
||||||
|
"noStickerPacks": "No Sticker Packs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"spoiler": "Spoiler",
|
||||||
|
"activityHeatmap": "Activity Heatmap",
|
||||||
|
"custom": "Custom",
|
||||||
|
"usernameColor": "Username Color",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"plain": "Plain",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"colorValue": "Color Value",
|
||||||
|
"gradientDirection": "Gradient Direction",
|
||||||
|
"gradientDirectionToRight": "To Right",
|
||||||
|
"gradientDirectionToLeft": "To Left",
|
||||||
|
"gradientDirectionToBottom": "To Bottom",
|
||||||
|
"gradientDirectionToTop": "To Top",
|
||||||
|
"gradientDirectionToBottomRight": "To Bottom Right",
|
||||||
|
"gradientDirectionToBottomLeft": "To Bottom Left",
|
||||||
|
"gradientDirectionToTopRight": "To Top Right",
|
||||||
|
"gradientDirectionToTopLeft": "To Top Left",
|
||||||
|
"gradientColors": "Gradient Colors",
|
||||||
|
"color": "Color",
|
||||||
|
"addColor": "Add Color",
|
||||||
|
"availableWithYourPlan": "Available with your plan",
|
||||||
|
"upgradeRequired": "Upgrade required",
|
||||||
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "Time zone not found",
|
||||||
|
"awardPoints": "Awarded {} points",
|
||||||
|
"postFeaturedOn": "Post featured on {}",
|
||||||
|
"messageSentAt": "Sent at {}",
|
||||||
|
"myTickets": "My Tickets",
|
||||||
|
"drawHistory": "Draw History",
|
||||||
|
"lottery": "Lottery",
|
||||||
|
"noLotteryTickets": "No lottery tickets yet",
|
||||||
|
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
|
||||||
|
"buyTicket": "Buy Ticket",
|
||||||
|
"ticketNumbers": "Numbers: {}, Special: {}",
|
||||||
|
"cost": "Cost",
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"prizeWon": "Prize Won",
|
||||||
|
"pending": "Pending",
|
||||||
|
"drawn": "Drawn",
|
||||||
|
"won": "Won",
|
||||||
|
"lost": "Lost",
|
||||||
|
"noDrawHistory": "No draw history yet",
|
||||||
|
"buyLotteryTicket": "Buy Lottery Ticket",
|
||||||
|
"selectNumbers": "Select Numbers",
|
||||||
|
"select5UniqueNumbers": "Select 5 unique numbers",
|
||||||
|
"selectSpecialNumber": "Select Special Number",
|
||||||
|
"selectMultiplier": "Select Multiplier",
|
||||||
|
"baseCost": "Base Cost",
|
||||||
|
"totalCost": "Total Cost",
|
||||||
|
"prizeStructure": "Prize Structure",
|
||||||
|
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
|
||||||
|
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
|
||||||
|
"winningNumbers": "Winning Numbers",
|
||||||
|
"specialNumber": "Special Number",
|
||||||
|
"totalTickets": "Total Tickets",
|
||||||
|
"totalWinners": "Total Winners",
|
||||||
|
"prizePool": "Prize Pool",
|
||||||
|
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
|
||||||
|
"purchase": "Purchase",
|
||||||
|
"multiplierLabel": "Multiplier",
|
||||||
|
"specialOnly": "Special Only",
|
||||||
|
"matches": "Matches",
|
||||||
|
"thoughtDefaultTopic": "Reflection",
|
||||||
|
"thoughtAiName": "SN-chan",
|
||||||
|
"thoughtUserName": "You",
|
||||||
|
"thoughtStreamingHint": "Sn-chan is thinking...",
|
||||||
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
|
"thoughtFunctionCall": "Use {}",
|
||||||
|
"aiThought": "AI Thought",
|
||||||
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
|
"fabLocation": "FAB Location",
|
||||||
|
"activities": "Activities",
|
||||||
|
"presenceTypeGaming": "Playing",
|
||||||
|
"presenceTypeMusic": "Listening to Music",
|
||||||
|
"presenceTypeWorkout": "Working out",
|
||||||
|
"articleCompose": "Compose Article",
|
||||||
|
"backToHub": "Back to Hub",
|
||||||
|
"advancedFilters": "Advanced Filters",
|
||||||
|
"searchPosts": "Search Posts",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"fromDate": "From Date",
|
||||||
|
"toDate": "To Date",
|
||||||
|
"popularity": "Popularity",
|
||||||
|
"descendingOrder": "Descending Order",
|
||||||
|
"selectDate": "Select Date",
|
||||||
|
"pinnedPosts": "Pinned Posts",
|
||||||
|
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||||
|
"publicationSites": "Publication Sites",
|
||||||
|
"uploadTasks": "Upload Tasks",
|
||||||
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "{} responded",
|
||||||
|
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||||
|
"more": "More",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||||
|
"discard": "Discard",
|
||||||
|
"fund": "Fund",
|
||||||
|
"fundsRecent": "Recent Funds",
|
||||||
|
"fundCreateNew": "Create New",
|
||||||
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
|
"amountOfSplits": "Amount of Splits",
|
||||||
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share",
|
||||||
|
"affiliationSpell": "Affiliation Spell",
|
||||||
|
"affiliationSpellHint": "If you have an affiliation spell, enter it here.",
|
||||||
|
"friendsOnline": "Friends Online",
|
||||||
|
"createAccountAlmostThere": "Almost There",
|
||||||
|
"createAccountAlmostThereHint": "You're one step away from joining the Solar Network! Please solve the captcha puzzle shows next.",
|
||||||
|
"createAccountNotice": "Things you need to know before you create an account:",
|
||||||
|
"createAccountConfirmEmail": "After your account being created, you need go to your email inbox to active your account to get permission to use all features.",
|
||||||
|
"createAccountNoAltAccounts": "Multiple or alternative accounts are banned from the Solar Network, that will violates our terms of services.",
|
||||||
|
"createAccountAgreeTerms": "I've read these terms and agree to the terms of service.",
|
||||||
|
"createAccountProfile": "Create your profile",
|
||||||
|
"createAccountToS": "Review Terms & Conditions",
|
||||||
|
"updateYourProfileDescription": "Adjust how you looks on the Solar Network.",
|
||||||
|
"realmsDescription": "Manage realms you've joined.",
|
||||||
|
"exploreDescription": "Explore contents on the Solar Network.",
|
||||||
|
"accountDescription": "Information about your account.",
|
||||||
|
"chatDescription": "Group Chats and Direct Messages",
|
||||||
|
"connectionServerDown": "Unable to Connect",
|
||||||
|
"appSettingsDescription": "Customize your app.",
|
||||||
|
"accountSettingsDescription": "Manage your preferences on the Solar Network.",
|
||||||
|
"walletDescription": "Your source point wallet.",
|
||||||
|
"relationshipsDescription": "Friends and connections.",
|
||||||
|
"notificationsDescription": "See what's happended related to you recently.",
|
||||||
|
"settingsFestivalFeatures": "Festival Limited Features",
|
||||||
|
"categoriesAndTags": "Categories & Tags",
|
||||||
|
"webArticlesStandDescription": "Explore external sites articles.",
|
||||||
|
"aboutDescription": "Learn more about the Solar Network.",
|
||||||
|
"abuseReportsDescription": "View and manage abuse reports.",
|
||||||
|
"stickerMarketplaceDescription": "Browse and add sticker packs from the Solar Network marketplace.",
|
||||||
|
"webFeedsDescription": "Browse and subscribe to web feeds from the Solar Network.",
|
||||||
|
"discoverRealmsDescription": "Discover new realms and join them.",
|
||||||
|
"postShuffleDescription": "Shuffle posts to see the posts randomly.",
|
||||||
|
"levelingDescription": "See your leveling progress and history.",
|
||||||
|
"notableDayToday": "{} is today!",
|
||||||
|
"authSessionLogout": "Logout Session",
|
||||||
|
"authSessionLogoutHint": "Are you sure you want to logout this session? This will terminate this specific login session.",
|
||||||
|
"filesDescription": "Manage your files on the Solar Network Drive.",
|
||||||
|
"postComposeDescription": "Compose a new post",
|
||||||
|
"searchPostsDescription": "Search posts by title, content, or else.",
|
||||||
|
"accountActivationAlert": "Activate your account",
|
||||||
|
"accountActivationAlertHint": "Unactivated account may leads to various of permission issues, activate your account by clicking the link we sent to your email inbox.",
|
||||||
|
"accountActivationResendHint": "Didn't see it? Try click the button below to resend one. If you need to update your email while your account was unactivated, feel free to contact our customer service.",
|
||||||
|
"accountActivationResend": "Resend",
|
||||||
|
"ipAddress": "IP Address",
|
||||||
|
"noFurtherData": "No further data",
|
||||||
|
"searchAnything": "Search Anything...",
|
||||||
|
"tapToViewAllNotifications": "Tap to view all notifications",
|
||||||
|
"mostRecent": "Most Recent",
|
||||||
|
"noNotificationsYet": "No notifications yet",
|
||||||
|
"recentChats": "Recent Chats",
|
||||||
|
"noFeaturedPostsAvailable": "No featured posts available",
|
||||||
|
"searchChatsAndPages": "Search chats and pages...",
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"dashboardDescription": "All your data in one place.",
|
||||||
|
"postTagsCategories": "Post Tags and Categories",
|
||||||
|
"postTagsCategoriesDescription": "Browse posts by category and tags.",
|
||||||
|
"debugLogs": "Debug Logs",
|
||||||
|
"debugLogsDescription": "View debug logs for troubleshooting.",
|
||||||
|
"pinChatRoom": "Pin Chat Room",
|
||||||
|
"pinChatRoomDescription": "Pin this chat room to the top.",
|
||||||
|
"chatRoomPinned": "Chat room pinned successfully.",
|
||||||
|
"chatRoomUnpinned": "Chat room unpinned successfully.",
|
||||||
|
"pinnedChatRoom": "Pinned Rooms",
|
||||||
|
"settingsGroupedChatList": "Grouped Chat List",
|
||||||
|
"settingsNotifyWithHaptic": "Notification with Haptic Feedback",
|
||||||
|
"settingsDashSearchEngine": "Search Engine for web",
|
||||||
|
"settingsDashSearchEngineHelper": "Use %s as the placeholder for the query.",
|
||||||
|
"settingsDefaultScreen": "Default Screen",
|
||||||
|
"notableDayChristmas": "Christmas",
|
||||||
|
"notableDayNewYear": "New Year"
|
||||||
}
|
}
|
||||||
@@ -158,14 +158,12 @@
|
|||||||
"checkIn": "Check In",
|
"checkIn": "Check In",
|
||||||
"checkInNone": "Not checked-in yet",
|
"checkInNone": "Not checked-in yet",
|
||||||
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
||||||
"checkInResultLevel0": "Wrost Luck",
|
"checkInResultLevel0": "Worst Luck",
|
||||||
"checkInResultLevel1": "Bad Luck",
|
"checkInResultLevel1": "Bad Luck",
|
||||||
"checkInResultLevel2": "A Normal Day",
|
"checkInResultLevel2": "A Normal Day",
|
||||||
"checkInResultLevel3": "Good Luck",
|
"checkInResultLevel3": "Good Luck",
|
||||||
"checkInResultLevel4": "Best Luck",
|
"checkInResultLevel4": "Best Luck",
|
||||||
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
||||||
"eventCalander": "Event Calander",
|
|
||||||
"eventCalanderEmpty": "No events on that day.",
|
|
||||||
"fortuneGraph": "Fortune Trend",
|
"fortuneGraph": "Fortune Trend",
|
||||||
"noFortuneData": "No fortune data available for this month.",
|
"noFortuneData": "No fortune data available for this month.",
|
||||||
"creatorHub": "Creator Hub",
|
"creatorHub": "Creator Hub",
|
||||||
@@ -316,7 +314,6 @@
|
|||||||
"settingsAutoTranslate": "Auto Translate",
|
"settingsAutoTranslate": "Auto Translate",
|
||||||
"settingsHideBottomNav": "Hide Bottom Navigation",
|
"settingsHideBottomNav": "Hide Bottom Navigation",
|
||||||
"settingsSoundEffects": "Sound Effects",
|
"settingsSoundEffects": "Sound Effects",
|
||||||
"settingsAprilFoolFeatures": "April Fool Features",
|
|
||||||
"settingsEnterToSend": "Enter to Send",
|
"settingsEnterToSend": "Enter to Send",
|
||||||
"settingsTransparentAppBar": "Transparent App Bar",
|
"settingsTransparentAppBar": "Transparent App Bar",
|
||||||
"settingsCustomFonts": "Custom Fonts",
|
"settingsCustomFonts": "Custom Fonts",
|
||||||
@@ -684,9 +681,9 @@
|
|||||||
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
|
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
|
||||||
"postVisibility": "Post Visibility",
|
"postVisibility": "Post Visibility",
|
||||||
"currentMembershipMember": "A member of Stellar Program · {}",
|
"currentMembershipMember": "A member of Stellar Program · {}",
|
||||||
"membershipPriceStellar": "1200 NSP per month, level 3+ required",
|
"membershipPriceStellar": "1200 NSP per month, level 20+ required",
|
||||||
"membershipPriceNova": "2400 NSP per month, level 6+ required",
|
"membershipPriceNova": "2400 NSP per month, level 40+ required",
|
||||||
"membershipPriceSupernova": "3600 NSP per month, level 9+ required",
|
"membershipPriceSupernova": "3600 NSP per month, level 60+ required",
|
||||||
"sharePostPhoto": "Share Post as Photo",
|
"sharePostPhoto": "Share Post as Photo",
|
||||||
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
|
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
|
||||||
"abuseReports": "Abuse Reports",
|
"abuseReports": "Abuse Reports",
|
||||||
@@ -707,7 +704,7 @@
|
|||||||
"aboutScreenDeveloperSectionTitle": "Developer",
|
"aboutScreenDeveloperSectionTitle": "Developer",
|
||||||
"aboutScreenContactUsTitle": "Contact Us",
|
"aboutScreenContactUsTitle": "Contact Us",
|
||||||
"aboutScreenLicenseTitle": "License",
|
"aboutScreenLicenseTitle": "License",
|
||||||
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
|
"aboutScreenLicenseContent": "AGPLv3",
|
||||||
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
|
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
|
||||||
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
|
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
|
||||||
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
|
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
|
||||||
@@ -752,21 +749,6 @@
|
|||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"markAsSensitive": "Mark as Sensitive",
|
"markAsSensitive": "Mark as Sensitive",
|
||||||
"fileName": "File name",
|
"fileName": "File name",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "Language",
|
|
||||||
"sexualContent": "Sexual Content",
|
|
||||||
"violence": "Violence",
|
|
||||||
"profanity": "Profanity",
|
|
||||||
"hateSpeech": "Hate Speech",
|
|
||||||
"racism": "Racism",
|
|
||||||
"adultContent": "Adult Content",
|
|
||||||
"drugAbuse": "Drug Abuse",
|
|
||||||
"alcoholAbuse": "Alcohol Abuse",
|
|
||||||
"gambling": "Gambling",
|
|
||||||
"selfHarm": "Self-harm",
|
|
||||||
"childAbuse": "Child Abuse",
|
|
||||||
"other": "Other"
|
|
||||||
},
|
|
||||||
"poll": "Poll",
|
"poll": "Poll",
|
||||||
"pollsRecent": "Recent Polls",
|
"pollsRecent": "Recent Polls",
|
||||||
"pollCreateNew": "Create New",
|
"pollCreateNew": "Create New",
|
||||||
@@ -1075,5 +1057,490 @@
|
|||||||
"deleteRecycledFiles": "Delete Recycled Files",
|
"deleteRecycledFiles": "Delete Recycled Files",
|
||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
"upload": "Upload"
|
"upload": "Upload",
|
||||||
|
"deleteMessage": "Delete Message",
|
||||||
|
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
|
||||||
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactions": "Custom Reactions",
|
||||||
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
|
"reactionAttitude": "Reaction Attitude",
|
||||||
|
"addReaction": "Add Reaction",
|
||||||
|
"eventCalendar": "Event Calendar",
|
||||||
|
"eventCalendarEmpty": "No events on that day.",
|
||||||
|
"walletStats": "Wallet Statistics",
|
||||||
|
"totalTransactions": "Total Transactions",
|
||||||
|
"totalOrders": "Total Orders",
|
||||||
|
"totalIncome": "Total Income",
|
||||||
|
"totalOutgoing": "Total Outgoing",
|
||||||
|
"netBalance": "Net Balance",
|
||||||
|
"messageUpdateLinks": "Server generated links previews",
|
||||||
|
"messageUpdateEdited": "Edited a message",
|
||||||
|
"settingsCardBackgroundOpacity": "Card Background Opacity",
|
||||||
|
"settingsThemeMode": "Theme Mode",
|
||||||
|
"settingsThemeModeSystem": "System",
|
||||||
|
"settingsThemeModeLight": "Light",
|
||||||
|
"settingsThemeModeDark": "Dark",
|
||||||
|
"enterPin": "Enter your PIN code",
|
||||||
|
"chatReplyingTo": "Replying to {}",
|
||||||
|
"chatForwarding": "Forwarding message",
|
||||||
|
"chatEditing": "Editing message",
|
||||||
|
"chatNoContent": "No content",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "Language",
|
||||||
|
"sexualContent": "Sexual Content",
|
||||||
|
"violence": "Violence",
|
||||||
|
"profanity": "Profanity",
|
||||||
|
"hateSpeech": "Hate Speech",
|
||||||
|
"racism": "Racism",
|
||||||
|
"adultContent": "Adult Content",
|
||||||
|
"drugAbuse": "Drug Abuse",
|
||||||
|
"alcoholAbuse": "Alcohol Abuse",
|
||||||
|
"gambling": "Gambling",
|
||||||
|
"selfHarm": "Self-harm",
|
||||||
|
"childAbuse": "Child Abuse",
|
||||||
|
"other": "Other"
|
||||||
|
},
|
||||||
|
"Searching...": "Searching...",
|
||||||
|
"searchError": "Search failed. Please try again.",
|
||||||
|
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
||||||
|
"settingsWindowOpacity": "Window Opacity",
|
||||||
|
"messageContent": "Message Content",
|
||||||
|
"updateAvailable": "Update available",
|
||||||
|
"noChangelogProvided": "No changelog provided.",
|
||||||
|
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||||
|
"installUpdate": "Install update",
|
||||||
|
"openReleasePage": "Open release page",
|
||||||
|
"postCompose": "Compose Post",
|
||||||
|
"postPublish": "Publish Post",
|
||||||
|
"restoreDraftTitle": "Restore Draft",
|
||||||
|
"restoreDraftMessage": "A draft was found. Do you want to restore it?",
|
||||||
|
"draft": "Draft",
|
||||||
|
"purchaseGift": "Purchase Gift",
|
||||||
|
"selectRecipient": "Select Recipient",
|
||||||
|
"changeRecipient": "Change Recipient",
|
||||||
|
"addMessage": "Add Message",
|
||||||
|
"skipRecipient": "Skip Recipient",
|
||||||
|
"giftSubscriptions": "Gift Subscriptions",
|
||||||
|
"purchaseAGift": "Purchase a Gift",
|
||||||
|
"redeemAGift": "Redeem a Gift",
|
||||||
|
"giftHistory": "Gift History",
|
||||||
|
"sentGifts": "Sent Gifts",
|
||||||
|
"receivedGifts": "Received Gifts",
|
||||||
|
"noSentGifts": "No sent gifts",
|
||||||
|
"noReceivedGifts": "No received gifts",
|
||||||
|
"stellarGift": "Stellar Gift",
|
||||||
|
"novaGift": "Nova Gift",
|
||||||
|
"supernovaGift": "Supernova Gift",
|
||||||
|
"sameAsMembership": "Same as membership",
|
||||||
|
"enterGiftCodeToRedeem": "Enter gift code to redeem",
|
||||||
|
"enterGiftCode": "Enter gift code",
|
||||||
|
"giftPurchased": "Gift Purchased!",
|
||||||
|
"shareCodeWithRecipient": "Share this code with the recipient to redeem the gift.",
|
||||||
|
"openGiftAnyoneCanRedeem": "This is an open gift that anyone can redeem.",
|
||||||
|
"ok": "OK",
|
||||||
|
"selectedRecipient": "Selected recipient",
|
||||||
|
"noRecipientSelected": "No recipient selected",
|
||||||
|
"thisWillBeAnOpenGift": "This will be an open gift",
|
||||||
|
"personalMessage": "Personal Message",
|
||||||
|
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
||||||
|
"giftStatusCreated": "Created",
|
||||||
|
"giftStatusSent": "Sent",
|
||||||
|
"giftStatusRedeemed": "Redeemed",
|
||||||
|
"giftStatusCancelled": "Cancelled",
|
||||||
|
"giftStatusExpired": "Expired",
|
||||||
|
"giftStatusUnknown": "Unknown",
|
||||||
|
"giftCodeCopiedToClipboard": "Gift code copied to clipboard",
|
||||||
|
"codeLabel": "Code: ",
|
||||||
|
"subscriptionLabel": "Subscription: ",
|
||||||
|
"toLabel": "To: ",
|
||||||
|
"fromLabel": "From: ",
|
||||||
|
"messageLabel": "Message: ",
|
||||||
|
"giftRedeemed": "Gift Redeemed!",
|
||||||
|
"giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.",
|
||||||
|
"cancelGift": "Cancel Gift",
|
||||||
|
"cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.",
|
||||||
|
"giftCancelledSuccessfully": "Gift cancelled successfully",
|
||||||
|
"createFund": "Create Fund",
|
||||||
|
"fundAmount": "Fund Amount",
|
||||||
|
"enterAmount": "Enter Amount",
|
||||||
|
"selectCurrency": "Select Currency",
|
||||||
|
"splitType": "Split Type",
|
||||||
|
"evenSplit": "Even Split",
|
||||||
|
"equalAmountEach": "Equal amount for each recipient",
|
||||||
|
"randomSplit": "Random Split",
|
||||||
|
"randomAmountEach": "Random amount for each recipient",
|
||||||
|
"recipientCount": "Recipient Count",
|
||||||
|
"numberOfRecipients": "Number of Recipients",
|
||||||
|
"addPersonalMessageForRecipients": "Add a personal message for recipients",
|
||||||
|
"invalidAmount": "Invalid amount",
|
||||||
|
"invalidRecipientCount": "Invalid recipient count",
|
||||||
|
"fundOverview": "Fund Overview",
|
||||||
|
"totalFundsSent": "Total Funds Sent",
|
||||||
|
"totalFundsReceived": "Total Funds Received",
|
||||||
|
"transactions": "Transactions",
|
||||||
|
"myFunds": "My Funds",
|
||||||
|
"availableFunds": "Available Funds",
|
||||||
|
"fundStatusCreated": "Created",
|
||||||
|
"fundStatusPartial": "Partially Claimed",
|
||||||
|
"fundStatusCompleted": "Fully Claimed",
|
||||||
|
"fundStatusExpired": "Expired",
|
||||||
|
"fundStatusUnknown": "Unknown",
|
||||||
|
"recipients": "Recipients",
|
||||||
|
"fundClaimedSuccessfully": "Fund claimed successfully!",
|
||||||
|
"claim": "Claim",
|
||||||
|
"noFundsCreated": "No funds created yet",
|
||||||
|
"createYourFirstFund": "Create your first fund to get started",
|
||||||
|
"noAvailableFunds": "No available funds",
|
||||||
|
"fundsWillAppearHere": "Funds you can claim will appear here",
|
||||||
|
"fundCreatedSuccessfully": "Fund created successfully!",
|
||||||
|
"selectRecipients": "Select Recipients",
|
||||||
|
"noRecipientsSelected": "No recipients selected",
|
||||||
|
"selectRecipientsToSendFund": "Select recipients to send the fund to",
|
||||||
|
"addRecipient": "Add Recipient",
|
||||||
|
"addMoreRecipients": "Add More Recipients",
|
||||||
|
"transactionDetails": "Transaction Details",
|
||||||
|
"remarks": "Remarks",
|
||||||
|
"payer": "Payer",
|
||||||
|
"payee": "Payee",
|
||||||
|
"transactionType": "Transaction Type",
|
||||||
|
"transfer": "Transfer",
|
||||||
|
"payment": "Payment",
|
||||||
|
"systemWallet": "System Wallet",
|
||||||
|
"date": "Date",
|
||||||
|
"createTransfer": "Create Transfer",
|
||||||
|
"transferAmount": "Transfer Amount",
|
||||||
|
"selectPayee": "Select Payee",
|
||||||
|
"selectedPayee": "Selected Payee",
|
||||||
|
"noPayeeSelected": "No payee selected",
|
||||||
|
"selectPayeeToTransfer": "Select payee to transfer to",
|
||||||
|
"addRemark": "Add Remark",
|
||||||
|
"transferRemark": "Transfer Remark",
|
||||||
|
"addRemarkForTransfer": "Add remark for transfer",
|
||||||
|
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
|
||||||
|
"transferCreatedSuccessfully": "Transfer created successfully!",
|
||||||
|
"postUpdate": "Update",
|
||||||
|
"fileMetadata": "File Metadata",
|
||||||
|
"resend": "Resend",
|
||||||
|
"fileInfoTitle": "File Information",
|
||||||
|
"download": "Download",
|
||||||
|
"info": "Info",
|
||||||
|
"noStickers": "No Stickers",
|
||||||
|
"noStickersInPack": "This pack does not contains stickers",
|
||||||
|
"noStickerPacks": "No Sticker Packs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"spoiler": "Spoiler",
|
||||||
|
"activityHeatmap": "Activity Heatmap",
|
||||||
|
"custom": "Custom",
|
||||||
|
"usernameColor": "Username Color",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"plain": "Plain",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"colorValue": "Color Value",
|
||||||
|
"gradientDirection": "Gradient Direction",
|
||||||
|
"gradientDirectionToRight": "To Right",
|
||||||
|
"gradientDirectionToLeft": "To Left",
|
||||||
|
"gradientDirectionToBottom": "To Bottom",
|
||||||
|
"gradientDirectionToTop": "To Top",
|
||||||
|
"gradientDirectionToBottomRight": "To Bottom Right",
|
||||||
|
"gradientDirectionToBottomLeft": "To Bottom Left",
|
||||||
|
"gradientDirectionToTopRight": "To Top Right",
|
||||||
|
"gradientDirectionToTopLeft": "To Top Left",
|
||||||
|
"gradientColors": "Gradient Colors",
|
||||||
|
"color": "Color",
|
||||||
|
"addColor": "Add Color",
|
||||||
|
"availableWithYourPlan": "Available with your plan",
|
||||||
|
"upgradeRequired": "Upgrade required",
|
||||||
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "Time zone not found",
|
||||||
|
"awardPoints": "Awarded {} points",
|
||||||
|
"postFeaturedOn": "Post featured on {}",
|
||||||
|
"messageSentAt": "Sent at {}",
|
||||||
|
"myTickets": "My Tickets",
|
||||||
|
"drawHistory": "Draw History",
|
||||||
|
"lottery": "Lottery",
|
||||||
|
"noLotteryTickets": "No lottery tickets yet",
|
||||||
|
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
|
||||||
|
"buyTicket": "Buy Ticket",
|
||||||
|
"ticketNumbers": "Numbers: {}, Special: {}",
|
||||||
|
"cost": "Cost",
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"prizeWon": "Prize Won",
|
||||||
|
"pending": "Pending",
|
||||||
|
"drawn": "Drawn",
|
||||||
|
"won": "Won",
|
||||||
|
"lost": "Lost",
|
||||||
|
"noDrawHistory": "No draw history yet",
|
||||||
|
"buyLotteryTicket": "Buy Lottery Ticket",
|
||||||
|
"selectNumbers": "Select Numbers",
|
||||||
|
"select5UniqueNumbers": "Select 5 unique numbers",
|
||||||
|
"selectSpecialNumber": "Select Special Number",
|
||||||
|
"selectMultiplier": "Select Multiplier",
|
||||||
|
"baseCost": "Base Cost",
|
||||||
|
"totalCost": "Total Cost",
|
||||||
|
"prizeStructure": "Prize Structure",
|
||||||
|
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
|
||||||
|
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
|
||||||
|
"winningNumbers": "Winning Numbers",
|
||||||
|
"specialNumber": "Special Number",
|
||||||
|
"totalTickets": "Total Tickets",
|
||||||
|
"totalWinners": "Total Winners",
|
||||||
|
"prizePool": "Prize Pool",
|
||||||
|
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
|
||||||
|
"purchase": "Purchase",
|
||||||
|
"multiplierLabel": "Multiplier",
|
||||||
|
"specialOnly": "Special Only",
|
||||||
|
"matches": "Matches",
|
||||||
|
"thoughtDefaultTopic": "Reflection",
|
||||||
|
"thoughtAiName": "SN-chan",
|
||||||
|
"thoughtUserName": "You",
|
||||||
|
"thoughtStreamingHint": "Sn-chan is thinking...",
|
||||||
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
|
"thoughtFunctionCall": "Use {}",
|
||||||
|
"aiThought": "AI Thought",
|
||||||
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
|
"fabLocation": "FAB Location",
|
||||||
|
"activities": "Activities",
|
||||||
|
"presenceTypeGaming": "Playing",
|
||||||
|
"presenceTypeMusic": "Listening to Music",
|
||||||
|
"presenceTypeWorkout": "Working out",
|
||||||
|
"articleCompose": "Compose Article",
|
||||||
|
"backToHub": "Back to Hub",
|
||||||
|
"advancedFilters": "Advanced Filters",
|
||||||
|
"searchPosts": "Search Posts",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"fromDate": "From Date",
|
||||||
|
"toDate": "To Date",
|
||||||
|
"popularity": "Popularity",
|
||||||
|
"descendingOrder": "Descending Order",
|
||||||
|
"selectDate": "Select Date",
|
||||||
|
"pinnedPosts": "Pinned Posts",
|
||||||
|
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||||
|
"publicationSites": "Publication Sites",
|
||||||
|
"uploadTasks": "Upload Tasks",
|
||||||
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "{} responded",
|
||||||
|
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||||
|
"more": "More",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||||
|
"discard": "Discard",
|
||||||
|
"fund": "Fund",
|
||||||
|
"fundsRecent": "Recent Funds",
|
||||||
|
"fundCreateNew": "Create New",
|
||||||
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
|
"amountOfSplits": "Amount of Splits",
|
||||||
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share",
|
||||||
|
"affiliationSpell": "Affiliation Spell",
|
||||||
|
"affiliationSpellHint": "If you have an affiliation spell, enter it here.",
|
||||||
|
"friendsOnline": "Friends Online",
|
||||||
|
"createAccountAlmostThere": "Almost There",
|
||||||
|
"createAccountAlmostThereHint": "You're one step away from joining the Solar Network! Please solve the captcha puzzle shows next.",
|
||||||
|
"createAccountNotice": "Things you need to know before you create an account:",
|
||||||
|
"createAccountConfirmEmail": "After your account being created, you need go to your email inbox to active your account to get permission to use all features.",
|
||||||
|
"createAccountNoAltAccounts": "Multiple or alternative accounts are banned from the Solar Network, that will violates our terms of services.",
|
||||||
|
"createAccountAgreeTerms": "I've read these terms and agree to the terms of service.",
|
||||||
|
"createAccountProfile": "Create your profile",
|
||||||
|
"createAccountToS": "Review Terms & Conditions",
|
||||||
|
"updateYourProfileDescription": "Adjust how you looks on the Solar Network.",
|
||||||
|
"realmsDescription": "Manage realms you've joined.",
|
||||||
|
"exploreDescription": "Explore contents on the Solar Network.",
|
||||||
|
"accountDescription": "Information about your account.",
|
||||||
|
"chatDescription": "Group Chats and Direct Messages",
|
||||||
|
"connectionServerDown": "Unable to Connect",
|
||||||
|
"appSettingsDescription": "Customize your app.",
|
||||||
|
"accountSettingsDescription": "Manage your preferences on the Solar Network.",
|
||||||
|
"walletDescription": "Your source point wallet.",
|
||||||
|
"relationshipsDescription": "Friends and connections.",
|
||||||
|
"notificationsDescription": "See what's happended related to you recently.",
|
||||||
|
"settingsFestivalFeatures": "Festival Limited Features",
|
||||||
|
"categoriesAndTags": "Categories & Tags",
|
||||||
|
"webArticlesStandDescription": "Explore external sites articles.",
|
||||||
|
"aboutDescription": "Learn more about the Solar Network.",
|
||||||
|
"abuseReportsDescription": "View and manage abuse reports.",
|
||||||
|
"stickerMarketplaceDescription": "Browse and add sticker packs from the Solar Network marketplace.",
|
||||||
|
"webFeedsDescription": "Browse and subscribe to web feeds from the Solar Network.",
|
||||||
|
"discoverRealmsDescription": "Discover new realms and join them.",
|
||||||
|
"postShuffleDescription": "Shuffle posts to see the posts randomly.",
|
||||||
|
"levelingDescription": "See your leveling progress and history.",
|
||||||
|
"notableDayToday": "{} is today!",
|
||||||
|
"authSessionLogout": "Logout Session",
|
||||||
|
"authSessionLogoutHint": "Are you sure you want to logout this session? This will terminate this specific login session.",
|
||||||
|
"filesDescription": "Manage your files on the Solar Network Drive.",
|
||||||
|
"postComposeDescription": "Compose a new post",
|
||||||
|
"searchPostsDescription": "Search posts by title, content, or else.",
|
||||||
|
"accountActivationAlert": "Activate your account",
|
||||||
|
"accountActivationAlertHint": "Unactivated account may leads to various of permission issues, activate your account by clicking the link we sent to your email inbox.",
|
||||||
|
"accountActivationResendHint": "Didn't see it? Try click the button below to resend one. If you need to update your email while your account was unactivated, feel free to contact our customer service.",
|
||||||
|
"accountActivationResend": "Resend",
|
||||||
|
"ipAddress": "IP Address",
|
||||||
|
"noFurtherData": "No further data",
|
||||||
|
"searchAnything": "Search Anything...",
|
||||||
|
"tapToViewAllNotifications": "Tap to view all notifications",
|
||||||
|
"mostRecent": "Most Recent",
|
||||||
|
"noNotificationsYet": "No notifications yet",
|
||||||
|
"recentChats": "Recent Chats",
|
||||||
|
"noFeaturedPostsAvailable": "No featured posts available",
|
||||||
|
"searchChatsAndPages": "Search chats and pages...",
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"dashboardDescription": "All your data in one place.",
|
||||||
|
"postTagsCategories": "Post Tags and Categories",
|
||||||
|
"postTagsCategoriesDescription": "Browse posts by category and tags.",
|
||||||
|
"debugLogs": "Debug Logs",
|
||||||
|
"debugLogsDescription": "View debug logs for troubleshooting.",
|
||||||
|
"pinChatRoom": "Pin Chat Room",
|
||||||
|
"pinChatRoomDescription": "Pin this chat room to the top.",
|
||||||
|
"chatRoomPinned": "Chat room pinned successfully.",
|
||||||
|
"chatRoomUnpinned": "Chat room unpinned successfully.",
|
||||||
|
"pinnedChatRoom": "Pinned Rooms",
|
||||||
|
"settingsGroupedChatList": "Grouped Chat List",
|
||||||
|
"settingsNotifyWithHaptic": "Notification with Haptic Feedback",
|
||||||
|
"settingsDashSearchEngine": "Search Engine for web",
|
||||||
|
"settingsDashSearchEngineHelper": "Use %s as the placeholder for the query.",
|
||||||
|
"settingsDefaultScreen": "Default Screen",
|
||||||
|
"notableDayChristmas": "Christmas",
|
||||||
|
"notableDayNewYear": "New Year"
|
||||||
}
|
}
|
||||||
@@ -158,14 +158,12 @@
|
|||||||
"checkIn": "Check In",
|
"checkIn": "Check In",
|
||||||
"checkInNone": "Not checked-in yet",
|
"checkInNone": "Not checked-in yet",
|
||||||
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
||||||
"checkInResultLevel0": "Wrost Luck",
|
"checkInResultLevel0": "Worst Luck",
|
||||||
"checkInResultLevel1": "Bad Luck",
|
"checkInResultLevel1": "Bad Luck",
|
||||||
"checkInResultLevel2": "A Normal Day",
|
"checkInResultLevel2": "A Normal Day",
|
||||||
"checkInResultLevel3": "Good Luck",
|
"checkInResultLevel3": "Good Luck",
|
||||||
"checkInResultLevel4": "Best Luck",
|
"checkInResultLevel4": "Best Luck",
|
||||||
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
"checkInActivityTitle": "{} checked in on {} and got a {}",
|
||||||
"eventCalander": "Event Calander",
|
|
||||||
"eventCalanderEmpty": "No events on that day.",
|
|
||||||
"fortuneGraph": "Fortune Trend",
|
"fortuneGraph": "Fortune Trend",
|
||||||
"noFortuneData": "No fortune data available for this month.",
|
"noFortuneData": "No fortune data available for this month.",
|
||||||
"creatorHub": "Creator Hub",
|
"creatorHub": "Creator Hub",
|
||||||
@@ -316,7 +314,6 @@
|
|||||||
"settingsAutoTranslate": "Auto Translate",
|
"settingsAutoTranslate": "Auto Translate",
|
||||||
"settingsHideBottomNav": "Hide Bottom Navigation",
|
"settingsHideBottomNav": "Hide Bottom Navigation",
|
||||||
"settingsSoundEffects": "Sound Effects",
|
"settingsSoundEffects": "Sound Effects",
|
||||||
"settingsAprilFoolFeatures": "April Fool Features",
|
|
||||||
"settingsEnterToSend": "Enter to Send",
|
"settingsEnterToSend": "Enter to Send",
|
||||||
"settingsTransparentAppBar": "Transparent App Bar",
|
"settingsTransparentAppBar": "Transparent App Bar",
|
||||||
"settingsCustomFonts": "Custom Fonts",
|
"settingsCustomFonts": "Custom Fonts",
|
||||||
@@ -684,9 +681,9 @@
|
|||||||
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
|
"articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
|
||||||
"postVisibility": "Post Visibility",
|
"postVisibility": "Post Visibility",
|
||||||
"currentMembershipMember": "A member of Stellar Program · {}",
|
"currentMembershipMember": "A member of Stellar Program · {}",
|
||||||
"membershipPriceStellar": "1200 NSP per month, level 3+ required",
|
"membershipPriceStellar": "1200 NSP per month, level 20+ required",
|
||||||
"membershipPriceNova": "2400 NSP per month, level 6+ required",
|
"membershipPriceNova": "2400 NSP per month, level 40+ required",
|
||||||
"membershipPriceSupernova": "3600 NSP per month, level 9+ required",
|
"membershipPriceSupernova": "3600 NSP per month, level 60+ required",
|
||||||
"sharePostPhoto": "Share Post as Photo",
|
"sharePostPhoto": "Share Post as Photo",
|
||||||
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
|
"wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?",
|
||||||
"abuseReports": "Abuse Reports",
|
"abuseReports": "Abuse Reports",
|
||||||
@@ -707,7 +704,7 @@
|
|||||||
"aboutScreenDeveloperSectionTitle": "Developer",
|
"aboutScreenDeveloperSectionTitle": "Developer",
|
||||||
"aboutScreenContactUsTitle": "Contact Us",
|
"aboutScreenContactUsTitle": "Contact Us",
|
||||||
"aboutScreenLicenseTitle": "License",
|
"aboutScreenLicenseTitle": "License",
|
||||||
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
|
"aboutScreenLicenseContent": "AGPLv3",
|
||||||
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
|
"aboutScreenCopyright": "All rights reserved © Solsynth {}",
|
||||||
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
|
"aboutScreenMadeWith": "Made with ❤︎️ by Solar Network Team",
|
||||||
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
|
"aboutScreenFailedToLoadPackageInfo": "Failed to load package info: {error}",
|
||||||
@@ -752,21 +749,6 @@
|
|||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"markAsSensitive": "Mark as Sensitive",
|
"markAsSensitive": "Mark as Sensitive",
|
||||||
"fileName": "File name",
|
"fileName": "File name",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "Language",
|
|
||||||
"sexualContent": "Sexual Content",
|
|
||||||
"violence": "Violence",
|
|
||||||
"profanity": "Profanity",
|
|
||||||
"hateSpeech": "Hate Speech",
|
|
||||||
"racism": "Racism",
|
|
||||||
"adultContent": "Adult Content",
|
|
||||||
"drugAbuse": "Drug Abuse",
|
|
||||||
"alcoholAbuse": "Alcohol Abuse",
|
|
||||||
"gambling": "Gambling",
|
|
||||||
"selfHarm": "Self-harm",
|
|
||||||
"childAbuse": "Child Abuse",
|
|
||||||
"other": "Other"
|
|
||||||
},
|
|
||||||
"poll": "Poll",
|
"poll": "Poll",
|
||||||
"pollsRecent": "Recent Polls",
|
"pollsRecent": "Recent Polls",
|
||||||
"pollCreateNew": "Create New",
|
"pollCreateNew": "Create New",
|
||||||
@@ -1075,5 +1057,490 @@
|
|||||||
"deleteRecycledFiles": "Delete Recycled Files",
|
"deleteRecycledFiles": "Delete Recycled Files",
|
||||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||||
"upload": "Upload"
|
"upload": "Upload",
|
||||||
|
"deleteMessage": "Delete Message",
|
||||||
|
"deleteMessageConfirmation": "Are you sure you want to delete this message?",
|
||||||
|
"customReaction": "Custom Reaction",
|
||||||
|
"customReactions": "Custom Reactions",
|
||||||
|
"stickerPlaceholder": "Sticker Placeholder",
|
||||||
|
"reactionAttitude": "Reaction Attitude",
|
||||||
|
"addReaction": "Add Reaction",
|
||||||
|
"eventCalendar": "Event Calendar",
|
||||||
|
"eventCalendarEmpty": "No events on that day.",
|
||||||
|
"walletStats": "Wallet Statistics",
|
||||||
|
"totalTransactions": "Total Transactions",
|
||||||
|
"totalOrders": "Total Orders",
|
||||||
|
"totalIncome": "Total Income",
|
||||||
|
"totalOutgoing": "Total Outgoing",
|
||||||
|
"netBalance": "Net Balance",
|
||||||
|
"messageUpdateLinks": "Server generated links previews",
|
||||||
|
"messageUpdateEdited": "Edited a message",
|
||||||
|
"settingsCardBackgroundOpacity": "Card Background Opacity",
|
||||||
|
"settingsThemeMode": "Theme Mode",
|
||||||
|
"settingsThemeModeSystem": "System",
|
||||||
|
"settingsThemeModeLight": "Light",
|
||||||
|
"settingsThemeModeDark": "Dark",
|
||||||
|
"enterPin": "Enter your PIN code",
|
||||||
|
"chatReplyingTo": "Replying to {}",
|
||||||
|
"chatForwarding": "Forwarding message",
|
||||||
|
"chatEditing": "Editing message",
|
||||||
|
"chatNoContent": "No content",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "Language",
|
||||||
|
"sexualContent": "Sexual Content",
|
||||||
|
"violence": "Violence",
|
||||||
|
"profanity": "Profanity",
|
||||||
|
"hateSpeech": "Hate Speech",
|
||||||
|
"racism": "Racism",
|
||||||
|
"adultContent": "Adult Content",
|
||||||
|
"drugAbuse": "Drug Abuse",
|
||||||
|
"alcoholAbuse": "Alcohol Abuse",
|
||||||
|
"gambling": "Gambling",
|
||||||
|
"selfHarm": "Self-harm",
|
||||||
|
"childAbuse": "Child Abuse",
|
||||||
|
"other": "Other"
|
||||||
|
},
|
||||||
|
"Searching...": "Searching...",
|
||||||
|
"searchError": "Search failed. Please try again.",
|
||||||
|
"tryDifferentKeywords": "Try different keywords or remove search filters",
|
||||||
|
"settingsWindowOpacity": "Window Opacity",
|
||||||
|
"messageContent": "Message Content",
|
||||||
|
"updateAvailable": "Update available",
|
||||||
|
"noChangelogProvided": "No changelog provided.",
|
||||||
|
"useSecondarySourceForDownload": "Use secondary source for download",
|
||||||
|
"installUpdate": "Install update",
|
||||||
|
"openReleasePage": "Open release page",
|
||||||
|
"postCompose": "Compose Post",
|
||||||
|
"postPublish": "Publish Post",
|
||||||
|
"restoreDraftTitle": "Restore Draft",
|
||||||
|
"restoreDraftMessage": "A draft was found. Do you want to restore it?",
|
||||||
|
"draft": "Draft",
|
||||||
|
"purchaseGift": "Purchase Gift",
|
||||||
|
"selectRecipient": "Select Recipient",
|
||||||
|
"changeRecipient": "Change Recipient",
|
||||||
|
"addMessage": "Add Message",
|
||||||
|
"skipRecipient": "Skip Recipient",
|
||||||
|
"giftSubscriptions": "Gift Subscriptions",
|
||||||
|
"purchaseAGift": "Purchase a Gift",
|
||||||
|
"redeemAGift": "Redeem a Gift",
|
||||||
|
"giftHistory": "Gift History",
|
||||||
|
"sentGifts": "Sent Gifts",
|
||||||
|
"receivedGifts": "Received Gifts",
|
||||||
|
"noSentGifts": "No sent gifts",
|
||||||
|
"noReceivedGifts": "No received gifts",
|
||||||
|
"stellarGift": "Stellar Gift",
|
||||||
|
"novaGift": "Nova Gift",
|
||||||
|
"supernovaGift": "Supernova Gift",
|
||||||
|
"sameAsMembership": "Same as membership",
|
||||||
|
"enterGiftCodeToRedeem": "Enter gift code to redeem",
|
||||||
|
"enterGiftCode": "Enter gift code",
|
||||||
|
"giftPurchased": "Gift Purchased!",
|
||||||
|
"shareCodeWithRecipient": "Share this code with the recipient to redeem the gift.",
|
||||||
|
"openGiftAnyoneCanRedeem": "This is an open gift that anyone can redeem.",
|
||||||
|
"ok": "OK",
|
||||||
|
"selectedRecipient": "Selected recipient",
|
||||||
|
"noRecipientSelected": "No recipient selected",
|
||||||
|
"thisWillBeAnOpenGift": "This will be an open gift",
|
||||||
|
"personalMessage": "Personal Message",
|
||||||
|
"addPersonalMessageForRecipient": "Add a personal message for the recipient",
|
||||||
|
"giftStatusCreated": "Created",
|
||||||
|
"giftStatusSent": "Sent",
|
||||||
|
"giftStatusRedeemed": "Redeemed",
|
||||||
|
"giftStatusCancelled": "Cancelled",
|
||||||
|
"giftStatusExpired": "Expired",
|
||||||
|
"giftStatusUnknown": "Unknown",
|
||||||
|
"giftCodeCopiedToClipboard": "Gift code copied to clipboard",
|
||||||
|
"codeLabel": "Code: ",
|
||||||
|
"subscriptionLabel": "Subscription: ",
|
||||||
|
"toLabel": "To: ",
|
||||||
|
"fromLabel": "From: ",
|
||||||
|
"messageLabel": "Message: ",
|
||||||
|
"giftRedeemed": "Gift Redeemed!",
|
||||||
|
"giftRedeemedSuccessfully": "You have successfully redeemed the gift. Your new subscription is now active.",
|
||||||
|
"cancelGift": "Cancel Gift",
|
||||||
|
"cancelGiftConfirm": "Are you sure you want to cancel this gift? This action cannot be undone.",
|
||||||
|
"giftCancelledSuccessfully": "Gift cancelled successfully",
|
||||||
|
"createFund": "Create Fund",
|
||||||
|
"fundAmount": "Fund Amount",
|
||||||
|
"enterAmount": "Enter Amount",
|
||||||
|
"selectCurrency": "Select Currency",
|
||||||
|
"splitType": "Split Type",
|
||||||
|
"evenSplit": "Even Split",
|
||||||
|
"equalAmountEach": "Equal amount for each recipient",
|
||||||
|
"randomSplit": "Random Split",
|
||||||
|
"randomAmountEach": "Random amount for each recipient",
|
||||||
|
"recipientCount": "Recipient Count",
|
||||||
|
"numberOfRecipients": "Number of Recipients",
|
||||||
|
"addPersonalMessageForRecipients": "Add a personal message for recipients",
|
||||||
|
"invalidAmount": "Invalid amount",
|
||||||
|
"invalidRecipientCount": "Invalid recipient count",
|
||||||
|
"fundOverview": "Fund Overview",
|
||||||
|
"totalFundsSent": "Total Funds Sent",
|
||||||
|
"totalFundsReceived": "Total Funds Received",
|
||||||
|
"transactions": "Transactions",
|
||||||
|
"myFunds": "My Funds",
|
||||||
|
"availableFunds": "Available Funds",
|
||||||
|
"fundStatusCreated": "Created",
|
||||||
|
"fundStatusPartial": "Partially Claimed",
|
||||||
|
"fundStatusCompleted": "Fully Claimed",
|
||||||
|
"fundStatusExpired": "Expired",
|
||||||
|
"fundStatusUnknown": "Unknown",
|
||||||
|
"recipients": "Recipients",
|
||||||
|
"fundClaimedSuccessfully": "Fund claimed successfully!",
|
||||||
|
"claim": "Claim",
|
||||||
|
"noFundsCreated": "No funds created yet",
|
||||||
|
"createYourFirstFund": "Create your first fund to get started",
|
||||||
|
"noAvailableFunds": "No available funds",
|
||||||
|
"fundsWillAppearHere": "Funds you can claim will appear here",
|
||||||
|
"fundCreatedSuccessfully": "Fund created successfully!",
|
||||||
|
"selectRecipients": "Select Recipients",
|
||||||
|
"noRecipientsSelected": "No recipients selected",
|
||||||
|
"selectRecipientsToSendFund": "Select recipients to send the fund to",
|
||||||
|
"addRecipient": "Add Recipient",
|
||||||
|
"addMoreRecipients": "Add More Recipients",
|
||||||
|
"transactionDetails": "Transaction Details",
|
||||||
|
"remarks": "Remarks",
|
||||||
|
"payer": "Payer",
|
||||||
|
"payee": "Payee",
|
||||||
|
"transactionType": "Transaction Type",
|
||||||
|
"transfer": "Transfer",
|
||||||
|
"payment": "Payment",
|
||||||
|
"systemWallet": "System Wallet",
|
||||||
|
"date": "Date",
|
||||||
|
"createTransfer": "Create Transfer",
|
||||||
|
"transferAmount": "Transfer Amount",
|
||||||
|
"selectPayee": "Select Payee",
|
||||||
|
"selectedPayee": "Selected Payee",
|
||||||
|
"noPayeeSelected": "No payee selected",
|
||||||
|
"selectPayeeToTransfer": "Select payee to transfer to",
|
||||||
|
"addRemark": "Add Remark",
|
||||||
|
"transferRemark": "Transfer Remark",
|
||||||
|
"addRemarkForTransfer": "Add remark for transfer",
|
||||||
|
"enterPinToConfirmTransfer": "Enter your 6-digit PIN to confirm transfer",
|
||||||
|
"transferCreatedSuccessfully": "Transfer created successfully!",
|
||||||
|
"postUpdate": "Update",
|
||||||
|
"fileMetadata": "File Metadata",
|
||||||
|
"resend": "Resend",
|
||||||
|
"fileInfoTitle": "File Information",
|
||||||
|
"download": "Download",
|
||||||
|
"info": "Info",
|
||||||
|
"noStickers": "No Stickers",
|
||||||
|
"noStickersInPack": "This pack does not contains stickers",
|
||||||
|
"noStickerPacks": "No Sticker Packs",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"spoiler": "Spoiler",
|
||||||
|
"activityHeatmap": "Activity Heatmap",
|
||||||
|
"custom": "Custom",
|
||||||
|
"usernameColor": "Username Color",
|
||||||
|
"colorType": "Color Type",
|
||||||
|
"plain": "Plain",
|
||||||
|
"gradient": "Gradient",
|
||||||
|
"colorValue": "Color Value",
|
||||||
|
"gradientDirection": "Gradient Direction",
|
||||||
|
"gradientDirectionToRight": "To Right",
|
||||||
|
"gradientDirectionToLeft": "To Left",
|
||||||
|
"gradientDirectionToBottom": "To Bottom",
|
||||||
|
"gradientDirectionToTop": "To Top",
|
||||||
|
"gradientDirectionToBottomRight": "To Bottom Right",
|
||||||
|
"gradientDirectionToBottomLeft": "To Bottom Left",
|
||||||
|
"gradientDirectionToTopRight": "To Top Right",
|
||||||
|
"gradientDirectionToTopLeft": "To Top Left",
|
||||||
|
"gradientColors": "Gradient Colors",
|
||||||
|
"color": "Color",
|
||||||
|
"addColor": "Add Color",
|
||||||
|
"availableWithYourPlan": "Available with your plan",
|
||||||
|
"upgradeRequired": "Upgrade required",
|
||||||
|
"settingsDisableAnimation": "Disable Animation",
|
||||||
|
"addTag": "Add Tag",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "Time zone not found",
|
||||||
|
"awardPoints": "Awarded {} points",
|
||||||
|
"postFeaturedOn": "Post featured on {}",
|
||||||
|
"messageSentAt": "Sent at {}",
|
||||||
|
"myTickets": "My Tickets",
|
||||||
|
"drawHistory": "Draw History",
|
||||||
|
"lottery": "Lottery",
|
||||||
|
"noLotteryTickets": "No lottery tickets yet",
|
||||||
|
"buyYourFirstTicket": "Buy your first lottery ticket to get started!",
|
||||||
|
"buyTicket": "Buy Ticket",
|
||||||
|
"ticketNumbers": "Numbers: {}, Special: {}",
|
||||||
|
"cost": "Cost",
|
||||||
|
"multiplier": "Multiplier",
|
||||||
|
"prizeWon": "Prize Won",
|
||||||
|
"pending": "Pending",
|
||||||
|
"drawn": "Drawn",
|
||||||
|
"won": "Won",
|
||||||
|
"lost": "Lost",
|
||||||
|
"noDrawHistory": "No draw history yet",
|
||||||
|
"buyLotteryTicket": "Buy Lottery Ticket",
|
||||||
|
"selectNumbers": "Select Numbers",
|
||||||
|
"select5UniqueNumbers": "Select 5 unique numbers",
|
||||||
|
"selectSpecialNumber": "Select Special Number",
|
||||||
|
"selectMultiplier": "Select Multiplier",
|
||||||
|
"baseCost": "Base Cost",
|
||||||
|
"totalCost": "Total Cost",
|
||||||
|
"prizeStructure": "Prize Structure",
|
||||||
|
"enterPinToConfirmPurchase": "Enter your PIN to confirm purchase",
|
||||||
|
"ticketPurchasedSuccessfully": "Ticket purchased successfully!",
|
||||||
|
"winningNumbers": "Winning Numbers",
|
||||||
|
"specialNumber": "Special Number",
|
||||||
|
"totalTickets": "Total Tickets",
|
||||||
|
"totalWinners": "Total Winners",
|
||||||
|
"prizePool": "Prize Pool",
|
||||||
|
"enterPinToConfirmPayment": "Enter your PIN code to confirm payment",
|
||||||
|
"purchase": "Purchase",
|
||||||
|
"multiplierLabel": "Multiplier",
|
||||||
|
"specialOnly": "Special Only",
|
||||||
|
"matches": "Matches",
|
||||||
|
"thoughtDefaultTopic": "Reflection",
|
||||||
|
"thoughtAiName": "SN-chan",
|
||||||
|
"thoughtUserName": "You",
|
||||||
|
"thoughtStreamingHint": "Sn-chan is thinking...",
|
||||||
|
"thoughtInputHint": "Ask sn-chan anything...",
|
||||||
|
"thoughtNewConversation": "Start New Conversation",
|
||||||
|
"thoughtParseError": "Failed to parse AI response",
|
||||||
|
"thoughtFunctionCall": "Use {}",
|
||||||
|
"aiThought": "AI Thought",
|
||||||
|
"aiThoughtTitle": "Let sn-chan think",
|
||||||
|
"postReferenceUnavailable": "Referenced post is unavailable",
|
||||||
|
"fabLocation": "FAB Location",
|
||||||
|
"activities": "Activities",
|
||||||
|
"presenceTypeGaming": "Playing",
|
||||||
|
"presenceTypeMusic": "Listening to Music",
|
||||||
|
"presenceTypeWorkout": "Working out",
|
||||||
|
"articleCompose": "Compose Article",
|
||||||
|
"backToHub": "Back to Hub",
|
||||||
|
"advancedFilters": "Advanced Filters",
|
||||||
|
"searchPosts": "Search Posts",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"fromDate": "From Date",
|
||||||
|
"toDate": "To Date",
|
||||||
|
"popularity": "Popularity",
|
||||||
|
"descendingOrder": "Descending Order",
|
||||||
|
"selectDate": "Select Date",
|
||||||
|
"pinnedPosts": "Pinned Posts",
|
||||||
|
"customReactionHint": "Custom Reaction allow you to use user uploaded stickers as the symbol of the reaction for the post. Exclusive for Stellar Program members.",
|
||||||
|
"publicationSites": "Publication Sites",
|
||||||
|
"uploadTasks": "Upload Tasks",
|
||||||
|
"thoughtFunctionCallBegin": "Calling tool {}",
|
||||||
|
"thoughtFunctionCallFinish": "{} responded",
|
||||||
|
"thoughtUnpaidHint": "Thinking unavaiable due to unpaid orders",
|
||||||
|
"more": "More",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.",
|
||||||
|
"discard": "Discard",
|
||||||
|
"fund": "Fund",
|
||||||
|
"fundsRecent": "Recent Funds",
|
||||||
|
"fundCreateNew": "Create New",
|
||||||
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
|
"amountOfSplits": "Amount of Splits",
|
||||||
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder",
|
||||||
|
"clearCompleted": "Clear Completed",
|
||||||
|
"uploadSuccess": "Upload successful!",
|
||||||
|
"wouldYouLikeToViewFile": "Would you like to view the file?",
|
||||||
|
"contentCantEmpty": "Content cannot be empty",
|
||||||
|
"features": "Features",
|
||||||
|
"unnamed": "Unnamed",
|
||||||
|
"fundEnvelopeLoadFailed": "Failed to load fund envelope",
|
||||||
|
"fundEnvelope": "Fund Envelope",
|
||||||
|
"fundEnvelopeRemaining": "Remaining: {} {}",
|
||||||
|
"fundEnvelopeSplit": "Split: {}",
|
||||||
|
"fundEnvelopeSplitEvenly": "Evenly",
|
||||||
|
"fundEnvelopeSplitRandomly": "Randomly",
|
||||||
|
"fundEnvelopeClaimSuccess": "Fund claimed successfully!",
|
||||||
|
"fundEnvelopeStatusCreated": "Created",
|
||||||
|
"fundEnvelopeStatusPartial": "Partially Claimed",
|
||||||
|
"fundEnvelopeStatusCompleted": "Fully Claimed",
|
||||||
|
"fundEnvelopeStatusExpired": "Expired",
|
||||||
|
"fundEnvelopeStatusUnknown": "Unknown",
|
||||||
|
"fundEnvelopeRecipients": "Recipients ({}/{} claimed)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "Expired {} day ago",
|
||||||
|
"other": "Expired {} days ago"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "Expires soon",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "Expires in {} hour",
|
||||||
|
"other": "Expires in {} hours"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "Expires in {} day",
|
||||||
|
"other": "Expires in {} days"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} splits",
|
||||||
|
"fundEnvelopeUnknownUser": "Unknown User",
|
||||||
|
"deleteSite": "Delete Site",
|
||||||
|
"deleteSiteConfirm": "Are you sure you want to delete this site?",
|
||||||
|
"siteDeletedSuccess": "Site deleted successfully",
|
||||||
|
"siteSlug": "Slug",
|
||||||
|
"siteSlugHint": "my-site",
|
||||||
|
"siteSlugRequired": "Please enter a slug",
|
||||||
|
"siteSlugInvalid": "Slug can only contain lowercase letters, numbers, and dashes",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"siteNameHint": "My Publication Site",
|
||||||
|
"siteNameRequired": "Please enter a site name",
|
||||||
|
"siteMode": "Mode",
|
||||||
|
"siteModeFullyManaged": "Fully Managed",
|
||||||
|
"siteModeSelfManaged": "Self-Managed",
|
||||||
|
"editPublicationSite": "Edit Publication Site",
|
||||||
|
"deletePublicationSite": "Delete Publication Site",
|
||||||
|
"publicationSiteSavedSuccess": "Publication site saved successfully",
|
||||||
|
"publicationSiteDeleteConfirm": "Are you sure you want to delete this publication site? This action cannot be undone.",
|
||||||
|
"publicationSiteDeletedSuccess": "Publication site deleted successfully",
|
||||||
|
"newPublicationSite": "New Publication Site",
|
||||||
|
"siteDetails": "Site Details",
|
||||||
|
"siteInformation": "Site Information",
|
||||||
|
"siteDomain": "Domain",
|
||||||
|
"siteCreated": "Created",
|
||||||
|
"siteUpdated": "Updated",
|
||||||
|
"failedToLoadSite": "Failed to load site",
|
||||||
|
"sitePages": "Pages",
|
||||||
|
"noPagesYet": "No pages yet",
|
||||||
|
"createFirstPage": "Create your first page to get started",
|
||||||
|
"failedToLoadPages": "Failed to load pages",
|
||||||
|
"fileManagement": "File Management",
|
||||||
|
"siteFiles": "Files",
|
||||||
|
"siteFolder": "Folder",
|
||||||
|
"siteRoot": "Root",
|
||||||
|
"noFilesUploadedYet": "No files uploaded yet",
|
||||||
|
"uploadFirstFile": "Upload your first file to get started",
|
||||||
|
"failedToLoadFiles": "Failed to load files",
|
||||||
|
"noFilesFoundInFolder": "No files found in the selected folder",
|
||||||
|
"fileActions": "File Actions",
|
||||||
|
"purgeFiles": "Purge Files",
|
||||||
|
"purgeFilesDescription": "Remove all uploaded files from the site",
|
||||||
|
"deploySite": "Deploy Site",
|
||||||
|
"deploySiteDescription": "Upload and deploy a new version from ZIP archive",
|
||||||
|
"confirmPurge": "Confirm Purge",
|
||||||
|
"purgeFilesConfirm": "This will permanently delete all files uploaded to this site. This action cannot be undone. Are you sure you want to continue?",
|
||||||
|
"purgeAllFiles": "Purge All Files",
|
||||||
|
"allFilesPurgedSuccess": "All files purged successfully",
|
||||||
|
"failedToPurgeFiles": "Failed to purge files: {}",
|
||||||
|
"siteDeployedSuccess": "Site deployed successfully",
|
||||||
|
"failedToDeploySite": "Failed to deploy site: {}",
|
||||||
|
"createPage": "Create Page",
|
||||||
|
"editPage": "Edit Page",
|
||||||
|
"pageType": "Page Type",
|
||||||
|
"htmlPage": "HTML Page",
|
||||||
|
"redirectPage": "Redirect Page",
|
||||||
|
"pageTypeRequired": "Please select a page type",
|
||||||
|
"pagePath": "Page Path",
|
||||||
|
"pagePathHint": "/about, /contact, etc.",
|
||||||
|
"pagePathRequired": "Please enter a page path",
|
||||||
|
"pagePathInvalid": "Page path can only contain letters, numbers, hyphens, underscores, and slashes",
|
||||||
|
"pagePathMustStartWithSlash": "Page path must start with /",
|
||||||
|
"pagePathNoConsecutiveSlashes": "Page path cannot have consecutive slashes",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleHint": "About Us, Contact, etc.",
|
||||||
|
"pageTitleRequired": "Please enter a page title",
|
||||||
|
"pageContentHtml": "Page Content (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>This is my page content...</p>",
|
||||||
|
"pageContentRequired": "Please enter HTML content for the page",
|
||||||
|
"redirectTarget": "Redirect Target",
|
||||||
|
"redirectTargetHint": "/new-page, https://example.com, etc.",
|
||||||
|
"redirectTargetRequired": "Please enter a redirect target",
|
||||||
|
"redirectTargetInvalid": "Target must be a relative path (/) or absolute URL (http/https)",
|
||||||
|
"deletePage": "Delete Page",
|
||||||
|
"deletePageConfirm": "Are you sure you want to delete this page?",
|
||||||
|
"savePage": "Save Page",
|
||||||
|
"pageCreatedSuccess": "Page created successfully",
|
||||||
|
"pageUpdatedSuccess": "Page updated successfully",
|
||||||
|
"pageDeletedSuccess": "Page deleted successfully",
|
||||||
|
"uploadFiles": "Upload Files",
|
||||||
|
"uploadPath": "Upload Path",
|
||||||
|
"uploadPathHint": "/ (root) or /assets/images/",
|
||||||
|
"uploadPathRequired": "Please enter an upload path",
|
||||||
|
"uploadPathMustStartWithSlash": "Path must start with /",
|
||||||
|
"uploadPathNoSpaces": "Path cannot contain spaces",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "Path cannot have consecutive slashes",
|
||||||
|
"percentCompleted": "{}% completed",
|
||||||
|
"filesToUpload": "{} files to upload",
|
||||||
|
"fileSizeKb": "Size: {} KB",
|
||||||
|
"uploadingEllipsis": "Uploading...",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "Upload {} File",
|
||||||
|
"other": "Upload {} Files"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "All uploads completed",
|
||||||
|
"someUploadsFailed": "Some uploads failed",
|
||||||
|
"uploadingInProgress": "Uploading in progress",
|
||||||
|
"readyToUpload": "Ready to upload",
|
||||||
|
"allFilesUploadedSuccess": "All files uploaded successfully",
|
||||||
|
"lotteryLastNumberSpecial": "The last selected number will be your special number.",
|
||||||
|
"lotteryMultiplierRequired": "Please enter a multiplier",
|
||||||
|
"lotteryMultiplierRange": "Multiplier must be between 1 and 10",
|
||||||
|
"dropToShare": "Drop to share",
|
||||||
|
"affiliationSpell": "Affiliation Spell",
|
||||||
|
"affiliationSpellHint": "If you have an affiliation spell, enter it here.",
|
||||||
|
"friendsOnline": "Friends Online",
|
||||||
|
"createAccountAlmostThere": "Almost There",
|
||||||
|
"createAccountAlmostThereHint": "You're one step away from joining the Solar Network! Please solve the captcha puzzle shows next.",
|
||||||
|
"createAccountNotice": "Things you need to know before you create an account:",
|
||||||
|
"createAccountConfirmEmail": "After your account being created, you need go to your email inbox to active your account to get permission to use all features.",
|
||||||
|
"createAccountNoAltAccounts": "Multiple or alternative accounts are banned from the Solar Network, that will violates our terms of services.",
|
||||||
|
"createAccountAgreeTerms": "I've read these terms and agree to the terms of service.",
|
||||||
|
"createAccountProfile": "Create your profile",
|
||||||
|
"createAccountToS": "Review Terms & Conditions",
|
||||||
|
"updateYourProfileDescription": "Adjust how you looks on the Solar Network.",
|
||||||
|
"realmsDescription": "Manage realms you've joined.",
|
||||||
|
"exploreDescription": "Explore contents on the Solar Network.",
|
||||||
|
"accountDescription": "Information about your account.",
|
||||||
|
"chatDescription": "Group Chats and Direct Messages",
|
||||||
|
"connectionServerDown": "Unable to Connect",
|
||||||
|
"appSettingsDescription": "Customize your app.",
|
||||||
|
"accountSettingsDescription": "Manage your preferences on the Solar Network.",
|
||||||
|
"walletDescription": "Your source point wallet.",
|
||||||
|
"relationshipsDescription": "Friends and connections.",
|
||||||
|
"notificationsDescription": "See what's happended related to you recently.",
|
||||||
|
"settingsFestivalFeatures": "Festival Limited Features",
|
||||||
|
"categoriesAndTags": "Categories & Tags",
|
||||||
|
"webArticlesStandDescription": "Explore external sites articles.",
|
||||||
|
"aboutDescription": "Learn more about the Solar Network.",
|
||||||
|
"abuseReportsDescription": "View and manage abuse reports.",
|
||||||
|
"stickerMarketplaceDescription": "Browse and add sticker packs from the Solar Network marketplace.",
|
||||||
|
"webFeedsDescription": "Browse and subscribe to web feeds from the Solar Network.",
|
||||||
|
"discoverRealmsDescription": "Discover new realms and join them.",
|
||||||
|
"postShuffleDescription": "Shuffle posts to see the posts randomly.",
|
||||||
|
"levelingDescription": "See your leveling progress and history.",
|
||||||
|
"notableDayToday": "{} is today!",
|
||||||
|
"authSessionLogout": "Logout Session",
|
||||||
|
"authSessionLogoutHint": "Are you sure you want to logout this session? This will terminate this specific login session.",
|
||||||
|
"filesDescription": "Manage your files on the Solar Network Drive.",
|
||||||
|
"postComposeDescription": "Compose a new post",
|
||||||
|
"searchPostsDescription": "Search posts by title, content, or else.",
|
||||||
|
"accountActivationAlert": "Activate your account",
|
||||||
|
"accountActivationAlertHint": "Unactivated account may leads to various of permission issues, activate your account by clicking the link we sent to your email inbox.",
|
||||||
|
"accountActivationResendHint": "Didn't see it? Try click the button below to resend one. If you need to update your email while your account was unactivated, feel free to contact our customer service.",
|
||||||
|
"accountActivationResend": "Resend",
|
||||||
|
"ipAddress": "IP Address",
|
||||||
|
"noFurtherData": "No further data",
|
||||||
|
"searchAnything": "Search Anything...",
|
||||||
|
"tapToViewAllNotifications": "Tap to view all notifications",
|
||||||
|
"mostRecent": "Most Recent",
|
||||||
|
"noNotificationsYet": "No notifications yet",
|
||||||
|
"recentChats": "Recent Chats",
|
||||||
|
"noFeaturedPostsAvailable": "No featured posts available",
|
||||||
|
"searchChatsAndPages": "Search chats and pages...",
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"dashboardDescription": "All your data in one place.",
|
||||||
|
"postTagsCategories": "Post Tags and Categories",
|
||||||
|
"postTagsCategoriesDescription": "Browse posts by category and tags.",
|
||||||
|
"debugLogs": "Debug Logs",
|
||||||
|
"debugLogsDescription": "View debug logs for troubleshooting.",
|
||||||
|
"pinChatRoom": "Pin Chat Room",
|
||||||
|
"pinChatRoomDescription": "Pin this chat room to the top.",
|
||||||
|
"chatRoomPinned": "Chat room pinned successfully.",
|
||||||
|
"chatRoomUnpinned": "Chat room unpinned successfully.",
|
||||||
|
"pinnedChatRoom": "Pinned Rooms",
|
||||||
|
"settingsGroupedChatList": "Grouped Chat List",
|
||||||
|
"settingsNotifyWithHaptic": "Notification with Haptic Feedback",
|
||||||
|
"settingsDashSearchEngine": "Search Engine for web",
|
||||||
|
"settingsDashSearchEngineHelper": "Use %s as the placeholder for the query.",
|
||||||
|
"settingsDefaultScreen": "Default Screen",
|
||||||
|
"notableDayChristmas": "Christmas",
|
||||||
|
"notableDayNewYear": "New Year"
|
||||||
}
|
}
|
||||||
@@ -64,8 +64,8 @@
|
|||||||
"authFactorTOTPDescription": "由 TOTP 验证器生成的一次性验证码。",
|
"authFactorTOTPDescription": "由 TOTP 验证器生成的一次性验证码。",
|
||||||
"authFactorInAppNotify": "应用内通知",
|
"authFactorInAppNotify": "应用内通知",
|
||||||
"authFactorInAppNotifyDescription": "通过应用内通知发送的一次性验证码。",
|
"authFactorInAppNotifyDescription": "通过应用内通知发送的一次性验证码。",
|
||||||
"authFactorPin": "Pin 码",
|
"authFactorPin": "PIN 码",
|
||||||
"authFactorPinDescription": "它由6位数字组成。它不能用于登录。 当执行一些危险的操作时,系统将要求您输入此 PIN 进行确认。",
|
"authFactorPinDescription": "此 PIN 码由 6 位数字组成,不可用于登录。执行高风险操作时,系统会要求输入此 PIN 码进行确认。",
|
||||||
"realms": "领域",
|
"realms": "领域",
|
||||||
"createRealm": "创建领域",
|
"createRealm": "创建领域",
|
||||||
"createRealmHint": "结识志同道合的朋友、建立社区等等。",
|
"createRealmHint": "结识志同道合的朋友、建立社区等等。",
|
||||||
@@ -158,14 +158,12 @@
|
|||||||
"checkIn": "签到",
|
"checkIn": "签到",
|
||||||
"checkInNone": "尚未签到",
|
"checkInNone": "尚未签到",
|
||||||
"checkInNoneHint": "通过签到获取您的财富提示和每日奖励。",
|
"checkInNoneHint": "通过签到获取您的财富提示和每日奖励。",
|
||||||
"checkInResultLevel0": "最差运气",
|
"checkInResultLevel0": "大凶",
|
||||||
"checkInResultLevel1": "坏运气",
|
"checkInResultLevel1": "凶",
|
||||||
"checkInResultLevel2": "一个普通的日常",
|
"checkInResultLevel2": "中平",
|
||||||
"checkInResultLevel3": "好运",
|
"checkInResultLevel3": "吉",
|
||||||
"checkInResultLevel4": "最佳运气",
|
"checkInResultLevel4": "大吉",
|
||||||
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
||||||
"eventCalander": "活动日历",
|
|
||||||
"eventCalanderEmpty": "该日无活动。",
|
|
||||||
"fortuneGraph": "时运趋势",
|
"fortuneGraph": "时运趋势",
|
||||||
"noFortuneData": "本月沒有时运數據。",
|
"noFortuneData": "本月沒有时运數據。",
|
||||||
"creatorHub": "创作者中心",
|
"creatorHub": "创作者中心",
|
||||||
@@ -316,7 +314,6 @@
|
|||||||
"settingsAutoTranslate": "自动翻译",
|
"settingsAutoTranslate": "自动翻译",
|
||||||
"settingsHideBottomNav": "隐藏底部导航",
|
"settingsHideBottomNav": "隐藏底部导航",
|
||||||
"settingsSoundEffects": "音效",
|
"settingsSoundEffects": "音效",
|
||||||
"settingsAprilFoolFeatures": "愚人节功能",
|
|
||||||
"settingsEnterToSend": "按下 Enter 发送",
|
"settingsEnterToSend": "按下 Enter 发送",
|
||||||
"settingsTransparentAppBar": "使用完全透明的状态栏",
|
"settingsTransparentAppBar": "使用完全透明的状态栏",
|
||||||
"settingsCustomFonts": "自定义字体",
|
"settingsCustomFonts": "自定义字体",
|
||||||
@@ -344,7 +341,7 @@
|
|||||||
"accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。",
|
"accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。",
|
||||||
"unauthorized": "未授权",
|
"unauthorized": "未授权",
|
||||||
"unauthorizedHint": "您未登录或会话已过期,请重新登录。",
|
"unauthorizedHint": "您未登录或会话已过期,请重新登录。",
|
||||||
"publisherBelongsTo": "属于",
|
"publisherBelongsTo": "属于 {}",
|
||||||
"postContent": "内容",
|
"postContent": "内容",
|
||||||
"postSettings": "设置",
|
"postSettings": "设置",
|
||||||
"postPublisherUnselected": "未指定发布者",
|
"postPublisherUnselected": "未指定发布者",
|
||||||
@@ -391,10 +388,6 @@
|
|||||||
"other": "{} 正在输入……"
|
"other": "{} 正在输入……"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "外观",
|
"settingsAppearance": "外观",
|
||||||
"settingsThemeMode": "主题模式",
|
|
||||||
"settingsThemeModeSystem": "跟随系统",
|
|
||||||
"settingsThemeModeLight": "浅色",
|
|
||||||
"settingsThemeModeDark": "深色",
|
|
||||||
"settingsServer": "服务器",
|
"settingsServer": "服务器",
|
||||||
"settingsBehavior": "行为",
|
"settingsBehavior": "行为",
|
||||||
"settingsDesktop": "桌面",
|
"settingsDesktop": "桌面",
|
||||||
@@ -417,7 +410,7 @@
|
|||||||
"contactMethodAddressHint": "输入您的现实地址",
|
"contactMethodAddressHint": "输入您的现实地址",
|
||||||
"contactMethodEmailDescription": "您的电子邮件将用于帐户恢复和通知",
|
"contactMethodEmailDescription": "您的电子邮件将用于帐户恢复和通知",
|
||||||
"contactMethodPhoneDescription": "您的电话号码将用于帐户恢复和通知",
|
"contactMethodPhoneDescription": "您的电话号码将用于帐户恢复和通知",
|
||||||
"contactMethodAddressDescription": "您的实际地址将用于运输和计费目的。",
|
"contactMethodAddressDescription": "您的地址将用于物流和账单。",
|
||||||
"contactMethodVerified": "已验证",
|
"contactMethodVerified": "已验证",
|
||||||
"contactMethodUnverified": "未认证",
|
"contactMethodUnverified": "未认证",
|
||||||
"contactMethodVerify": "验证联系方式",
|
"contactMethodVerify": "验证联系方式",
|
||||||
@@ -475,8 +468,8 @@
|
|||||||
"description": "描述",
|
"description": "描述",
|
||||||
"pinCode": "PIN 码",
|
"pinCode": "PIN 码",
|
||||||
"biometric": "生物识别",
|
"biometric": "生物识别",
|
||||||
"enterPinToConfirm": "请输入您的6位数字 PIN 以确认付款",
|
"enterPinToConfirm": "请输入您的 6 位数字 PIN 以确认付款",
|
||||||
"clearPin": "清除 PIN 码",
|
"clearPin": "清除 PIN",
|
||||||
"useBiometricToConfirm": "使用生物特征认证来确认付款",
|
"useBiometricToConfirm": "使用生物特征认证来确认付款",
|
||||||
"touchSensorToAuthenticate": "触摸传感器进行身份验证",
|
"touchSensorToAuthenticate": "触摸传感器进行身份验证",
|
||||||
"authenticating": "认证中……",
|
"authenticating": "认证中……",
|
||||||
@@ -485,7 +478,7 @@
|
|||||||
"processingPayment": "处理付款中……",
|
"processingPayment": "处理付款中……",
|
||||||
"pleaseWait": "请稍候",
|
"pleaseWait": "请稍候",
|
||||||
"paymentFailed": "付款失败,请重试。",
|
"paymentFailed": "付款失败,请重试。",
|
||||||
"invalidPin": "错误的 PIN。请再试一次。",
|
"invalidPin": "PIN 码错误,请重试。",
|
||||||
"biometricAuthFailed": "生物识别身份验证失败。请重试。",
|
"biometricAuthFailed": "生物识别身份验证失败。请重试。",
|
||||||
"paymentSuccess": "付款成功完成!",
|
"paymentSuccess": "付款成功完成!",
|
||||||
"membershipPurchaseSuccess": "好耶,会员购买成功!",
|
"membershipPurchaseSuccess": "好耶,会员购买成功!",
|
||||||
@@ -529,7 +522,7 @@
|
|||||||
"safetyReportReasonHint": "请提供更多证据……",
|
"safetyReportReasonHint": "请提供更多证据……",
|
||||||
"safetyReportSubmit": "提交举报",
|
"safetyReportSubmit": "提交举报",
|
||||||
"safetyReportSubmitting": "提交中……",
|
"safetyReportSubmitting": "提交中……",
|
||||||
"safetyReportSuccess": "举报成功,感谢您参与维护社区健康发展。",
|
"safetyReportSuccess": "举报提交成功,感谢您协助维护社区安全。",
|
||||||
"safetyReportError": "举报失败,请稍后重试。",
|
"safetyReportError": "举报失败,请稍后重试。",
|
||||||
"safetyReportReasonRequired": "请提供举报证据",
|
"safetyReportReasonRequired": "请提供举报证据",
|
||||||
"safetyReportTypeSpam": "垃圾或导向错误",
|
"safetyReportTypeSpam": "垃圾或导向错误",
|
||||||
@@ -603,7 +596,7 @@
|
|||||||
"abuseReportReason": "补充详情",
|
"abuseReportReason": "补充详情",
|
||||||
"abuseReportReasonHint": "请提供更多详情……",
|
"abuseReportReasonHint": "请提供更多详情……",
|
||||||
"abuseReportSubmit": "提交举报",
|
"abuseReportSubmit": "提交举报",
|
||||||
"abuseReportSuccess": "举报提交成功,感谢你为社区维护作出贡献。",
|
"abuseReportSuccess": "举报提交成功,感谢您协助维护社区安全。",
|
||||||
"abuseReportError": "无法提交举报,请稍后再试。",
|
"abuseReportError": "无法提交举报,请稍后再试。",
|
||||||
"abuseReportReasonRequired": "请提供关于此事件的细节",
|
"abuseReportReasonRequired": "请提供关于此事件的细节",
|
||||||
"abuseReportSuccessTitle": "举报已提交",
|
"abuseReportSuccessTitle": "举报已提交",
|
||||||
@@ -688,9 +681,9 @@
|
|||||||
"articleAttachmentHint": "附件必须上传并插入到文章主体中才能显示出来。",
|
"articleAttachmentHint": "附件必须上传并插入到文章主体中才能显示出来。",
|
||||||
"postVisibility": "可见性",
|
"postVisibility": "可见性",
|
||||||
"currentMembershipMember": "恒星计划成员 · {}",
|
"currentMembershipMember": "恒星计划成员 · {}",
|
||||||
"membershipPriceStellar": "需要用户等级 3+,每月价格 1200 NSP",
|
"membershipPriceStellar": "需要用户等级达到 20 以上,每月价格 1200 NSP",
|
||||||
"membershipPriceNova": "需要用户等级 6+,每月价格 2400 NSP",
|
"membershipPriceNova": "需要用户等级达到 40 以上,每月价格 2400 NSP",
|
||||||
"membershipPriceSupernova": "需要用户等级 9+,每月价格 3600 NSP",
|
"membershipPriceSupernova": "需要用户等级达到 60 以上,每月价格 3600 NSP",
|
||||||
"sharePostPhoto": "通过图片分享帖子",
|
"sharePostPhoto": "通过图片分享帖子",
|
||||||
"wouldYouLikeToNavigateToChat": "你想要前往聊天页面吗?",
|
"wouldYouLikeToNavigateToChat": "你想要前往聊天页面吗?",
|
||||||
"abuseReports": "举报",
|
"abuseReports": "举报",
|
||||||
@@ -711,7 +704,7 @@
|
|||||||
"aboutScreenDeveloperSectionTitle": "开发者",
|
"aboutScreenDeveloperSectionTitle": "开发者",
|
||||||
"aboutScreenContactUsTitle": "联系我们",
|
"aboutScreenContactUsTitle": "联系我们",
|
||||||
"aboutScreenLicenseTitle": "许可",
|
"aboutScreenLicenseTitle": "许可",
|
||||||
"aboutScreenLicenseContent": "无法翻译",
|
"aboutScreenLicenseContent": "GNU Affero 通用公共许可证 v3.0",
|
||||||
"aboutScreenCopyright": "版权所有 © Solsynth {}",
|
"aboutScreenCopyright": "版权所有 © Solsynth {}",
|
||||||
"aboutScreenMadeWith": "由 Solar Network 团队用 ❤︎️ 制作",
|
"aboutScreenMadeWith": "由 Solar Network 团队用 ❤︎️ 制作",
|
||||||
"aboutScreenFailedToLoadPackageInfo": "无法加载包信息:{error}",
|
"aboutScreenFailedToLoadPackageInfo": "无法加载包信息:{error}",
|
||||||
@@ -756,21 +749,6 @@
|
|||||||
"rename": "重命名",
|
"rename": "重命名",
|
||||||
"markAsSensitive": "标记为敏感",
|
"markAsSensitive": "标记为敏感",
|
||||||
"fileName": "文件名",
|
"fileName": "文件名",
|
||||||
"sensitiveCategories": {
|
|
||||||
"language": "语言",
|
|
||||||
"sexualContent": "色情内容",
|
|
||||||
"violence": "暴力",
|
|
||||||
"profanity": "亵渎",
|
|
||||||
"hateSpeech": "仇恨言论",
|
|
||||||
"racism": "种族主义",
|
|
||||||
"adultContent": "成人内容",
|
|
||||||
"drugAbuse": "药物滥用",
|
|
||||||
"alcoholAbuse": "酗酒",
|
|
||||||
"gambling": "赌博",
|
|
||||||
"selfHarm": "自残",
|
|
||||||
"childAbuse": "虐待儿童",
|
|
||||||
"other": "其他"
|
|
||||||
},
|
|
||||||
"poll": "投票",
|
"poll": "投票",
|
||||||
"pollsRecent": "最近投票",
|
"pollsRecent": "最近投票",
|
||||||
"pollCreateNew": "创建新投票",
|
"pollCreateNew": "创建新投票",
|
||||||
@@ -944,7 +922,7 @@
|
|||||||
"editBot": "编辑机器人",
|
"editBot": "编辑机器人",
|
||||||
"botAutomatedBy": "由 {} 自动化",
|
"botAutomatedBy": "由 {} 自动化",
|
||||||
"botDetails": "机器人详情",
|
"botDetails": "机器人详情",
|
||||||
"overview": "总揽",
|
"overview": "总览",
|
||||||
"keys": "密钥",
|
"keys": "密钥",
|
||||||
"botNotFound": "机器人未找到。",
|
"botNotFound": "机器人未找到。",
|
||||||
"newBotKey": "新建密钥",
|
"newBotKey": "新建密钥",
|
||||||
@@ -956,8 +934,8 @@
|
|||||||
"keyName": "密钥名称",
|
"keyName": "密钥名称",
|
||||||
"newKeyGenerated": "新密钥已生成",
|
"newKeyGenerated": "新密钥已生成",
|
||||||
"copyKeyHint": "请安全的保存该密钥,你不会再次看到它。",
|
"copyKeyHint": "请安全的保存该密钥,你不会再次看到它。",
|
||||||
"rotateKey": "旋转密钥",
|
"rotateKey": "轮换密钥",
|
||||||
"rotateBotKey": "旋转密钥",
|
"rotateBotKey": "轮换机器人密钥",
|
||||||
"rotateBotKeyHint": "你确认要旋转这个密钥?久的密钥会立即失效,该操作无法撤销。",
|
"rotateBotKeyHint": "你确认要旋转这个密钥?久的密钥会立即失效,该操作无法撤销。",
|
||||||
"webFeedArticleCount": {
|
"webFeedArticleCount": {
|
||||||
"zero": "没有文章",
|
"zero": "没有文章",
|
||||||
@@ -1064,7 +1042,7 @@
|
|||||||
"selectPool": "选择储存池",
|
"selectPool": "选择储存池",
|
||||||
"choosePool": "选择一个储存池",
|
"choosePool": "选择一个储存池",
|
||||||
"errorLoadingPools": "加载池时出错",
|
"errorLoadingPools": "加载池时出错",
|
||||||
"quotaCostInfo": "此上传将消耗{} 配额点",
|
"quotaCostInfo": "此上传将消耗 {} 配额点",
|
||||||
"uploadConstraints": "上传限制",
|
"uploadConstraints": "上传限制",
|
||||||
"fileSizeExceeded": "文件大小超过了 {} 的最大限制",
|
"fileSizeExceeded": "文件大小超过了 {} 的最大限制",
|
||||||
"fileTypeNotAccepted": "此储存池不接受该文件类型",
|
"fileTypeNotAccepted": "此储存池不接受该文件类型",
|
||||||
@@ -1079,5 +1057,490 @@
|
|||||||
"deleteRecycledFiles": "删除被回收的文件",
|
"deleteRecycledFiles": "删除被回收的文件",
|
||||||
"recycledFilesDeleted": "被回收文件成功删除",
|
"recycledFilesDeleted": "被回收文件成功删除",
|
||||||
"failedToDeleteRecycledFiles": "删除被回收文件失败",
|
"failedToDeleteRecycledFiles": "删除被回收文件失败",
|
||||||
"upload": "上传"
|
"upload": "上传",
|
||||||
}
|
"deleteMessage": "删除消息",
|
||||||
|
"deleteMessageConfirmation": "您确定要删除这条消息吗?",
|
||||||
|
"customReaction": "自定义反应",
|
||||||
|
"customReactions": "自定义反应",
|
||||||
|
"stickerPlaceholder": "贴图占位符",
|
||||||
|
"reactionAttitude": "反应属性",
|
||||||
|
"addReaction": "添加反应",
|
||||||
|
"eventCalendar": "活动日历",
|
||||||
|
"eventCalendarEmpty": "该日无活动。",
|
||||||
|
"walletStats": "钱包统计",
|
||||||
|
"totalTransactions": "交易总额",
|
||||||
|
"totalOrders": "总订单",
|
||||||
|
"totalIncome": "总收入",
|
||||||
|
"totalOutgoing": "总支出",
|
||||||
|
"netBalance": "净余额",
|
||||||
|
"messageUpdateLinks": "服务器生成的链接预览",
|
||||||
|
"messageUpdateEdited": "编辑一条消息",
|
||||||
|
"settingsCardBackgroundOpacity": "卡片背景不透明度",
|
||||||
|
"settingsThemeMode": "主题模式",
|
||||||
|
"settingsThemeModeSystem": "跟随系统",
|
||||||
|
"settingsThemeModeLight": "亮色",
|
||||||
|
"settingsThemeModeDark": "暗色",
|
||||||
|
"enterPin": "请输入您的PIN码",
|
||||||
|
"chatReplyingTo": "回复给 {}",
|
||||||
|
"chatForwarding": "正在转发消息",
|
||||||
|
"chatEditing": "编辑消息",
|
||||||
|
"chatNoContent": "没有内容",
|
||||||
|
"sensitiveCategories": {
|
||||||
|
"language": "语言",
|
||||||
|
"sexualContent": "色情内容",
|
||||||
|
"violence": "暴力",
|
||||||
|
"profanity": "亵渎",
|
||||||
|
"hateSpeech": "仇恨言论",
|
||||||
|
"racism": "种族主义",
|
||||||
|
"adultContent": "成人内容",
|
||||||
|
"drugAbuse": "药物滥用",
|
||||||
|
"alcoholAbuse": "酗酒",
|
||||||
|
"gambling": "赌博",
|
||||||
|
"selfHarm": "自残",
|
||||||
|
"childAbuse": "虐待儿童",
|
||||||
|
"other": "其他"
|
||||||
|
},
|
||||||
|
"Searching...": "搜索中……",
|
||||||
|
"searchError": "搜索失败,请重试。",
|
||||||
|
"tryDifferentKeywords": "尝试不同的关键字或删除搜索过滤器",
|
||||||
|
"settingsWindowOpacity": "窗口不透明度",
|
||||||
|
"messageContent": "消息内容",
|
||||||
|
"updateAvailable": "更新可用",
|
||||||
|
"noChangelogProvided": "没有提供更新日志。",
|
||||||
|
"useSecondarySourceForDownload": "使用次要源下载",
|
||||||
|
"installUpdate": "安装更新",
|
||||||
|
"openReleasePage": "打开发行页面",
|
||||||
|
"postCompose": "撰写帖子",
|
||||||
|
"postPublish": "发布帖子",
|
||||||
|
"restoreDraftTitle": "恢复草稿",
|
||||||
|
"restoreDraftMessage": "发现了一个草稿。你想要恢复它吗?",
|
||||||
|
"draft": "草稿",
|
||||||
|
"purchaseGift": "购买礼物",
|
||||||
|
"selectRecipient": "选择款件人",
|
||||||
|
"changeRecipient": "修改款件人",
|
||||||
|
"addMessage": "添加信息",
|
||||||
|
"skipRecipient": "跳过款件人",
|
||||||
|
"giftSubscriptions": "礼物订阅",
|
||||||
|
"purchaseAGift": "购买礼物",
|
||||||
|
"redeemAGift": "兑换礼物",
|
||||||
|
"giftHistory": "礼物历史",
|
||||||
|
"sentGifts": "发送礼物",
|
||||||
|
"receivedGifts": "接收礼物",
|
||||||
|
"noSentGifts": "没有发送过礼物",
|
||||||
|
"noReceivedGifts": "没有收到过礼物",
|
||||||
|
"stellarGift": "恒星订阅",
|
||||||
|
"novaGift": "新星订阅",
|
||||||
|
"supernovaGift": "超新星订阅",
|
||||||
|
"sameAsMembership": "与成员相同",
|
||||||
|
"enterGiftCodeToRedeem": "输入礼品代码以兑换",
|
||||||
|
"enterGiftCode": "输入礼物代码",
|
||||||
|
"giftPurchased": "已购买礼物!",
|
||||||
|
"shareCodeWithRecipient": "与收件人分享此代码来兑换礼物。",
|
||||||
|
"openGiftAnyoneCanRedeem": "这是一份任何人都可以兑换的公开礼物。",
|
||||||
|
"ok": "确定",
|
||||||
|
"selectedRecipient": "选定收件人",
|
||||||
|
"noRecipientSelected": "未选择收件人",
|
||||||
|
"thisWillBeAnOpenGift": "这将是一份公开的礼物",
|
||||||
|
"personalMessage": "个人消息",
|
||||||
|
"addPersonalMessageForRecipient": "为收件人添加个人消息",
|
||||||
|
"giftStatusCreated": "已创建",
|
||||||
|
"giftStatusSent": "发送",
|
||||||
|
"giftStatusRedeemed": "已兑换",
|
||||||
|
"giftStatusCancelled": "已取消",
|
||||||
|
"giftStatusExpired": "已过期",
|
||||||
|
"giftStatusUnknown": "未知",
|
||||||
|
"giftCodeCopiedToClipboard": "礼物代码已复制到剪贴板",
|
||||||
|
"codeLabel": "代码: ",
|
||||||
|
"subscriptionLabel": "订阅: ",
|
||||||
|
"toLabel": "发送至: ",
|
||||||
|
"fromLabel": "来自: ",
|
||||||
|
"messageLabel": "消息: ",
|
||||||
|
"giftRedeemed": "礼物兑换成功!",
|
||||||
|
"giftRedeemedSuccessfully": "您已成功兑换了礼物。您的新订阅现在已经生效。",
|
||||||
|
"cancelGift": "取消礼物",
|
||||||
|
"cancelGiftConfirm": "您确定要取消此礼物?此操作不能撤消。",
|
||||||
|
"giftCancelledSuccessfully": "已成功取消礼物",
|
||||||
|
"createFund": "创建红包",
|
||||||
|
"fundAmount": "红包金额",
|
||||||
|
"enterAmount": "输入金额",
|
||||||
|
"selectCurrency": "选择币种",
|
||||||
|
"splitType": "拆分类型",
|
||||||
|
"evenSplit": "平均分配",
|
||||||
|
"equalAmountEach": "每个收款人的金额相同",
|
||||||
|
"randomSplit": "随机分配",
|
||||||
|
"randomAmountEach": "每个收款人的金额随机",
|
||||||
|
"recipientCount": "收款人总计",
|
||||||
|
"numberOfRecipients": "收款人数量",
|
||||||
|
"addPersonalMessageForRecipients": "为收款人添加个人信息",
|
||||||
|
"invalidAmount": "无效的金额",
|
||||||
|
"invalidRecipientCount": "收款人数量无效",
|
||||||
|
"fundOverview": "红包概述",
|
||||||
|
"totalFundsSent": "已发送的红包总额",
|
||||||
|
"totalFundsReceived": "收到的红包总额",
|
||||||
|
"transactions": "交易",
|
||||||
|
"myFunds": "我的支票",
|
||||||
|
"availableFunds": "可用支票",
|
||||||
|
"fundStatusCreated": "已创建",
|
||||||
|
"fundStatusPartial": "部分领取",
|
||||||
|
"fundStatusCompleted": "已领完",
|
||||||
|
"fundStatusExpired": "已过期",
|
||||||
|
"fundStatusUnknown": "未知",
|
||||||
|
"recipients": "收款人",
|
||||||
|
"fundClaimedSuccessfully": "支票领取成功!",
|
||||||
|
"claim": "申请",
|
||||||
|
"noFundsCreated": "尚未创建任何支票",
|
||||||
|
"createYourFirstFund": "创建您的第一个支票来开始",
|
||||||
|
"noAvailableFunds": "暂无可用支票",
|
||||||
|
"fundsWillAppearHere": "您可以领取的支票将出现在这里",
|
||||||
|
"fundCreatedSuccessfully": "支票创建成功!",
|
||||||
|
"selectRecipients": "选择收款人",
|
||||||
|
"noRecipientsSelected": "尚未选择收款人",
|
||||||
|
"selectRecipientsToSendFund": "选择收款人将支票发送到",
|
||||||
|
"addRecipient": "添加收款人",
|
||||||
|
"addMoreRecipients": "添加更多收款人",
|
||||||
|
"transactionDetails": "交易详情",
|
||||||
|
"remarks": "备注",
|
||||||
|
"payer": "付款方",
|
||||||
|
"payee": "交易方",
|
||||||
|
"transactionType": "交易类型",
|
||||||
|
"transfer": "转账",
|
||||||
|
"payment": "支付",
|
||||||
|
"systemWallet": "中央统筹",
|
||||||
|
"date": "日期",
|
||||||
|
"createTransfer": "创建转账",
|
||||||
|
"transferAmount": "转账金额",
|
||||||
|
"selectPayee": "请选择收款人",
|
||||||
|
"selectedPayee": "选定的收款人",
|
||||||
|
"noPayeeSelected": "没有选择收款人",
|
||||||
|
"selectPayeeToTransfer": "选择要转账的收款人",
|
||||||
|
"addRemark": "添加备注",
|
||||||
|
"transferRemark": "转账备注",
|
||||||
|
"addRemarkForTransfer": "为转账添加备注",
|
||||||
|
"enterPinToConfirmTransfer": "输入您的 6 位 PIN 码以确认转账",
|
||||||
|
"transferCreatedSuccessfully": "转账成功创建!",
|
||||||
|
"postUpdate": "更新",
|
||||||
|
"fileMetadata": "文件元数据",
|
||||||
|
"resend": "重新发送",
|
||||||
|
"fileInfoTitle": "文件信息",
|
||||||
|
"download": "下载",
|
||||||
|
"info": "信息",
|
||||||
|
"noStickers": "无贴图",
|
||||||
|
"noStickersInPack": "这个包没有贴纸",
|
||||||
|
"noStickerPacks": "无贴图包",
|
||||||
|
"refresh": "刷新",
|
||||||
|
"spoiler": "已隐藏",
|
||||||
|
"activityHeatmap": "活动热力图",
|
||||||
|
"custom": "自定义",
|
||||||
|
"usernameColor": "用户名颜色",
|
||||||
|
"colorType": "颜色类型",
|
||||||
|
"plain": "纯色",
|
||||||
|
"gradient": "渐变",
|
||||||
|
"colorValue": "色值",
|
||||||
|
"gradientDirection": "渐变方向",
|
||||||
|
"gradientDirectionToRight": "向右",
|
||||||
|
"gradientDirectionToLeft": "向左",
|
||||||
|
"gradientDirectionToBottom": "向底部",
|
||||||
|
"gradientDirectionToTop": "向上",
|
||||||
|
"gradientDirectionToBottomRight": "向右下角",
|
||||||
|
"gradientDirectionToBottomLeft": "向左下角",
|
||||||
|
"gradientDirectionToTopRight": "向右上角",
|
||||||
|
"gradientDirectionToTopLeft": "向左下角",
|
||||||
|
"gradientColors": "渐变颜色",
|
||||||
|
"color": "颜色",
|
||||||
|
"addColor": "添加颜色",
|
||||||
|
"availableWithYourPlan": "适用于您的计划",
|
||||||
|
"upgradeRequired": "需要升级恒星计划等级",
|
||||||
|
"settingsDisableAnimation": "禁用动画",
|
||||||
|
"addTag": "添加标签",
|
||||||
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
|
"timezoneNotFound": "未找到时区",
|
||||||
|
"awardPoints": "收到 {} 点奖励",
|
||||||
|
"postFeaturedOn": "帖子在 {} 被精选",
|
||||||
|
"messageSentAt": "在 {} 发送",
|
||||||
|
"myTickets": "我的彩票",
|
||||||
|
"drawHistory": "抽奖历史",
|
||||||
|
"lottery": "彩票",
|
||||||
|
"noLotteryTickets": "暂无彩票",
|
||||||
|
"buyYourFirstTicket": "购买您的第一张彩票开始!",
|
||||||
|
"buyTicket": "购买彩票",
|
||||||
|
"ticketNumbers": "数字: {}, 特殊数字: {}",
|
||||||
|
"cost": "花费",
|
||||||
|
"multiplier": "倍率",
|
||||||
|
"prizeWon": "获奖者",
|
||||||
|
"pending": "准备中",
|
||||||
|
"drawn": "已开奖",
|
||||||
|
"won": "赢",
|
||||||
|
"lost": "输",
|
||||||
|
"noDrawHistory": "暂无开奖历史",
|
||||||
|
"buyLotteryTicket": "购买彩票",
|
||||||
|
"selectNumbers": "选择数字",
|
||||||
|
"select5UniqueNumbers": "选择 5 个唯一数字",
|
||||||
|
"selectSpecialNumber": "选择特殊数字",
|
||||||
|
"selectMultiplier": "选择倍数",
|
||||||
|
"baseCost": "基础花费",
|
||||||
|
"totalCost": "总费用",
|
||||||
|
"prizeStructure": "奖金分级",
|
||||||
|
"enterPinToConfirmPurchase": "输入您的 PIN 码以确认购买",
|
||||||
|
"ticketPurchasedSuccessfully": "彩票购买成功!",
|
||||||
|
"winningNumbers": "获胜数字",
|
||||||
|
"specialNumber": "特殊数字",
|
||||||
|
"totalTickets": "总售出票数",
|
||||||
|
"totalWinners": "中奖者总人数",
|
||||||
|
"prizePool": "奖金池",
|
||||||
|
"enterPinToConfirmPayment": "输入您的 PIN 码以确认付款",
|
||||||
|
"purchase": "购买",
|
||||||
|
"multiplierLabel": "倍率",
|
||||||
|
"specialOnly": "仅特殊的",
|
||||||
|
"matches": "场次",
|
||||||
|
"thoughtDefaultTopic": "寻思",
|
||||||
|
"thoughtAiName": "SN 酱",
|
||||||
|
"thoughtUserName": "您",
|
||||||
|
"thoughtStreamingHint": "SN 酱正在思考...",
|
||||||
|
"thoughtInputHint": "问 SN 酱任何问题...",
|
||||||
|
"thoughtNewConversation": "开始新对话",
|
||||||
|
"thoughtParseError": "解析 AI 响应失败",
|
||||||
|
"thoughtFunctionCall": "调用 {} 函数",
|
||||||
|
"aiThought": "寻思",
|
||||||
|
"aiThoughtTitle": "让 SN 酱寻思寻思",
|
||||||
|
"postReferenceUnavailable": "引用的帖子不可用",
|
||||||
|
"fabLocation": "底部导航按钮位置",
|
||||||
|
"activities": "活动",
|
||||||
|
"presenceTypeGaming": "正在玩",
|
||||||
|
"presenceTypeMusic": "正在听音乐",
|
||||||
|
"presenceTypeWorkout": "锻炼中",
|
||||||
|
"articleCompose": "撰写文章",
|
||||||
|
"backToHub": "返回至主页",
|
||||||
|
"advancedFilters": "高级筛选",
|
||||||
|
"searchPosts": "搜索帖子",
|
||||||
|
"sortBy": "排序方式",
|
||||||
|
"fromDate": "起始日期",
|
||||||
|
"toDate": "截止日期",
|
||||||
|
"popularity": "按热度",
|
||||||
|
"descendingOrder": "降序排序",
|
||||||
|
"selectDate": "选择日期",
|
||||||
|
"pinnedPosts": "已置顶的帖子",
|
||||||
|
"customReactionHint": "自定义反应允许你使用用户上传贴纸作为帖子反应的符号,需要恒星计划订阅。",
|
||||||
|
"publicationSites": "发布者站点",
|
||||||
|
"uploadTasks": "上传任务",
|
||||||
|
"thoughtFunctionCallBegin": "调用工具 {}",
|
||||||
|
"thoughtFunctionCallFinish": "工具 {} 响应",
|
||||||
|
"thoughtUnpaidHint": "寻思因为有未支付的订单而被禁用",
|
||||||
|
"more": "更多",
|
||||||
|
"collapse": "折叠",
|
||||||
|
"pollConfirmDiscard": "您确定要离开吗?您编辑的所有数据都不会被保存。",
|
||||||
|
"discard": "放弃",
|
||||||
|
"fund": "支票",
|
||||||
|
"fundsRecent": "最近支票",
|
||||||
|
"fundCreateNew": "创建新支票",
|
||||||
|
"fundCreateNewHint": "为您的消息创建一个新的红包。选择接收者和金额。",
|
||||||
|
"amountOfSplits": "份数",
|
||||||
|
"enterNumberOfSplits": "单份金额",
|
||||||
|
"orCreateWith": "或\n使用第三方帐户注册",
|
||||||
|
"unindexedFiles": "未索引的文件",
|
||||||
|
"folder": "文件夹",
|
||||||
|
"clearCompleted": "清除已完成的",
|
||||||
|
"uploadSuccess": "上传成功!",
|
||||||
|
"wouldYouLikeToViewFile": "预览此文件?",
|
||||||
|
"contentCantEmpty": "内容不能为空",
|
||||||
|
"features": "特征",
|
||||||
|
"unnamed": "未命名",
|
||||||
|
"fundEnvelopeLoadFailed": "加载支票信封失败",
|
||||||
|
"fundEnvelope": "支票信封",
|
||||||
|
"fundEnvelopeRemaining": "剩余:{} {}",
|
||||||
|
"fundEnvelopeSplit": "拆分:{}",
|
||||||
|
"fundEnvelopeSplitEvenly": "均分",
|
||||||
|
"fundEnvelopeSplitRandomly": "随机",
|
||||||
|
"fundEnvelopeClaimSuccess": "支票领取成功!",
|
||||||
|
"fundEnvelopeStatusCreated": "已创建",
|
||||||
|
"fundEnvelopeStatusPartial": "已领取部分",
|
||||||
|
"fundEnvelopeStatusCompleted": "已全部领取",
|
||||||
|
"fundEnvelopeStatusExpired": "已过期",
|
||||||
|
"fundEnvelopeStatusUnknown": "未知",
|
||||||
|
"fundEnvelopeRecipients": "收款人 ({}/{} 已领取)",
|
||||||
|
"fundEnvelopeExpiredDaysAgo": {
|
||||||
|
"one": "{} 天前过期",
|
||||||
|
"other": "{} 天前过期"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresSoon": "即将到期",
|
||||||
|
"fundEnvelopeExpiresInHours": {
|
||||||
|
"one": "{} 小时后到期",
|
||||||
|
"other": "{}小时后到期"
|
||||||
|
},
|
||||||
|
"fundEnvelopeExpiresInDays": {
|
||||||
|
"one": "{} 天后到期",
|
||||||
|
"other": "{} 天后到期"
|
||||||
|
},
|
||||||
|
"fundEnvelopeRemainingWithSplits": "{} {} / {} 份",
|
||||||
|
"fundEnvelopeUnknownUser": "未知用户",
|
||||||
|
"deleteSite": "删除网站",
|
||||||
|
"deleteSiteConfirm": "您确定要删除此网站?",
|
||||||
|
"siteDeletedSuccess": "网站成功删除",
|
||||||
|
"siteSlug": "标识符",
|
||||||
|
"siteSlugHint": "我的网站",
|
||||||
|
"siteSlugRequired": "请输入一个标识符",
|
||||||
|
"siteSlugInvalid": "标识符只能包含小写字母、数字和短横线",
|
||||||
|
"siteName": "网站名称",
|
||||||
|
"siteNameHint": "我的发布者网站",
|
||||||
|
"siteNameRequired": "请输入网站名称",
|
||||||
|
"siteMode": "模式",
|
||||||
|
"siteModeFullyManaged": "全托管",
|
||||||
|
"siteModeSelfManaged": "自托管",
|
||||||
|
"editPublicationSite": "编辑发布者网站",
|
||||||
|
"deletePublicationSite": "删除发布者网站",
|
||||||
|
"publicationSiteSavedSuccess": "发布者网站成功删除",
|
||||||
|
"publicationSiteDeleteConfirm": "您确定要删除该发布者网站吗?此操作不能撤销。",
|
||||||
|
"publicationSiteDeletedSuccess": "发布者网站成功删除",
|
||||||
|
"newPublicationSite": "新建发布者网站",
|
||||||
|
"siteDetails": "网站描述",
|
||||||
|
"siteInformation": "网站信息",
|
||||||
|
"siteDomain": "域名",
|
||||||
|
"siteCreated": "创建于",
|
||||||
|
"siteUpdated": "更新于",
|
||||||
|
"failedToLoadSite": "加载网站失败",
|
||||||
|
"sitePages": "页面",
|
||||||
|
"noPagesYet": "还没有页面",
|
||||||
|
"createFirstPage": "创建您的第一个页面以开始",
|
||||||
|
"failedToLoadPages": "加载页面失败",
|
||||||
|
"fileManagement": "文件管理",
|
||||||
|
"siteFiles": "文件",
|
||||||
|
"siteFolder": "文件夹",
|
||||||
|
"siteRoot": "根",
|
||||||
|
"noFilesUploadedYet": "还没有文件被删除",
|
||||||
|
"uploadFirstFile": "上传您的第一个文件以开始",
|
||||||
|
"failedToLoadFiles": "加载文件失败",
|
||||||
|
"noFilesFoundInFolder": "选择的文件夹里没有文件",
|
||||||
|
"fileActions": "文件选项",
|
||||||
|
"purgeFiles": "清除文件",
|
||||||
|
"purgeFilesDescription": "从这个网站删除全部文件",
|
||||||
|
"deploySite": "部署网站",
|
||||||
|
"deploySiteDescription": "从ZIP存档上传和部署新版本",
|
||||||
|
"confirmPurge": "确认清空",
|
||||||
|
"purgeFilesConfirm": "这将永久删除上传到本网站的所有文件。此操作无法撤销。您确定要继续吗?",
|
||||||
|
"purgeAllFiles": "清除所有文件",
|
||||||
|
"allFilesPurgedSuccess": "所有文件都清除成功",
|
||||||
|
"failedToPurgeFiles": "清除文件失败:{}",
|
||||||
|
"siteDeployedSuccess": "网站部署成功",
|
||||||
|
"failedToDeploySite": "部署网站失败:{}",
|
||||||
|
"createPage": "创建页面",
|
||||||
|
"editPage": "编辑页面",
|
||||||
|
"pageType": "页面类型",
|
||||||
|
"htmlPage": "HTML 页面",
|
||||||
|
"redirectPage": "重定向页面",
|
||||||
|
"pageTypeRequired": "请选择一个页面类型",
|
||||||
|
"pagePath": "页面路径",
|
||||||
|
"pagePathHint": "例如 /about, /contact 等。",
|
||||||
|
"pagePathRequired": "请输入一个页面路径",
|
||||||
|
"pagePathInvalid": "页面路径只能包含字母、数字、连字符、下划线和斜杠",
|
||||||
|
"pagePathMustStartWithSlash": "页面路径必须以 / 开头",
|
||||||
|
"pagePathNoConsecutiveSlashes": "页面路径不能有连续的斜杠",
|
||||||
|
"pageTitle": "页面标题",
|
||||||
|
"pageTitleHint": "例如关于我们,联系方式等。",
|
||||||
|
"pageTitleRequired": "请输入一个页面标题",
|
||||||
|
"pageContentHtml": "页面内容 (HTML)",
|
||||||
|
"pageContentHint": "<h1>Hello World</h1><p>这是我的页面内容…</p>",
|
||||||
|
"pageContentRequired": "请为这个页面输入HTML内容",
|
||||||
|
"redirectTarget": "重定向目标",
|
||||||
|
"redirectTargetHint": "例如 /new-page, https://example.com 等。",
|
||||||
|
"redirectTargetRequired": "请输入重定向目标",
|
||||||
|
"redirectTargetInvalid": "目标必须是相对路径 (/) 或绝对URL (http/https)",
|
||||||
|
"deletePage": "删除页面",
|
||||||
|
"deletePageConfirm": "您确定要删除此页面?",
|
||||||
|
"savePage": "保存页面",
|
||||||
|
"pageCreatedSuccess": "页面成功创建",
|
||||||
|
"pageUpdatedSuccess": "页面上传成功",
|
||||||
|
"pageDeletedSuccess": "页面已成功删除",
|
||||||
|
"uploadFiles": "上传文件",
|
||||||
|
"uploadPath": "上传路径",
|
||||||
|
"uploadPathHint": "/ (根) 或 /assets/images/",
|
||||||
|
"uploadPathRequired": "请输入一个上传路径",
|
||||||
|
"uploadPathMustStartWithSlash": "路径必须以/开头",
|
||||||
|
"uploadPathNoSpaces": "路径不能包含空格",
|
||||||
|
"uploadPathNoConsecutiveSlashes": "路径不能有连续的斜杠",
|
||||||
|
"percentCompleted": "{}% 已完成",
|
||||||
|
"filesToUpload": "{} 个文件已上传",
|
||||||
|
"fileSizeKb": "大小:{} KB",
|
||||||
|
"uploadingEllipsis": "上传中……",
|
||||||
|
"uploadFilesCount": {
|
||||||
|
"one": "上传 {} 个文件",
|
||||||
|
"other": "上传 {} 个文件"
|
||||||
|
},
|
||||||
|
"allUploadsCompleted": "所有文件已上传",
|
||||||
|
"someUploadsFailed": "一些上传失败",
|
||||||
|
"uploadingInProgress": "上传正在进行中",
|
||||||
|
"readyToUpload": "准备好上传",
|
||||||
|
"allFilesUploadedSuccess": "所有文件已成功上传",
|
||||||
|
"lotteryLastNumberSpecial": "最后选择的数字将是您的特殊数字。",
|
||||||
|
"lotteryMultiplierRequired": "请输入倍率",
|
||||||
|
"lotteryMultiplierRange": "倍率必须在 1 到 10 之间",
|
||||||
|
"dropToShare": "拖到此处以分享",
|
||||||
|
"affiliationSpell": "邀请码",
|
||||||
|
"affiliationSpellHint": "如果您有邀请码,请在这里输入。",
|
||||||
|
"friendsOnline": "在线好友",
|
||||||
|
"createAccountAlmostThere": "即将完成",
|
||||||
|
"createAccountAlmostThereHint": "您距离加入 Solar Network 只差一步了!请完成接下来显示的人机验证。",
|
||||||
|
"createAccountNotice": "在创建账户之前需要了解的事项:",
|
||||||
|
"createAccountConfirmEmail": "在账户创建后,您需要去邮箱激活您的账户,以获得使用所有功能的权限。",
|
||||||
|
"createAccountNoAltAccounts": "在 Solar Network 上禁止使用多个账户或替代账户,这将违反我们的服务条款。",
|
||||||
|
"createAccountAgreeTerms": "我已经阅读并同意这些服务协议。",
|
||||||
|
"createAccountProfile": "创建您的个人资料",
|
||||||
|
"createAccountToS": "查看用户协议 & 服务条款",
|
||||||
|
"updateYourProfileDescription": "在 Solar Network 上完善你的个人形象。",
|
||||||
|
"realmsDescription": "管理您已加入的领域。",
|
||||||
|
"exploreDescription": "探索 Solar Network 上的内容。",
|
||||||
|
"accountDescription": "关于您账户的信息。",
|
||||||
|
"chatDescription": "群组聊天与私聊",
|
||||||
|
"connectionServerDown": "无法连接",
|
||||||
|
"appSettingsDescription": "自定义您的应用程序。",
|
||||||
|
"accountSettingsDescription": "管理您在 Solar Network 上的偏好设置。",
|
||||||
|
"walletDescription": "您的源点钱包。",
|
||||||
|
"relationshipsDescription": "好友与联系人。",
|
||||||
|
"notificationsDescription": "查看最近与您相关的事情。",
|
||||||
|
"settingsFestivalFeatures": "节日限定功能",
|
||||||
|
"categoriesAndTags": "类别 & 标签",
|
||||||
|
"webArticlesStandDescription": "浏览外部网站文章。",
|
||||||
|
"aboutDescription": "了解更多有关 Solar Network 的信息。",
|
||||||
|
"abuseReportsDescription": "查看并管理滥用报告。",
|
||||||
|
"stickerMarketplaceDescription": "浏览并从 Solar Network 市场添加贴纸包。",
|
||||||
|
"webFeedsDescription": "浏览并订阅 Solar Network 的网页订阅源。",
|
||||||
|
"discoverRealmsDescription": "发现领域并加入它们。",
|
||||||
|
"postShuffleDescription": "打乱帖子以随机查看帖子。",
|
||||||
|
"levelingDescription": "查看您的升级进度和历史记录。",
|
||||||
|
"notableDayToday": "{} 就是今天!",
|
||||||
|
"authSessionLogout": "注销登录",
|
||||||
|
"authSessionLogoutHint": "确认要退出登录吗?此操作将仅结束当前浏览会话,不会影响其他已登录的设备。",
|
||||||
|
"filesDescription": "在 Solar Network Drive 管理您的文件。",
|
||||||
|
"postComposeDescription": "撰写一篇新帖子",
|
||||||
|
"searchPostsDescription": "输入标题、正文或关键词进行搜索。",
|
||||||
|
"accountActivationAlert": "激活您的账号",
|
||||||
|
"accountActivationAlertHint": "账户未激活将无法使用完整功能,请查收邮件并点击链接完成激活。",
|
||||||
|
"accountActivationResendHint": "没收到邮件?请点击下方按钮重新发送。若需在账户未激活时更新邮箱,请联系我们的客服。",
|
||||||
|
"accountActivationResend": "重新发送",
|
||||||
|
"ipAddress": "IP 地址",
|
||||||
|
"noFurtherData": "没有更多数据",
|
||||||
|
"searchAnything": "搜索任何内容……",
|
||||||
|
"tapToViewAllNotifications": "轻点显示所有通知",
|
||||||
|
"mostRecent": "最近",
|
||||||
|
"noNotificationsYet": "没有通知",
|
||||||
|
"recentChats": "最近聊天",
|
||||||
|
"noFeaturedPostsAvailable": "没有可用的精选文章",
|
||||||
|
"searchChatsAndPages": "搜索聊天或页面……",
|
||||||
|
"dashboard": "仪表板",
|
||||||
|
"dashboardDescription": "在一处整合您的所有数据。",
|
||||||
|
"postTagsCategories": "文章标签和分类",
|
||||||
|
"postTagsCategoriesDescription": "按类别和标签浏览帖子。",
|
||||||
|
"debugLogs": "调试日志",
|
||||||
|
"debugLogsDescription": "查看调试日志以进行故障排除。",
|
||||||
|
"pinChatRoom": "置顶聊天室",
|
||||||
|
"pinChatRoomDescription": "将此聊天室固定到顶部。",
|
||||||
|
"chatRoomPinned": "聊天室已置顶。",
|
||||||
|
"chatRoomUnpinned": "聊天室已取消置顶。",
|
||||||
|
"pinnedChatRoom": "已置顶的聊天室",
|
||||||
|
"settingsGroupedChatList": "启用群组聊天列表分组",
|
||||||
|
"settingsNotifyWithHaptic": "通知触感反馈",
|
||||||
|
"settingsDashSearchEngine": "网络搜索引擎",
|
||||||
|
"settingsDashSearchEngineHelper": "请使用 %s 作为搜索词的占位符。",
|
||||||
|
"settingsDefaultScreen": "默认起始页面",
|
||||||
|
"notableDayChristmas": "圣诞节",
|
||||||
|
"notableDayNewYear": "新年"
|
||||||
|
}
|
||||||
BIN
assets/icons/icon-tray.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/images/oidc/spotify.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
1
assets/images/oidc/steam.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="2471" height="2500" viewBox="0 0 256 259" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M127.779 0C60.42 0 5.24 52.412 0 119.014l68.724 28.674a35.812 35.812 0 0 1 20.426-6.366c.682 0 1.356.019 2.02.056l30.566-44.71v-.626c0-26.903 21.69-48.796 48.353-48.796 26.662 0 48.352 21.893 48.352 48.796 0 26.902-21.69 48.804-48.352 48.804-.37 0-.73-.009-1.098-.018l-43.593 31.377c.028.582.046 1.163.046 1.735 0 20.204-16.283 36.636-36.294 36.636-17.566 0-32.263-12.658-35.584-29.412L4.41 164.654c15.223 54.313 64.673 94.132 123.369 94.132 70.818 0 128.221-57.938 128.221-129.393C256 57.93 198.597 0 127.779 0zM80.352 196.332l-15.749-6.568c2.787 5.867 7.621 10.775 14.033 13.47 13.857 5.83 29.836-.803 35.612-14.799a27.555 27.555 0 0 0 .046-21.035c-2.768-6.79-7.999-12.086-14.706-14.909-6.67-2.795-13.811-2.694-20.085-.304l16.275 6.79c10.222 4.3 15.056 16.145 10.794 26.46-4.253 10.314-15.998 15.195-26.22 10.895zm121.957-100.29c0-17.925-14.457-32.52-32.217-32.52-17.769 0-32.226 14.595-32.226 32.52 0 17.926 14.457 32.512 32.226 32.512 17.76 0 32.217-14.586 32.217-32.512zm-56.37-.055c0-13.488 10.84-24.42 24.2-24.42 13.368 0 24.208 10.932 24.208 24.42 0 13.488-10.84 24.421-24.209 24.421-13.359 0-24.2-10.933-24.2-24.42z" fill="#1A1918"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +1,6 @@
|
|||||||
description: This file stores settings for Dart & Flutter DevTools.
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
extensions:
|
extensions:
|
||||||
|
- drift: true
|
||||||
|
- provider: true
|
||||||
|
- shared_preferences: true
|
||||||
287
docs/activitypub/ACTIVITYPUB_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
# ActivityPub Implementation for Solar Network
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the initial implementation of ActivityPub federation for the Solar Network (DysonNetwork), following the plan outlined in `ACTIVITYPUB_PLAN.md`.
|
||||||
|
|
||||||
|
## What Has Been Created
|
||||||
|
|
||||||
|
### 1. Database Models (DysonNetwork.Shared/Models)
|
||||||
|
|
||||||
|
All ActivityPub-related models are shared across projects and located in `DysonNetwork.Shared/Models/`:
|
||||||
|
|
||||||
|
#### FediverseInstance.cs
|
||||||
|
- Tracks ActivityPub instances (servers) in the fediverse
|
||||||
|
- Stores instance metadata, blocking status, and activity tracking
|
||||||
|
- Links to actors and content from that instance
|
||||||
|
|
||||||
|
#### FediverseActor.cs
|
||||||
|
- Represents remote actors (users/accounts) from other instances
|
||||||
|
- Stores actor information including keys, inbox/outbox URLs
|
||||||
|
- Links to instance and manages relationships
|
||||||
|
- Tracks whether the actor is a bot, locked, or discoverable
|
||||||
|
|
||||||
|
#### FediverseContent.cs
|
||||||
|
- Stores content (posts, notes, etc.) received from the fediverse
|
||||||
|
- Supports multiple content types (Note, Article, Image, Video, etc.)
|
||||||
|
- Includes attachments, mentions, tags, and emojis
|
||||||
|
- Links to local posts for unified display
|
||||||
|
|
||||||
|
#### FediverseActivity.cs
|
||||||
|
- Tracks ActivityPub activities (Create, Follow, Like, Announce, etc.)
|
||||||
|
- Stores raw activity data and processing status
|
||||||
|
- Links to actors, content, and local entities
|
||||||
|
- Supports both incoming and outgoing activities
|
||||||
|
|
||||||
|
#### FediverseRelationship.cs
|
||||||
|
- Manages follow relationships between local and remote actors
|
||||||
|
- Tracks relationship state (Pending, Accepted, Rejected)
|
||||||
|
- Supports muting and blocking
|
||||||
|
- Links to local accounts/publishers
|
||||||
|
|
||||||
|
#### FediverseReaction.cs
|
||||||
|
- Stores reactions (likes, emoji) from both local and remote actors
|
||||||
|
- Links to content and actor
|
||||||
|
- Supports federation of reactions
|
||||||
|
|
||||||
|
### 2. Database Migration
|
||||||
|
|
||||||
|
**File**: `DysonNetwork.Sphere/Migrations/20251228120000_AddActivityPubModels.cs`
|
||||||
|
|
||||||
|
This migration creates the following tables:
|
||||||
|
- `fediverse_instances` - Instance tracking
|
||||||
|
- `fediverse_actors` - Remote actor profiles
|
||||||
|
- `fediverse_contents` - Federated content storage
|
||||||
|
- `fediverse_activities` - Activity tracking and processing
|
||||||
|
- `fediverse_relationships` - Follow relationships
|
||||||
|
- `fediverse_reactions` - Reactions from fediverse
|
||||||
|
|
||||||
|
### 3. API Controllers (DysonNetwork.Sphere/ActivityPub)
|
||||||
|
|
||||||
|
#### WebFingerController.cs
|
||||||
|
- **Endpoint**: `GET /.well-known/webfinger?resource=acct:<username>@<domain>`
|
||||||
|
- **Purpose**: Allows other instances to discover actors via WebFinger protocol
|
||||||
|
- **Response**: Returns actor's inbox/outbox URLs and profile page links
|
||||||
|
- Maps local Publishers to ActivityPub actors
|
||||||
|
|
||||||
|
#### ActivityPubController.cs
|
||||||
|
Provides three main endpoints:
|
||||||
|
|
||||||
|
1. **GET /activitypub/actors/{username}**
|
||||||
|
- Returns ActivityPub actor profile in JSON-LD format
|
||||||
|
- Includes actor's keys, inbox, outbox, followers, and following URLs
|
||||||
|
- Maps SnPublisher to ActivityPub Person type
|
||||||
|
|
||||||
|
2. **GET /activitypub/actors/{username}/outbox**
|
||||||
|
- Returns actor's outbox collection
|
||||||
|
- Lists public posts as ActivityPub activities
|
||||||
|
- Supports pagination
|
||||||
|
|
||||||
|
3. **POST /activitypub/actors/{username}/inbox**
|
||||||
|
- Receives incoming ActivityPub activities
|
||||||
|
- Supports Create, Follow, Like, Announce activities
|
||||||
|
- Placeholder for activity processing logic
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Remote Instance Solar Network (Sphere)
|
||||||
|
│ │
|
||||||
|
│ ───WebFinger─────> │
|
||||||
|
│ │
|
||||||
|
│ <───Actor JSON──── │
|
||||||
|
│ │
|
||||||
|
│ ───Activity─────> │ → Inbox Processing
|
||||||
|
│ │
|
||||||
|
│ <───Activity────── │ ← Outbox Distribution
|
||||||
|
```
|
||||||
|
|
||||||
|
### Model Relationships
|
||||||
|
|
||||||
|
- `SnFediverseInstance` has many `SnFediverseActor`
|
||||||
|
- `SnFediverseInstance` has many `SnFediverseContent`
|
||||||
|
- `SnFediverseActor` has many `SnFediverseContent`
|
||||||
|
- `SnFediverseActor` has many `SnFediverseActivity`
|
||||||
|
- `SnFediverseActor` has many `SnFediverseRelationship` (as follower and following)
|
||||||
|
- `SnFediverseContent` has many `SnFediverseActivity`
|
||||||
|
- `SnFediverseContent` has many `SnFediverseReaction`
|
||||||
|
- `SnFediverseContent` optionally links to `SnPost` (local copy)
|
||||||
|
|
||||||
|
### Local to Fediverse Mapping
|
||||||
|
|
||||||
|
| Solar Network Model | ActivityPub Type |
|
||||||
|
|-------------------|-----------------|
|
||||||
|
| SnPublisher | Person (Actor) |
|
||||||
|
| SnPost | Note / Article |
|
||||||
|
| SnPostReaction | Like / EmojiReact |
|
||||||
|
| Follow | Follow Activity |
|
||||||
|
| SnPublisherSubscription | Follow Relationship |
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Stage 1: Core Infrastructure ✅ (COMPLETED)
|
||||||
|
- ✅ Create database models for ActivityPub entities
|
||||||
|
- ✅ Create database migration
|
||||||
|
- ✅ Implement basic WebFinger endpoint
|
||||||
|
- ✅ Implement basic Actor endpoint
|
||||||
|
- ✅ Implement Inbox/Outbox endpoints
|
||||||
|
|
||||||
|
### Stage 2: Activity Processing ✅ (COMPLETED)
|
||||||
|
- ✅ Implement HTTP Signature verification (ActivityPubSignatureService)
|
||||||
|
- ✅ Process incoming activities:
|
||||||
|
- Follow/Accept/Reject
|
||||||
|
- Create (incoming posts)
|
||||||
|
- Like/Announce
|
||||||
|
- Delete/Update
|
||||||
|
- Undo
|
||||||
|
- ✅ Generate outgoing activities (ActivityPubDeliveryService)
|
||||||
|
- ✅ Queue and retry failed deliveries (basic implementation)
|
||||||
|
|
||||||
|
### Stage 3: Key Management ✅ (COMPLETED)
|
||||||
|
- ✅ Generate RSA key pairs for each Publisher (ActivityPubKeyService)
|
||||||
|
- ✅ Store public/private keys in Publisher.Meta
|
||||||
|
- ✅ Sign outgoing HTTP requests
|
||||||
|
- ✅ Verify incoming HTTP signatures
|
||||||
|
|
||||||
|
### Stage 4: Content Federation (IN PROGRESS)
|
||||||
|
- ✅ Convert between SnPost and ActivityPub Note/Article (basic mapping)
|
||||||
|
- ✅ Handle content attachments and media
|
||||||
|
- ✅ Support content warnings and sensitive content
|
||||||
|
- ✅ Handle replies, boosts, and mentions
|
||||||
|
- ⏳ Add local post reference for federated content
|
||||||
|
- ⏳ Handle media attachments in federated content
|
||||||
|
|
||||||
|
### Stage 5: Relationship Management ✅ (COMPLETED)
|
||||||
|
- ✅ Handle follow/unfollow logic
|
||||||
|
- ✅ Update followers/following collections
|
||||||
|
- ✅ Block/mute functionality (data model ready)
|
||||||
|
- ✅ Relationship state machine (Pending, Accepted, Rejected)
|
||||||
|
|
||||||
|
### Stage 6: Testing & Interop (NEXT)
|
||||||
|
- ⏳ Test with Mastodon instances
|
||||||
|
- ⏳ Test with Pleroma/Akkoma instances
|
||||||
|
- ⏳ Test with Lemmy instances
|
||||||
|
- ⏳ Verify WebFinger and actor discovery
|
||||||
|
- ⏳ Test activity delivery and processing
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Core Services
|
||||||
|
|
||||||
|
#### 1. ActivityPubKeyService
|
||||||
|
- Generates RSA 2048-bit key pairs for ActivityPub
|
||||||
|
- Signs data with private key
|
||||||
|
- Verifies signatures with public key
|
||||||
|
- Key stored in `SnPublisher.Meta["private_key"]` and `["public_key"]`
|
||||||
|
|
||||||
|
#### 2. ActivityPubSignatureService
|
||||||
|
- Verifies incoming HTTP Signature headers
|
||||||
|
- Signs outgoing HTTP requests
|
||||||
|
- Manages key retrieval and storage
|
||||||
|
- Builds signing strings according to ActivityPub spec
|
||||||
|
|
||||||
|
#### 3. ActivityPubActivityProcessor
|
||||||
|
- Processes all incoming activity types
|
||||||
|
- Follow: Creates relationship, sends Accept
|
||||||
|
- Accept: Updates relationship to accepted state
|
||||||
|
- Reject: Updates relationship to rejected state
|
||||||
|
- Create: Stores federated content
|
||||||
|
- Like: Records like reaction
|
||||||
|
- Announce: Increments boost count
|
||||||
|
- Undo: Reverts previous actions
|
||||||
|
- Delete: Soft-deletes federated content
|
||||||
|
- Update: Marks content as edited
|
||||||
|
|
||||||
|
#### 4. ActivityPubDeliveryService
|
||||||
|
- Sends Follow activities to remote instances
|
||||||
|
- Sends Accept activities in response to follows
|
||||||
|
- Sends Create activities (posts) to followers
|
||||||
|
- Sends Like activities to remote instances
|
||||||
|
- Sends Undo activities
|
||||||
|
- Fetches remote actor profiles on-demand
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
#### Incoming Activity Flow
|
||||||
|
```
|
||||||
|
Remote Server → HTTP Signature Verification → Activity Type → Specific Handler
|
||||||
|
↓
|
||||||
|
Database Update & Response
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Outgoing Activity Flow
|
||||||
|
```
|
||||||
|
Local Action → Create Activity → Sign with Key → Send to Followers' Inboxes
|
||||||
|
↓
|
||||||
|
Track Status & Retry
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add to `appsettings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ActivityPub": {
|
||||||
|
"Domain": "your-domain.com",
|
||||||
|
"EnableFederation": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Migration
|
||||||
|
|
||||||
|
To apply the migration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd DysonNetwork.Sphere
|
||||||
|
dotnet ef database update
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### WebFinger
|
||||||
|
```bash
|
||||||
|
curl "https://your-domain.com/.well-known/webfinger?resource=acct:username@your-domain.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actor Profile
|
||||||
|
```bash
|
||||||
|
curl -H "Accept: application/activity+json" https://your-domain.com/activitypub/actors/username
|
||||||
|
```
|
||||||
|
|
||||||
|
### Outbox
|
||||||
|
```bash
|
||||||
|
curl -H "Accept: application/activity+json" https://your-domain.com/activitypub/actors/username/outbox
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All models follow the existing Solar Network patterns (ModelBase, NodaTime, JSON columns)
|
||||||
|
- Controllers use standard ASP.NET Core patterns with dependency injection
|
||||||
|
- Database uses PostgreSQL with JSONB for flexible metadata storage
|
||||||
|
- Migration follows existing naming conventions
|
||||||
|
- Soft delete is enabled on all models
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ActivityPub W3C Recommendation](https://www.w3.org/TR/activitypub/)
|
||||||
|
- [ActivityStreams 2.0](https://www.w3.org/TR/activitystreams-core/)
|
||||||
|
- [WebFinger RFC 7033](https://tools.ietf.org/html/rfc7033)
|
||||||
|
- [Mastodon Federation Documentation](https://docs.joinmastodon.org/spec/activitypub/)
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [ ] Implement HTTP Signature verification middleware
|
||||||
|
- [ ] Create activity processor service
|
||||||
|
- [ ] Implement activity queue and retry logic
|
||||||
|
- [ ] Add key generation for Publishers
|
||||||
|
- [ ] Implement content conversion between formats
|
||||||
|
- [ ] Add inbox background worker
|
||||||
|
- [ ] Add outbox delivery worker
|
||||||
|
- [ ] Implement relationship management logic
|
||||||
|
- [ ] Add moderation tools for federated content
|
||||||
|
- [ ] Add federation metrics and monitoring
|
||||||
|
- [ ] Write comprehensive tests
|
||||||
197
docs/activitypub/ACTIVITYPUB_PLAN.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
🛠️ ActivityPub 接入 Solar Network 的分步清单
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
🧱 1. 准备 & 设计阶段
|
||||||
|
|
||||||
|
1.1 理解 ActivityPub 的核心概念
|
||||||
|
• Actor / Object / Activity / Collection
|
||||||
|
• Outbox / Inbox / Followers 列表
|
||||||
|
ActivityPub 是使用 JSON-LD + ActivityStreams 2.0 来描述社交行为的规范。 
|
||||||
|
|
||||||
|
1.2 映射你现有的 Solar Domain 结构
|
||||||
|
|
||||||
|
把你现在 Solar Network 的用户、帖子、关注、点赞等:
|
||||||
|
• 映射为 ActivityPub 的 Actor / Note / Follow / Like 等
|
||||||
|
• 明确本地模型与 ActivityStreams 对应关系
|
||||||
|
|
||||||
|
比如:
|
||||||
|
• Solar User → ActivityPub Actor
|
||||||
|
• Post → ActivityPub Note/Object
|
||||||
|
• Like → ActivityPub Like Activity
|
||||||
|
这一步是关键的领域建模设计。
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
🚪 2. Actor 发现与必要入口
|
||||||
|
|
||||||
|
2.1 实现 WebFinger
|
||||||
|
|
||||||
|
为每个用户提供 WebFinger endpoint:
|
||||||
|
|
||||||
|
GET /.well-known/webfinger?resource=acct:<username>@<domain>
|
||||||
|
|
||||||
|
用来让远端服务器查出 actor 细节(包括 inbox/outbox URL)。
|
||||||
|
|
||||||
|
2.2 Actor 资源 URL
|
||||||
|
|
||||||
|
确保每个用户有一个全局可访问的 URL,例如:
|
||||||
|
|
||||||
|
https://solar.io/users/alice
|
||||||
|
|
||||||
|
并在其 JSON-LD 中包含:
|
||||||
|
• inbox
|
||||||
|
• outbox
|
||||||
|
• followers
|
||||||
|
• following
|
||||||
|
这些是 ActivityPub 基础通信的入口。 
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
📮 3. 核心协议实现
|
||||||
|
|
||||||
|
3.1 Inbox / Outbox 接口
|
||||||
|
|
||||||
|
Inbox(接收来自其他实例的 Activity)
|
||||||
|
Outbox(本地用户发布 Activity 的出口)
|
||||||
|
|
||||||
|
Outbox 需要:
|
||||||
|
• 生成 activity JSON(Create、Follow、Like 等)
|
||||||
|
• 存储至本地数据库
|
||||||
|
• 推送到各 follower 的 Inbox
|
||||||
|
|
||||||
|
Inbox 需要:
|
||||||
|
• 接收并 parse Activity
|
||||||
|
• 验证签名
|
||||||
|
• 处理活动(如接受 Follow,记录远程 Post 等)
|
||||||
|
|
||||||
|
注意:
|
||||||
|
• 请求需要验证 HTTP Signatures(远端服务器签名)。 
|
||||||
|
• 必须满足 ActivityPub 规范对字段的要求。
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
🔐 4. 安全与签名
|
||||||
|
|
||||||
|
4.1 Actor Keys
|
||||||
|
|
||||||
|
每个 Actor 对应一对 RSA / Ed25519 密钥:
|
||||||
|
• 私钥用于签名发送到其它服务器的请求
|
||||||
|
• 公钥发布在 Actor JSON 中供对方验证
|
||||||
|
|
||||||
|
远端服务器发送到你的 Inbox 时,需要:
|
||||||
|
• 使用对方的公钥验证签名
|
||||||
|
|
||||||
|
HTTP Signatures 是服务器间通信安全的一部分,防止伪造请求。 
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
🌐 5. 实现联邦逻辑
|
||||||
|
|
||||||
|
5.1 关注逻辑
|
||||||
|
|
||||||
|
处理:
|
||||||
|
• Follow Activity
|
||||||
|
• Accept / Reject Activity
|
||||||
|
• 更新本地 followers / following 数据
|
||||||
|
|
||||||
|
实现流程参考:1. 本地用户发起 Follow 2. 推送 Follow 到远端 Inbox 3. 等待远端发送 Accept 或 Reject
|
||||||
|
|
||||||
|
5.2 推送 content(联邦同步)
|
||||||
|
|
||||||
|
当本地用户发布内容时:
|
||||||
|
• 从 Outbox 取出 Create Activity
|
||||||
|
• 发送到所有远端 followers 的 Inbox
|
||||||
|
注意:你可以缓存远端 followers 数据表来减少重复请求。
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
📡 6. 消息处理与存储
|
||||||
|
|
||||||
|
6.1 本地对象缓存
|
||||||
|
|
||||||
|
对于接收到的远端内容(Post / Note / Like 等):
|
||||||
|
• 需要保存到 Solar 的数据库
|
||||||
|
• 供 UI / API 生成用户时间线
|
||||||
|
这使得 Solar 能把远端联邦内容与本地内容统一展示。
|
||||||
|
|
||||||
|
6.2 处理 Collections
|
||||||
|
|
||||||
|
ActivityPub 定义了 Collection 类型用于:
|
||||||
|
• followers 列表
|
||||||
|
• liked 列表
|
||||||
|
• outbox、inbox
|
||||||
|
|
||||||
|
你需要实现这些集合的获取与分页逻辑。
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
🔁 7. 与现有 Solar Network API 协调
|
||||||
|
|
||||||
|
你可能已经有本地的帖子、用户 API。那么:
|
||||||
|
• 把这套 API 与 ActivityPub 同步层绑定
|
||||||
|
• 决定哪些内容对外发布
|
||||||
|
• 决定哪些 Activity 类型需要响应
|
||||||
|
|
||||||
|
比如:
|
||||||
|
|
||||||
|
Solar Post Create -> 生成 ActivityPub Create Note -> 发往联邦
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
📦 8. 测试与兼容性
|
||||||
|
|
||||||
|
8.1 与现存联邦测试
|
||||||
|
|
||||||
|
用已存在的 ActivityPub 实例测试兼容性:
|
||||||
|
• Mastodon
|
||||||
|
• Pleroma
|
||||||
|
• Lemmy 等
|
||||||
|
|
||||||
|
检查:
|
||||||
|
• 对方是否能关注 Solar 用户
|
||||||
|
• Solar 是否能接收远端内容
|
||||||
|
|
||||||
|
ActivityPub 规范(W3C Recommendation)有详细规范流包括:
|
||||||
|
• Server to Server API
|
||||||
|
你最重要的目标是与现存实例互操作。 
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
🧪 9. UX & 监控支持
|
||||||
|
|
||||||
|
9.1 用户显示远端内容
|
||||||
|
|
||||||
|
从 Inbox 收到内容后:
|
||||||
|
• 如何展示在 Solar UI
|
||||||
|
• 链接远端用户的展示名 / 头像
|
||||||
|
|
||||||
|
9.2 监控 & 审计
|
||||||
|
• 失败的推送
|
||||||
|
• 无法验证签名的请求
|
||||||
|
• 阻止 spam / 恶意 Activity
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
🏁 10. 逐步推进
|
||||||
|
|
||||||
|
建议按阶段 rollout:
|
||||||
|
|
||||||
|
阶段 目标
|
||||||
|
Stage 1 实现 Actor / WebFinger / Outbox / Inbox 基本框架
|
||||||
|
Stage 2 支持 Follow / Accept / Reject Activity
|
||||||
|
Stage 3 支持 Create / Like / Announce
|
||||||
|
Stage 4 与远端实例互联测试
|
||||||
|
Stage 5 UI & Feed 统一显示本地 + 联邦内容
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
📌 小结
|
||||||
|
|
||||||
|
核心步骤总结:1. 映射 Solar Network 数据模型到 ActivityPub 2. 实现 WebFinger + Actor JSON-LD 3. 实现 Inbox 和 Outbox endpoints 4. 管理 Actor Keys 与 HTTP Signatures 5. 处理关注/发帖/点赞等 Activity 6. 推送到远端 / 接收远端同步 7. 将远端内容存入 Solar 并展示 8. 测试与现有 Fediverse 实例互通
|
||||||
|
|
||||||
|
这套步骤覆盖了 ActivityPub 协议必须实现的点和实际联邦要处理的逻辑。 
|
||||||
|
|
||||||
|
⸻
|
||||||
|
|
||||||
|
如果你想,我可以进一步展开 Solar Network 对应的具体 API 设计模板(包括 Inbox / Outbox 的 REST 定义与 JSON 输出示例),甚至帮你写 可运行的 Go / .NET 样例代码。你希望从哪一部分开始深入?
|
||||||
273
docs/activitypub/ACTIVITYPUB_SUMMARY.md
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
# ActivityPub Implementation Summary
|
||||||
|
|
||||||
|
## What Has Been Implemented
|
||||||
|
|
||||||
|
### 1. Database Models ✅
|
||||||
|
All models located in `DysonNetwork.Shared/Models/`:
|
||||||
|
|
||||||
|
| Model | Purpose | Key Features |
|
||||||
|
|--------|---------|--------------|
|
||||||
|
| `SnFediverseInstance` | Track fediverse servers | Domain blocking, metadata, activity tracking |
|
||||||
|
| `SnFediverseActor` | Remote user profiles | Keys, inbox/outbox URLs, relationships |
|
||||||
|
| `SnFediverseContent` | Federated posts/notes | Multiple content types, attachments, mentions, tags |
|
||||||
|
| `SnFediverseActivity` | Activity tracking | All activity types, processing status, raw data |
|
||||||
|
| `SnFediverseRelationship` | Follow relationships | State machine, muting/blocking |
|
||||||
|
| `SnFediverseReaction` | Federated reactions | Likes, emoji reactions |
|
||||||
|
|
||||||
|
### 2. Database Migrations ✅
|
||||||
|
- `20251228120000_AddActivityPubModels.cs` - Core ActivityPub tables
|
||||||
|
- `20251228130000_AddPublisherMetaForActivityPubKeys.cs` - Publisher metadata for keys
|
||||||
|
|
||||||
|
### 3. Core Services ✅
|
||||||
|
|
||||||
|
#### ActivityPubKeyService
|
||||||
|
- **Location**: `DysonNetwork.Sphere/ActivityPub/ActivityPubKeyService.cs`
|
||||||
|
- **Responsibilities**:
|
||||||
|
- Generate RSA 2048-bit key pairs
|
||||||
|
- Sign data with private key
|
||||||
|
- Verify signatures with public key
|
||||||
|
- **Key Storage**: Keys stored in `SnPublisher.Meta`
|
||||||
|
|
||||||
|
#### ActivityPubSignatureService
|
||||||
|
- **Location**: `DysonNetwork.Sphere/ActivityPub/ActivityPubSignatureService.cs`
|
||||||
|
- **Responsibilities**:
|
||||||
|
- Verify incoming HTTP Signature headers
|
||||||
|
- Sign outgoing HTTP requests
|
||||||
|
- Build signing strings per ActivityPub spec
|
||||||
|
- Manage key retrieval for actors
|
||||||
|
- **Signature Algorithm**: RSA-SHA256
|
||||||
|
|
||||||
|
#### ActivityPubActivityProcessor
|
||||||
|
- **Location**: `DysonNetwork.Sphere/ActivityPub/ActivityPubActivityProcessor.cs`
|
||||||
|
- **Supported Activities**:
|
||||||
|
- ✅ Follow - Creates relationship, sends Accept
|
||||||
|
- ✅ Accept - Updates relationship to accepted
|
||||||
|
- ✅ Reject - Updates relationship to rejected
|
||||||
|
- ✅ Create - Stores federated content
|
||||||
|
- ✅ Like - Records like reaction
|
||||||
|
- ✅ Announce - Increments boost count
|
||||||
|
- ✅ Undo - Reverts previous actions
|
||||||
|
- ✅ Delete - Soft-deletes federated content
|
||||||
|
- ✅ Update - Marks content as edited
|
||||||
|
|
||||||
|
#### ActivityPubDeliveryService
|
||||||
|
- **Location**: `DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs`
|
||||||
|
- **Outgoing Activities**:
|
||||||
|
- ✅ Follow - Send to remote actors
|
||||||
|
- ✅ Accept - Respond to follow requests
|
||||||
|
- ✅ Create - Send new posts to followers
|
||||||
|
- ✅ Like - Send to remote instances
|
||||||
|
- ✅ Undo - Undo previous actions
|
||||||
|
- **Features**:
|
||||||
|
- HTTP signature signing
|
||||||
|
- Remote actor fetching
|
||||||
|
- Follower discovery
|
||||||
|
|
||||||
|
### 4. API Controllers ✅
|
||||||
|
|
||||||
|
#### WebFingerController
|
||||||
|
- **Location**: `DysonNetwork.Sphere/ActivityPub/WebFingerController.cs`
|
||||||
|
- **Endpoints**:
|
||||||
|
- `GET /.well-known/webfinger?resource=acct:<username>@<domain>`
|
||||||
|
- **Purpose**: Allow remote instances to discover local actors
|
||||||
|
|
||||||
|
#### ActivityPubController
|
||||||
|
- **Location**: `DysonNetwork.Sphere/ActivityPub/ActivityPubController.cs`
|
||||||
|
- **Endpoints**:
|
||||||
|
- `GET /activitypub/actors/{username}` - Actor profile in JSON-LD
|
||||||
|
- `GET /activitypub/actors/{username}/outbox` - Public posts
|
||||||
|
- `POST /activitypub/actors/{username}/inbox` - Receive activities
|
||||||
|
- **Features**:
|
||||||
|
- Public key in actor profile
|
||||||
|
- ActivityPub JSON-LD responses
|
||||||
|
- HTTP signature verification on inbox
|
||||||
|
- Activity processing pipeline
|
||||||
|
|
||||||
|
### 5. Model Updates ✅
|
||||||
|
- Added `Meta` field to `SnPublisher` for storing ActivityPub keys
|
||||||
|
- All models follow existing Solar Network patterns
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Incoming Activity Flow
|
||||||
|
```
|
||||||
|
1. Remote server sends POST to /inbox
|
||||||
|
2. HTTP Signature is verified
|
||||||
|
3. Activity type is identified
|
||||||
|
4. Specific handler processes activity:
|
||||||
|
- Follow: Create relationship, send Accept
|
||||||
|
- Create: Store content
|
||||||
|
- Like: Record reaction
|
||||||
|
- etc.
|
||||||
|
5. Database is updated
|
||||||
|
6. Response sent
|
||||||
|
```
|
||||||
|
|
||||||
|
### Outgoing Activity Flow
|
||||||
|
```
|
||||||
|
1. Local action occurs (post, like, follow)
|
||||||
|
2. Activity is created in ActivityPub format
|
||||||
|
3. Remote followers are discovered
|
||||||
|
4. HTTP request is signed with publisher's private key
|
||||||
|
5. Activity sent to each follower's inbox
|
||||||
|
6. Status logged
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Management
|
||||||
|
```
|
||||||
|
1. Publisher creates post/follows
|
||||||
|
2. Check if keys exist in Publisher.Meta
|
||||||
|
3. If not, generate RSA 2048-bit key pair
|
||||||
|
4. Store keys in Publisher.Meta
|
||||||
|
5. Use keys for signing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add to `appsettings.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ActivityPub": {
|
||||||
|
"Domain": "your-domain.com",
|
||||||
|
"EnableFederation": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### WebFinger
|
||||||
|
```bash
|
||||||
|
GET /.well-known/webfinger?resource=acct:username@domain.com
|
||||||
|
Accept: application/jrd+json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Actor Profile
|
||||||
|
```bash
|
||||||
|
GET /activitypub/actors/username
|
||||||
|
Accept: application/activity+json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Outbox
|
||||||
|
```bash
|
||||||
|
GET /activitypub/actors/username/outbox
|
||||||
|
Accept: application/activity+json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inbox
|
||||||
|
```bash
|
||||||
|
POST /activitypub/actors/username/inbox
|
||||||
|
Content-Type: application/activity+json
|
||||||
|
Signature: keyId="...",algorithm="...",headers="...",signature="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### Fediverse Tables
|
||||||
|
- `fediverse_instances` - Server metadata and blocking
|
||||||
|
- `fediverse_actors` - Remote actor profiles
|
||||||
|
- `fediverse_contents` - Federated posts/notes
|
||||||
|
- `fediverse_activities` - Activity tracking
|
||||||
|
- `fediverse_relationships` - Follow relationships
|
||||||
|
- `fediverse_reactions` - Federated reactions
|
||||||
|
|
||||||
|
### Publisher Enhancement
|
||||||
|
- Added `publishers.meta` JSONB column for key storage
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Immediate (Ready for Testing)
|
||||||
|
- Apply database migrations
|
||||||
|
- Test WebFinger with a Mastodon instance
|
||||||
|
- Test follow/unfollow with another instance
|
||||||
|
- Test receiving posts from federated timeline
|
||||||
|
|
||||||
|
### Short Term
|
||||||
|
- Add HTTP Signature verification middleware
|
||||||
|
- Implement activity queue with retry logic
|
||||||
|
- Add background worker for processing queued activities
|
||||||
|
- Add metrics and monitoring
|
||||||
|
- Implement local content display in timelines
|
||||||
|
|
||||||
|
### Long Term
|
||||||
|
- Add Media support for federated content
|
||||||
|
- Implement content filtering
|
||||||
|
- Add moderation tools for federated content
|
||||||
|
- Support more activity types
|
||||||
|
- Implement instance block list management
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
The implementation follows:
|
||||||
|
- ✅ [ActivityPub W3C Recommendation](https://www.w3.org/TR/activitypub/)
|
||||||
|
- ✅ [ActivityStreams 2.0](https://www.w3.org/TR/activitystreams-core/)
|
||||||
|
- ✅ [WebFinger RFC 7033](https://tools.ietf.org/html/rfc7033)
|
||||||
|
- ✅ [HTTP Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Local Testing
|
||||||
|
```bash
|
||||||
|
# 1. Apply migrations
|
||||||
|
cd DysonNetwork.Sphere
|
||||||
|
dotnet ef database update
|
||||||
|
|
||||||
|
# 2. Test WebFinger
|
||||||
|
curl "http://localhost:5000/.well-known/webfinger?resource=acct:username@localhost"
|
||||||
|
|
||||||
|
# 3. Test Actor
|
||||||
|
curl -H "Accept: application/activity+json" http://localhost:5000/activitypub/actors/username
|
||||||
|
```
|
||||||
|
|
||||||
|
### Federation Testing
|
||||||
|
1. Set up a Mastodon instance (or use a public one)
|
||||||
|
2. Follow a Mastodon user from Solar Network
|
||||||
|
3. Create a post on Solar Network
|
||||||
|
4. Verify it appears on Mastodon timeline
|
||||||
|
|
||||||
|
## Architecture Decisions
|
||||||
|
|
||||||
|
1. **Key Storage**: Using `SnPublisher.Meta` JSONB field for flexibility
|
||||||
|
2. **Content Storage**: Federated content stored separately from local posts
|
||||||
|
3. **Relationship State**: Implemented with explicit states (Pending, Accepted, Rejected)
|
||||||
|
4. **Signature Algorithm**: RSA-SHA256 for compatibility
|
||||||
|
5. **Activity Processing**: Synchronous for now, can be made async with queue
|
||||||
|
6. **Content Types**: Support for Note, Article initially (can expand)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All ActivityPub communication uses HTTP Signatures
|
||||||
|
- Private keys never leave the server
|
||||||
|
- Public keys are published in actor profiles
|
||||||
|
- Soft delete is enabled on all federated models
|
||||||
|
- Failed activity deliveries are logged but not retried (future enhancement)
|
||||||
|
- Content is federated only when visibility is Public
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- `DysonNetwork.Shared/Models/FediverseInstance.cs`
|
||||||
|
- `DysonNetwork.Shared/Models/FediverseActor.cs`
|
||||||
|
- `DysonNetwork.Shared/Models/FediverseContent.cs`
|
||||||
|
- `DysonNetwork.Shared/Models/FediverseActivity.cs`
|
||||||
|
- `DysonNetwork.Shared/Models/FediverseRelationship.cs`
|
||||||
|
- `DysonNetwork.Shared/Models/FediverseReaction.cs`
|
||||||
|
- `DysonNetwork.Sphere/ActivityPub/WebFingerController.cs`
|
||||||
|
- `DysonNetwork.Sphere/ActivityPub/ActivityPubController.cs`
|
||||||
|
- `DysonNetwork.Sphere/ActivityPub/ActivityPubKeyService.cs`
|
||||||
|
- `DysonNetwork.Sphere/ActivityPub/ActivityPubSignatureService.cs`
|
||||||
|
- `DysonNetwork.Sphere/ActivityPub/ActivityPubActivityProcessor.cs`
|
||||||
|
- `DysonNetwork.Sphere/ActivityPub/ActivityPubDeliveryService.cs`
|
||||||
|
- `DysonNetwork.Sphere/Migrations/20251228120000_AddActivityPubModels.cs`
|
||||||
|
- `DysonNetwork.Sphere/Migrations/20251228130000_AddPublisherMetaForActivityPubKeys.cs`
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `DysonNetwork.Shared/Models/Publisher.cs` - Added Meta field
|
||||||
|
- `DysonNetwork.Sphere/AppDatabase.cs` - Added DbSets for ActivityPub
|
||||||
|
- `DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs` - Registered ActivityPub services
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ActivityPub Implementation Guide](./ACTIVITYPUB_IMPLEMENTATION.md)
|
||||||
|
- [ActivityPub Plan](./ACTIVITYPUB_PLAN.md)
|
||||||
|
- [Solar Network Architecture](./README.md)
|
||||||
820
docs/activitypub/ACTIVITYPUB_TESTING_GUIDE.md
Normal file
@@ -0,0 +1,820 @@
|
|||||||
|
# ActivityPub Testing Guide for Solar Network
|
||||||
|
|
||||||
|
This guide will help you test the ActivityPub implementation in Solar Network, starting with a self-hosted instance and then moving to a real instance.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- ✅ Solar Network codebase with ActivityPub implementation
|
||||||
|
- ✅ Docker installed (for running Mastodon/Fediverse instances)
|
||||||
|
- ✅ PostgreSQL database running
|
||||||
|
- ✅ `.NET 10` SDK
|
||||||
|
|
||||||
|
## Part 1: Set Up a Self-Hosted Test Instance
|
||||||
|
|
||||||
|
### Option A: Using Mastodon (Recommended for Compatibility)
|
||||||
|
|
||||||
|
#### 1. Create a Docker Compose File
|
||||||
|
|
||||||
|
Create `docker-compose.mastodon-test.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
restart: always
|
||||||
|
image: postgres:14-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: mastodon
|
||||||
|
POSTGRES_PASSWORD: mastodon_password
|
||||||
|
POSTGRES_DB: mastodon
|
||||||
|
networks:
|
||||||
|
- mastodon_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "pg_isready", "-U", "mastodon"]
|
||||||
|
interval: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
restart: always
|
||||||
|
image: redis:7-alpine
|
||||||
|
networks:
|
||||||
|
- mastodon_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
es:
|
||||||
|
restart: always
|
||||||
|
image: docker.elastic.co/elasticsearch:8.10.2
|
||||||
|
environment:
|
||||||
|
- "discovery.type=single-node"
|
||||||
|
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||||
|
- "xpack.security.enabled=false"
|
||||||
|
networks:
|
||||||
|
- mastodon_network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -silent http://localhost:9200/_cluster/health || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
web:
|
||||||
|
restart: always
|
||||||
|
image: tootsuite/mastodon:latest
|
||||||
|
env_file: .env.mastodon
|
||||||
|
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
||||||
|
ports:
|
||||||
|
- "3001:3000"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
- es
|
||||||
|
networks:
|
||||||
|
- mastodon_network
|
||||||
|
volumes:
|
||||||
|
- ./mastodon-data/public:/mastodon/public/system
|
||||||
|
|
||||||
|
streaming:
|
||||||
|
restart: always
|
||||||
|
image: tootsuite/mastodon:latest
|
||||||
|
env_file: .env.mastodon
|
||||||
|
command: node ./streaming
|
||||||
|
ports:
|
||||||
|
- "4000:4000"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- mastodon_network
|
||||||
|
|
||||||
|
sidekiq:
|
||||||
|
restart: always
|
||||||
|
image: tootsuite/mastodon:latest
|
||||||
|
env_file: .env.mastodon
|
||||||
|
command: bundle exec sidekiq
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- mastodon_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mastodon_network:
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Create Environment File
|
||||||
|
|
||||||
|
Create `.env.mastodon`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Federation
|
||||||
|
LOCAL_DOMAIN=mastodon.local
|
||||||
|
LOCAL_HTTPS=false
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_HOST=db
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=mastodon
|
||||||
|
DB_NAME=mastodon
|
||||||
|
DB_PASS=mastodon_password
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
# Elasticsearch
|
||||||
|
ES_ENABLED=true
|
||||||
|
ES_HOST=es
|
||||||
|
ES_PORT=9200
|
||||||
|
|
||||||
|
# Secrets (generate these!)
|
||||||
|
SECRET_KEY_BASE=change_me_to_a_random_string_at_least_32_chars
|
||||||
|
OTP_SECRET=change_me_to_another_random_string
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
SINGLE_USER_MODE=false
|
||||||
|
DEFAULT_LOCALE=en
|
||||||
|
```
|
||||||
|
|
||||||
|
**Generate secrets:**
|
||||||
|
```bash
|
||||||
|
# Run these to generate random secrets
|
||||||
|
openssl rand -base64 32
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Start Mastodon
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.mastodon-test.yml up -d
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker-compose -f docker-compose.mastodon-test.yml logs -f web
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for the web service to be healthy (may take 2-5 minutes).
|
||||||
|
|
||||||
|
#### 4. Create a Mastodon Account
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run this command to create an admin account
|
||||||
|
docker-compose -f docker-compose.mastodon-test.yml exec web \
|
||||||
|
bin/tootctl accounts create \
|
||||||
|
testuser \
|
||||||
|
testuser@mastodon.local \
|
||||||
|
--email=test@example.com \
|
||||||
|
--confirmed \
|
||||||
|
--role=admin \
|
||||||
|
--approve
|
||||||
|
```
|
||||||
|
|
||||||
|
Set password: `TestPassword123!`
|
||||||
|
|
||||||
|
#### 5. Update Your /etc/hosts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
Add:
|
||||||
|
```
|
||||||
|
127.0.0.1 mastodon.local
|
||||||
|
127.0.0.1 solar.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: Using GoToSocial (Lightweight Alternative)
|
||||||
|
|
||||||
|
Create `docker-compose.gotosocial.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
gotosocial:
|
||||||
|
image: superseriousbusiness/gotosocial:latest
|
||||||
|
environment:
|
||||||
|
- GTS_HOST=gotosocial.local
|
||||||
|
- GTS_ACCOUNT_DOMAIN=gotosocial.local
|
||||||
|
- GTS_PROTOCOL=http
|
||||||
|
- GTS_DB_TYPE=sqlite
|
||||||
|
- GTS_DB_ADDRESS=/gotosocial/data/sqlite.db
|
||||||
|
- GTS_STORAGE_LOCAL_BASE_PATH=/gotosocial/data/storage
|
||||||
|
ports:
|
||||||
|
- "3002:8080"
|
||||||
|
volumes:
|
||||||
|
- ./gotosocial-data:/gotosocial/data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
```
|
||||||
|
|
||||||
|
Start it:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.gotosocial.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Create account:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.gotosocial.yml exec gotosocial \
|
||||||
|
/gotosocial/gotosocial admin account create \
|
||||||
|
--username testuser \
|
||||||
|
--email test@example.com \
|
||||||
|
--password TestPassword123!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Part 2: Configure Solar Network for Federation
|
||||||
|
|
||||||
|
### 1. Update appsettings.json
|
||||||
|
|
||||||
|
Edit `DysonNetwork.Sphere/appsettings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ActivityPub": {
|
||||||
|
"Domain": "solar.local",
|
||||||
|
"EnableFederation": true
|
||||||
|
},
|
||||||
|
"Kestrel": {
|
||||||
|
"Endpoints": {
|
||||||
|
"Http": {
|
||||||
|
"Url": "http://solar.local:5000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Update /etc/hosts
|
||||||
|
|
||||||
|
Add both instances:
|
||||||
|
```
|
||||||
|
127.0.0.1 mastodon.local
|
||||||
|
127.0.0.1 solar.local
|
||||||
|
127.0.0.1 gotosocial.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Apply Database Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd DysonNetwork.Sphere
|
||||||
|
dotnet ef database update
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Start Solar Network
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project DysonNetwork.Sphere
|
||||||
|
```
|
||||||
|
|
||||||
|
Solar Network should now be running on `http://solar.local:5000`
|
||||||
|
|
||||||
|
## Part 3: Create Test Users
|
||||||
|
|
||||||
|
### In Solar Network
|
||||||
|
|
||||||
|
1. Open http://solar.local:5000 (or your web interface)
|
||||||
|
2. Create a new account/publisher named `solaruser`
|
||||||
|
3. Note down the publisher ID for later
|
||||||
|
|
||||||
|
**Or via API** (if you have an existing account):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First, create a publisher in Solar Network
|
||||||
|
curl -X POST http://solar.local:5000/api/publishers \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"name": "solaruser",
|
||||||
|
"nick": "Solar User",
|
||||||
|
"bio": "Testing ActivityPub federation!",
|
||||||
|
"type": 0
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### In Mastodon
|
||||||
|
|
||||||
|
Open http://mastodon.local:3001 and log in with:
|
||||||
|
- Username: `testuser`
|
||||||
|
- Password: `TestPassword123!`
|
||||||
|
|
||||||
|
## Part 4: Test Federation Scenarios
|
||||||
|
|
||||||
|
### Test 1: WebFinger Discovery
|
||||||
|
|
||||||
|
**Goal**: Verify Solar Network is discoverable
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Query Solar Network's WebFinger endpoint
|
||||||
|
curl -v "http://solar.local:5000/.well-known/webfinger?resource=acct:solaruser@solar.local"
|
||||||
|
|
||||||
|
# Expected response (200 OK):
|
||||||
|
{
|
||||||
|
"subject": "acct:solaruser@solar.local",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": "https://solar.local:5000/activitypub/actors/solaruser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://webfinger.net/rel/profile-page",
|
||||||
|
"type": "text/html",
|
||||||
|
"href": "https://solar.local:5000/users/solaruser"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Fetch Actor Profile
|
||||||
|
|
||||||
|
**Goal**: Get ActivityPub actor JSON
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fetch Solar Network actor from Mastodon
|
||||||
|
curl -H "Accept: application/activity+json" \
|
||||||
|
http://solar.local:5000/activitypub/actors/solaruser
|
||||||
|
|
||||||
|
# Expected response includes:
|
||||||
|
{
|
||||||
|
"@context": ["https://www.w3.org/ns/activitystreams"],
|
||||||
|
"id": "https://solar.local:5000/activitypub/actors/solaruser",
|
||||||
|
"type": "Person",
|
||||||
|
"preferredUsername": "solaruser",
|
||||||
|
"inbox": "https://solar.local:5000/activitypub/actors/solaruser/inbox",
|
||||||
|
"outbox": "https://solar.local:5000/activitypub/actors/solaruser/outbox",
|
||||||
|
"followers": "https://solar.local:5000/activitypub/actors/solaruser/followers",
|
||||||
|
"publicKey": {
|
||||||
|
"id": "https://solar.local:5000/activitypub/actors/solaruser#main-key",
|
||||||
|
"owner": "https://solar.local:5000/activitypub/actors/solaruser",
|
||||||
|
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\n..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3: Follow from Mastodon to Solar Network
|
||||||
|
|
||||||
|
**Goal**: Mastodon user follows Solar Network user
|
||||||
|
|
||||||
|
1. **In Mastodon**:
|
||||||
|
- Go to http://mastodon.local:3001
|
||||||
|
- In search bar, type: `@solaruser@solar.local`
|
||||||
|
- Click the follow button
|
||||||
|
|
||||||
|
2. **Verify in Solar Network**:
|
||||||
|
```bash
|
||||||
|
# Check database for relationship
|
||||||
|
psql -d dyson_network -c \
|
||||||
|
"SELECT * FROM fediverse_relationships WHERE is_local_actor = true;"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check Solar Network logs**:
|
||||||
|
Should see:
|
||||||
|
```
|
||||||
|
Processing activity type: Follow from actor: ...
|
||||||
|
Processed follow from ... to ...
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verify Mastodon receives Accept**:
|
||||||
|
- Check Mastodon logs for Accept activity
|
||||||
|
- Verify follow appears as accepted in Mastodon
|
||||||
|
|
||||||
|
### Test 4: Follow from Solar Network to Mastodon
|
||||||
|
|
||||||
|
**Goal**: Solar Network user follows Mastodon user
|
||||||
|
|
||||||
|
You'll need to call the ActivityPub delivery service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Via API (you'll need to implement this endpoint):
|
||||||
|
curl -X POST http://solar.local:5000/api/activitypub/follow \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"targetActorUri": "http://mastodon.local:3001/users/testuser"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Or test directly with curl** (simulating a Follow activity):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a Follow activity
|
||||||
|
curl -X POST http://solar.local:5000/activitypub/actors/solaruser/inbox \
|
||||||
|
-H "Content-Type: application/activity+json" \
|
||||||
|
-d '{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "http://mastodon.local:3001/test-follow-activity",
|
||||||
|
"type": "Follow",
|
||||||
|
"actor": "http://mastodon.local:3001/users/testuser",
|
||||||
|
"object": "https://solar.local:5000/activitypub/actors/solaruser"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 5: Create a Post in Solar Network
|
||||||
|
|
||||||
|
**Goal**: Post federates to Mastodon
|
||||||
|
|
||||||
|
1. **Create a post via Solar Network API**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://solar.local:5000/api/posts \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"content": "Hello fediverse! Testing ActivityPub from Solar Network! 🚀",
|
||||||
|
"visibility": 0,
|
||||||
|
"publisherId": "PUBLISHER_ID"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Wait a few seconds**
|
||||||
|
|
||||||
|
3. **Check in Mastodon**:
|
||||||
|
- Go to http://mastodon.local:3001
|
||||||
|
- The post should appear in the federated timeline
|
||||||
|
- It should show `@solaruser@solar.local` as the author
|
||||||
|
|
||||||
|
4. **Verify Solar Network logs**:
|
||||||
|
```
|
||||||
|
Successfully sent activity to http://mastodon.local:3001/inbox
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 6: Like from Mastodon
|
||||||
|
|
||||||
|
**Goal**: Mastodon user likes a Solar Network post
|
||||||
|
|
||||||
|
1. **In Mastodon**:
|
||||||
|
- Find the Solar Network post
|
||||||
|
- Click the favorite/like button
|
||||||
|
|
||||||
|
2. **Verify in Solar Network**:
|
||||||
|
```bash
|
||||||
|
psql -d dyson_network -c \
|
||||||
|
"SELECT * FROM fediverse_reactions;"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check Solar Network logs**:
|
||||||
|
```
|
||||||
|
Processing activity type: Like from actor: ...
|
||||||
|
Processed like from ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 7: Reply from Mastodon
|
||||||
|
|
||||||
|
**Goal**: Reply federates to Solar Network
|
||||||
|
|
||||||
|
1. **In Mastodon**:
|
||||||
|
- Reply to the Solar Network post
|
||||||
|
- Write: "@solaruser Nice to meet you!"
|
||||||
|
|
||||||
|
2. **Verify in Solar Network**:
|
||||||
|
```bash
|
||||||
|
psql -d dyson_network -c \
|
||||||
|
"SELECT * FROM fediverse_contents WHERE in_reply_to IS NOT NULL;"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Part 5: Debugging and Troubleshooting
|
||||||
|
|
||||||
|
### Enable Detailed Logging
|
||||||
|
|
||||||
|
Edit `appsettings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"DysonNetwork.Sphere.ActivityPub": "Trace"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Database State
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check actors
|
||||||
|
psql -d dyson_network -c \
|
||||||
|
"SELECT uri, username, display_name FROM fediverse_actors;"
|
||||||
|
|
||||||
|
# Check contents
|
||||||
|
psql -d dyson_network -c \
|
||||||
|
"SELECT uri, type, content FROM fediverse_contents;"
|
||||||
|
|
||||||
|
# Check relationships
|
||||||
|
psql -d dyson_network -c \
|
||||||
|
"SELECT * FROM fediverse_relationships;"
|
||||||
|
|
||||||
|
# Check activities
|
||||||
|
psql -d dyson_network -c \
|
||||||
|
"SELECT type, status, error_message FROM fediverse_activities;"
|
||||||
|
|
||||||
|
# Check failed activities
|
||||||
|
psql -d dyson_network -c \
|
||||||
|
"SELECT * FROM fediverse_activities WHERE status = 3;" # 3 = Failed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Issue: "Failed to verify signature"
|
||||||
|
|
||||||
|
**Cause**: HTTP Signature verification failed
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Check the signature header format
|
||||||
|
2. Verify public key matches actor's keyId
|
||||||
|
3. Ensure Date header is within 5 minutes
|
||||||
|
4. Check host header matches request URL
|
||||||
|
|
||||||
|
#### Issue: "Target actor or inbox not found"
|
||||||
|
|
||||||
|
**Cause**: Remote actor not fetched yet
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Manually fetch the actor first
|
||||||
|
2. Check actor URL is correct
|
||||||
|
3. Verify remote instance is accessible
|
||||||
|
|
||||||
|
#### Issue: "Content already exists"
|
||||||
|
|
||||||
|
**Cause**: Duplicate activity received
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. This is normal - deduplication is working
|
||||||
|
2. Check if content appears correctly
|
||||||
|
|
||||||
|
#### Issue: CORS errors when testing from browser
|
||||||
|
|
||||||
|
**Cause**: Browser blocking cross-origin requests
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Use curl for API testing
|
||||||
|
2. Or disable CORS in development
|
||||||
|
3. Test directly from Mastodon interface
|
||||||
|
|
||||||
|
### View HTTP Signatures
|
||||||
|
|
||||||
|
For debugging, you can inspect the signature:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From Mastodon to Solar Network
|
||||||
|
curl -v -X POST http://solar.local:5000/activitypub/actors/solaruser/inbox \
|
||||||
|
-H "Content-Type: application/activity+json" \
|
||||||
|
-d '{"type":"Follow",...}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for the `Signature` header in the output.
|
||||||
|
|
||||||
|
### Test HTTP Signature Verification Manually
|
||||||
|
|
||||||
|
Create a test script `test-signature.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
// Test signature verification
|
||||||
|
const publicKey = `-----BEGIN PUBLIC KEY-----
|
||||||
|
...
|
||||||
|
-----END PUBLIC KEY-----`;
|
||||||
|
|
||||||
|
const signingString = `(request-target): post /activitypub/actors/solaruser/inbox
|
||||||
|
host: solar.local:5000
|
||||||
|
date: ${new Date().toUTCString()}
|
||||||
|
content-length: ...`;
|
||||||
|
|
||||||
|
const signature = '...';
|
||||||
|
|
||||||
|
const verify = crypto.createVerify('SHA256');
|
||||||
|
verify.update(signingString);
|
||||||
|
const isValid = verify.verify(publicKey, signature, 'base64');
|
||||||
|
|
||||||
|
console.log('Signature valid:', isValid);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Part 6: Test with a Real Instance
|
||||||
|
|
||||||
|
### Preparing for Public Federation
|
||||||
|
|
||||||
|
1. **Get a real domain** (e.g., via ngrok or a VPS)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using ngrok for testing
|
||||||
|
ngrok http 5000
|
||||||
|
|
||||||
|
# This gives you: https://random-id.ngrok-free.app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update Solar Network config**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ActivityPub": {
|
||||||
|
"Domain": "your-domain.com",
|
||||||
|
"EnableFederation": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update DNS** (if using real domain):
|
||||||
|
- Add A record pointing to your server
|
||||||
|
- Configure HTTPS (required for production federation)
|
||||||
|
|
||||||
|
4. **Test WebFinger with your domain**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://your-domain.com/.well-known/webfinger?resource=acct:username@your-domain.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test with Mastodon.social
|
||||||
|
|
||||||
|
1. **Create a Mastodon.social account**
|
||||||
|
- Go to https://mastodon.social
|
||||||
|
- Sign up for a test account
|
||||||
|
|
||||||
|
2. **Search for your Solar Network user**:
|
||||||
|
- In Mastodon.social search: `@username@your-domain.com`
|
||||||
|
- Click follow
|
||||||
|
|
||||||
|
3. **Create a post in Solar Network**
|
||||||
|
- Should appear in Mastodon.social
|
||||||
|
|
||||||
|
4. **Reply from Mastodon.social**
|
||||||
|
- Should appear in Solar Network
|
||||||
|
|
||||||
|
### Test with Other Instances
|
||||||
|
|
||||||
|
- **Pleroma**: Similar to Mastodon, good for testing
|
||||||
|
- **Lemmy**: For testing community features (later)
|
||||||
|
- **Pixelfed**: For testing media posts
|
||||||
|
- **PeerTube**: For testing video content (later)
|
||||||
|
|
||||||
|
## Part 7: Verification Checklist
|
||||||
|
|
||||||
|
### Self-Hosted Instance Tests
|
||||||
|
|
||||||
|
- [ ] WebFinger returns correct actor links
|
||||||
|
- [ ] Actor profile has all required fields
|
||||||
|
- [ ] Follow from Mastodon to Solar Network works
|
||||||
|
- [ ] Follow from Solar Network to Mastodon works
|
||||||
|
- [ ] Accept activity sent back to Mastodon
|
||||||
|
- [ ] Posts from Solar Network appear in Mastodon timeline
|
||||||
|
- [ ] Posts from Mastodon appear in Solar Network database
|
||||||
|
- [ ] Likes from Mastodon appear in Solar Network
|
||||||
|
- [ ] Replies from Mastodon appear in Solar Network
|
||||||
|
- [ ] Keys are properly generated and stored
|
||||||
|
- [ ] HTTP signatures are correctly verified
|
||||||
|
- [ ] Outbox returns public posts
|
||||||
|
|
||||||
|
### Real Instance Tests
|
||||||
|
|
||||||
|
- [ ] Domain is publicly accessible
|
||||||
|
- [ ] HTTPS is working (or HTTP for local testing)
|
||||||
|
- [ ] WebFinger works with your domain
|
||||||
|
- [ ] Actor is discoverable from other instances
|
||||||
|
- [ ] Posts federate to public instances
|
||||||
|
- [ ] Users can follow across instances
|
||||||
|
- [ ] Timelines show federated content
|
||||||
|
|
||||||
|
## Part 8: Monitoring During Tests
|
||||||
|
|
||||||
|
### Check Solar Network Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Follow logs in real-time
|
||||||
|
dotnet run --project DysonNetwork.Sphere | grep -i activitypub
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Mastodon Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.mastodon-test.yml logs -f web | grep -i federation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitor Database Activity
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Watch activity table
|
||||||
|
watch -n 2 'psql -d dyson_network -c "SELECT type, status, created_at FROM fediverse_activities ORDER BY created_at DESC LIMIT 10;"'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Network Traffic
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Monitor HTTP requests
|
||||||
|
tcpdump -i lo port 5000 or port 3001 -A
|
||||||
|
```
|
||||||
|
|
||||||
|
## Part 9: Advanced Testing
|
||||||
|
|
||||||
|
### Test HTTP Signature Fallbacks
|
||||||
|
|
||||||
|
Test with various signature headers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With Date header
|
||||||
|
curl -H "Date: $(date -u +%a,\ %d\ %b\ %Y\ %T\ GMT)" ...
|
||||||
|
|
||||||
|
# With Digest header
|
||||||
|
curl -H "Digest: SHA-256=$(echo -n '{}' | openssl dgst -sha256 -binary | base64)" ...
|
||||||
|
|
||||||
|
# Multiple signed headers
|
||||||
|
curl -H "Signature: keyId=\"...\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"...\"" ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Rate Limiting
|
||||||
|
|
||||||
|
Send multiple requests quickly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for i in {1..10}; do
|
||||||
|
curl -X POST http://solar.local:5000/activitypub/actors/solaruser/inbox \
|
||||||
|
-H "Content-Type: application/activity+json" \
|
||||||
|
-d '{"type":"Create",...}'
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Large Posts
|
||||||
|
|
||||||
|
Send post with attachments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://solar.local:5000/api/posts \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"content": "A post with an image",
|
||||||
|
"attachments": [{"id": "file-id"}],
|
||||||
|
"visibility": 0
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Part 10: Cleanup
|
||||||
|
|
||||||
|
### Stop Test Instances
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop Mastodon
|
||||||
|
docker-compose -f docker-compose.mastodon-test.yml down
|
||||||
|
|
||||||
|
# Stop GoToSocial
|
||||||
|
docker-compose -f docker-compose.gotosocial.yml down
|
||||||
|
|
||||||
|
# Remove data volumes
|
||||||
|
docker-compose -f docker-compose.mastodon-test.yml down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset Solar Network Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Warning: This deletes all data!
|
||||||
|
cd DysonNetwork.Sphere
|
||||||
|
dotnet ef database drop
|
||||||
|
dotnet ef database update
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove /etc/hosts Entries
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/hosts
|
||||||
|
|
||||||
|
# Remove these lines:
|
||||||
|
# 127.0.0.1 mastodon.local
|
||||||
|
# 127.0.0.1 solar.local
|
||||||
|
# 127.0.0.1 gotosocial.local
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps After Testing
|
||||||
|
|
||||||
|
1. **Fix any issues found during testing**
|
||||||
|
2. **Add retry logic for failed deliveries**
|
||||||
|
3. **Implement activity queue for async processing**
|
||||||
|
4. **Add monitoring and metrics**
|
||||||
|
5. **Test with more instances (Pleroma, Pixelfed, etc.)**
|
||||||
|
6. **Add support for more activity types**
|
||||||
|
7. **Improve error handling and logging**
|
||||||
|
8. **Add admin interface for managing federation**
|
||||||
|
|
||||||
|
## Useful Tools
|
||||||
|
|
||||||
|
### ActivityPub Testing Tools
|
||||||
|
- [ActivityPub Playground](https://swicth.github.io/activity-pub-playground/)
|
||||||
|
- [FediTest](https://feditest.com/)
|
||||||
|
- [FediVerse.net](https://fedi.net/)
|
||||||
|
|
||||||
|
### HTTP Testing
|
||||||
|
- [curl](https://curl.se/)
|
||||||
|
- [httpie](https://httpie.io/)
|
||||||
|
- [Postman](https://www.postman.com/)
|
||||||
|
|
||||||
|
### JSON Inspection
|
||||||
|
- [jq](https://stedolan.github.io/jq/)
|
||||||
|
- [jsonpath.com](https://jsonpath.com/)
|
||||||
|
|
||||||
|
### Network Debugging
|
||||||
|
- [Wireshark](https://www.wireshark.org/)
|
||||||
|
- [tcpdump](https://www.tcpdump.org/)
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ActivityPub W3C Spec](https://www.w3.org/TR/activitypub/)
|
||||||
|
- [HTTP Signatures Draft](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
|
||||||
|
- [WebFinger RFC 7033](https://tools.ietf.org/html/rfc7033)
|
||||||
|
- [Mastodon Federation Documentation](https://docs.joinmastodon.org/admin/federation/)
|
||||||
506
docs/activitypub/ACTIVITYPUB_TESTING_HELPER_API.md
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
# ActivityPub Testing Helper API
|
||||||
|
|
||||||
|
This document describes helper endpoints for testing ActivityPub federation.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
These endpoints allow you to manually trigger ActivityPub activities for testing purposes without implementing the full UI federation integration yet.
|
||||||
|
|
||||||
|
## Helper Endpoints
|
||||||
|
|
||||||
|
### 1. Send Follow Activity
|
||||||
|
|
||||||
|
**Endpoint**: `POST /api/activitypub/test/follow`
|
||||||
|
|
||||||
|
**Description**: Sends a Follow activity to a remote actor
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"targetActorUri": "http://mastodon.local:3001/users/testuser"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"activityId": "http://solar.local:5000/activitypub/activities/...",
|
||||||
|
"targetActor": "http://mastodon.local:3001/users/testuser"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Send Like Activity
|
||||||
|
|
||||||
|
**Endpoint**: `POST /api/activitypub/test/like`
|
||||||
|
|
||||||
|
**Description**: Sends a Like activity for a post (can be local or remote)
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"postId": "POST_ID",
|
||||||
|
"targetActorUri": "http://mastodon.local:3001/users/testuser"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"activityId": "http://solar.local:5000/activitypub/activities/..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Send Announce (Boost) Activity
|
||||||
|
|
||||||
|
**Endpoint**: `POST /api/activitypub/test/announce`
|
||||||
|
|
||||||
|
**Description**: Boosts a post to followers
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"postId": "POST_ID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Send Undo Activity
|
||||||
|
|
||||||
|
**Endpoint**: `POST /api/activitypub/test/undo`
|
||||||
|
|
||||||
|
**Description**: Undoes a previous activity
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activityType": "Like", // or "Follow", "Announce"
|
||||||
|
"objectUri": "http://solar.local:5000/activitypub/objects/POST_ID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Get Federation Status
|
||||||
|
|
||||||
|
**Endpoint**: `GET /api/activitypub/test/status`
|
||||||
|
|
||||||
|
**Description**: Returns current federation statistics
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"actors": {
|
||||||
|
"total": 5,
|
||||||
|
"local": 1,
|
||||||
|
"remote": 4
|
||||||
|
},
|
||||||
|
"contents": {
|
||||||
|
"total": 25,
|
||||||
|
"byType": {
|
||||||
|
"Note": 20,
|
||||||
|
"Article": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relationships": {
|
||||||
|
"total": 8,
|
||||||
|
"accepted": 6,
|
||||||
|
"pending": 1,
|
||||||
|
"rejected": 1
|
||||||
|
},
|
||||||
|
"activities": {
|
||||||
|
"total": 45,
|
||||||
|
"byStatus": {
|
||||||
|
"Completed": 40,
|
||||||
|
"Pending": 3,
|
||||||
|
"Failed": 2
|
||||||
|
},
|
||||||
|
"byType": {
|
||||||
|
"Create": 20,
|
||||||
|
"Follow": 8,
|
||||||
|
"Accept": 6,
|
||||||
|
"Like": 5,
|
||||||
|
"Announce": 3,
|
||||||
|
"Undo": 2,
|
||||||
|
"Delete": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Get Recent Activities
|
||||||
|
|
||||||
|
**Endpoint**: `GET /api/activitypub/test/activities`
|
||||||
|
|
||||||
|
**Query Parameters**:
|
||||||
|
- `limit`: Number of activities to return (default: 20)
|
||||||
|
- `type`: Filter by activity type (optional)
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"activities": [
|
||||||
|
{
|
||||||
|
"id": "ACTIVITY_ID",
|
||||||
|
"type": "Follow",
|
||||||
|
"status": "Completed",
|
||||||
|
"actorUri": "http://mastodon.local:3001/users/testuser",
|
||||||
|
"objectUri": "http://solar.local:5000/activitypub/actors/solaruser",
|
||||||
|
"createdAt": "2024-01-15T10:30:00Z",
|
||||||
|
"errorMessage": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Get Actor Keys
|
||||||
|
|
||||||
|
**Endpoint**: `GET /api/activitypub/test/actors/{username}/keys`
|
||||||
|
|
||||||
|
**Description**: Returns the public/private key pair for a publisher
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "solaruser",
|
||||||
|
"hasKeys": true,
|
||||||
|
"actorUri": "http://solar.local:5000/activitypub/actors/solaruser",
|
||||||
|
"publicKeyId": "http://solar.local:5000/activitypub/actors/solaruser#main-key",
|
||||||
|
"publicKey": "-----BEGIN PUBLIC KEY-----\n...",
|
||||||
|
"privateKeyStored": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Test HTTP Signature
|
||||||
|
|
||||||
|
**Endpoint**: `POST /api/activitypub/test/sign`
|
||||||
|
|
||||||
|
**Description**: Test if a signature string is valid for a given public key
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"publicKey": "-----BEGIN PUBLIC KEY-----\n...",
|
||||||
|
"signingString": "(request-target): post /inbox\nhost: example.com\ndate: ...",
|
||||||
|
"signature": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"message": "Signature is valid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Controller Implementation
|
||||||
|
|
||||||
|
Create `DysonNetwork.Sphere/ActivityPub/ActivityPubTestController.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.ActivityPub;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/activitypub/test")]
|
||||||
|
[Authorize] // Require auth for testing
|
||||||
|
public class ActivityPubTestController(
|
||||||
|
AppDatabase db,
|
||||||
|
ActivityPubDeliveryService deliveryService,
|
||||||
|
ActivityPubKeyService keyService,
|
||||||
|
ActivityPubSignatureService signatureService,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<ActivityPubTestController> logger
|
||||||
|
) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost("follow")]
|
||||||
|
public async Task<ActionResult> TestFollow([FromBody] TestFollowRequest request)
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
var publisher = await GetPublisherForUser(currentUser.Id);
|
||||||
|
|
||||||
|
if (publisher == null)
|
||||||
|
return BadRequest("Publisher not found");
|
||||||
|
|
||||||
|
var success = await deliveryService.SendFollowActivityAsync(
|
||||||
|
publisher.Id,
|
||||||
|
request.TargetActorUri
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success,
|
||||||
|
targetActorUri = request.TargetActorUri,
|
||||||
|
publisherId = publisher.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("like")]
|
||||||
|
public async Task<ActionResult> TestLike([FromBody] TestLikeRequest request)
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
var publisher = await GetPublisherForUser(currentUser.Id);
|
||||||
|
|
||||||
|
var success = await deliveryService.SendLikeActivityAsync(
|
||||||
|
request.PostId,
|
||||||
|
currentUser.Id,
|
||||||
|
request.TargetActorUri
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(new { success, postId = request.PostId });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("announce")]
|
||||||
|
public async Task<ActionResult> TestAnnounce([FromBody] TestAnnounceRequest request)
|
||||||
|
{
|
||||||
|
var post = await db.Posts.FindAsync(request.PostId);
|
||||||
|
if (post == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var success = await deliveryService.SendCreateActivityAsync(post);
|
||||||
|
|
||||||
|
return Ok(new { success, postId = request.PostId });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("undo")]
|
||||||
|
public async Task<ActionResult> TestUndo([FromBody] TestUndoRequest request)
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
var publisher = await GetPublisherForUser(currentUser.Id);
|
||||||
|
|
||||||
|
if (publisher == null)
|
||||||
|
return BadRequest("Publisher not found");
|
||||||
|
|
||||||
|
var success = await deliveryService.SendUndoActivityAsync(
|
||||||
|
request.ActivityType,
|
||||||
|
request.ObjectUri,
|
||||||
|
publisher.Id
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(new { success, activityType = request.ActivityType });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("status")]
|
||||||
|
public async Task<ActionResult> GetStatus()
|
||||||
|
{
|
||||||
|
var totalActors = await db.FediverseActors.CountAsync();
|
||||||
|
var localActors = await db.FediverseActors
|
||||||
|
.CountAsync(a => a.Uri.Contains("solar.local"));
|
||||||
|
|
||||||
|
var totalContents = await db.FediverseContents.CountAsync();
|
||||||
|
|
||||||
|
var relationships = await db.FediverseRelationships
|
||||||
|
.GroupBy(r => r.State)
|
||||||
|
.Select(g => new { State = g.Key, Count = g.Count() })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var activitiesByStatus = await db.FediverseActivities
|
||||||
|
.GroupBy(a => a.Status)
|
||||||
|
.Select(g => new { Status = g.Key, Count = g.Count() })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var activitiesByType = await db.FediverseActivities
|
||||||
|
.GroupBy(a => a.Type)
|
||||||
|
.Select(g => new { Type = g.Key, Count = g.Count() })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
actors = new
|
||||||
|
{
|
||||||
|
total = totalActors,
|
||||||
|
local = localActors,
|
||||||
|
remote = totalActors - localActors
|
||||||
|
},
|
||||||
|
contents = new
|
||||||
|
{
|
||||||
|
total = totalContents
|
||||||
|
},
|
||||||
|
relationships = relationships.ToDictionary(r => r.State.ToString(), r => r.Count),
|
||||||
|
activities = new
|
||||||
|
{
|
||||||
|
byStatus = activitiesByStatus.ToDictionary(a => a.Status.ToString(), a => a.Count),
|
||||||
|
byType = activitiesByType.ToDictionary(a => a.Type.ToString(), a => a.Count)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("activities")]
|
||||||
|
public async Task<ActionResult> GetActivities([FromQuery] int limit = 20, [FromQuery] string? type = null)
|
||||||
|
{
|
||||||
|
var query = db.FediverseActivities
|
||||||
|
.OrderByDescending(a => a.CreatedAt);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(type))
|
||||||
|
{
|
||||||
|
query = query.Where(a => a.Type.ToString() == type);
|
||||||
|
}
|
||||||
|
|
||||||
|
var activities = await query
|
||||||
|
.Take(limit)
|
||||||
|
.Select(a => new
|
||||||
|
{
|
||||||
|
a.Id,
|
||||||
|
a.Type,
|
||||||
|
a.Status,
|
||||||
|
ActorUri = a.Actor.Uri,
|
||||||
|
ObjectUri = a.ObjectUri,
|
||||||
|
a.CreatedAt,
|
||||||
|
a.ErrorMessage
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(new { activities });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("actors/{username}/keys")]
|
||||||
|
public async Task<ActionResult> GetActorKeys(string username)
|
||||||
|
{
|
||||||
|
var publisher = await db.Publishers
|
||||||
|
.FirstOrDefaultAsync(p => p.Name == username);
|
||||||
|
|
||||||
|
if (publisher == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var actorUrl = $"http://solar.local:5000/activitypub/actors/{username}";
|
||||||
|
|
||||||
|
var (privateKey, publicKey) = keyService.GenerateKeyPair();
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
hasKeys = publisher.Meta != null,
|
||||||
|
actorUri,
|
||||||
|
publicKeyId = $"{actorUrl}#main-key",
|
||||||
|
publicKey = publicKey,
|
||||||
|
privateKeyStored = publisher.Meta != null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("sign")]
|
||||||
|
public ActionResult TestSignature([FromBody] TestSignatureRequest request)
|
||||||
|
{
|
||||||
|
var isValid = keyService.Verify(
|
||||||
|
request.PublicKey,
|
||||||
|
request.SigningString,
|
||||||
|
request.Signature
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
valid = isValid,
|
||||||
|
message = isValid ? "Signature is valid" : "Signature is invalid"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<SnPublisher?> GetPublisherForUser(Guid accountId)
|
||||||
|
{
|
||||||
|
return await db.Publishers
|
||||||
|
.Include(p => p.Members)
|
||||||
|
.Where(p => p.Members.Any(m => m.AccountId == accountId))
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid GetCurrentUser()
|
||||||
|
{
|
||||||
|
// Implement based on your auth system
|
||||||
|
return Guid.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestFollowRequest
|
||||||
|
{
|
||||||
|
public string TargetActorUri { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestLikeRequest
|
||||||
|
{
|
||||||
|
public Guid PostId { get; set; }
|
||||||
|
public string TargetActorUri { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestAnnounceRequest
|
||||||
|
{
|
||||||
|
public Guid PostId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestUndoRequest
|
||||||
|
{
|
||||||
|
public string ActivityType { get; set; } = string.Empty;
|
||||||
|
public string ObjectUri { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestSignatureRequest
|
||||||
|
{
|
||||||
|
public string PublicKey { get; set; } = string.Empty;
|
||||||
|
public string SigningString { get; set; } = string.Empty;
|
||||||
|
public string Signature { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with Helper Endpoints
|
||||||
|
|
||||||
|
### 1. Test Follow
|
||||||
|
```bash
|
||||||
|
curl -X POST http://solar.local:5000/api/activitypub/test/follow \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"targetActorUri": "http://mastodon.local:3001/users/testuser"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Test Like
|
||||||
|
```bash
|
||||||
|
curl -X POST http://solar.local:5000/api/activitypub/test/like \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"postId": "YOUR_POST_ID",
|
||||||
|
"targetActorUri": "http://mastodon.local:5000/activitypub/actors/mastodonuser"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Check Status
|
||||||
|
```bash
|
||||||
|
curl http://solar.local:5000/api/activitypub/test/status \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Get Recent Activities
|
||||||
|
```bash
|
||||||
|
curl "http://solar.local:5000/api/activitypub/test/activities?limit=10" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Main Flow
|
||||||
|
|
||||||
|
These helper endpoints can be used to:
|
||||||
|
|
||||||
|
1. **Quickly test federation** without full UI integration
|
||||||
|
2. **Debug specific activity types** in isolation
|
||||||
|
3. **Verify HTTP signatures** are correct
|
||||||
|
4. **Test error handling** for various scenarios
|
||||||
|
5. **Monitor federation status** during development
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- All test endpoints require authentication
|
||||||
|
- Use only in development/staging environments
|
||||||
|
- Remove or disable in production
|
||||||
|
- Rate limiting recommended if exposing to public
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
After testing, you can:
|
||||||
|
|
||||||
|
1. Remove the test controller (optional)
|
||||||
|
2. Disable test endpoints
|
||||||
|
3. Clear test activities from database
|
||||||
|
4. Reset test relationships
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Clear test data
|
||||||
|
DELETE FROM fediverse_activities WHERE created_at < NOW() - INTERVAL '1 day';
|
||||||
|
```
|
||||||
448
docs/activitypub/ACTIVITYPUB_TESTING_INDEX.md
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
# ActivityPub Testing - Complete Guide
|
||||||
|
|
||||||
|
This is the complete guide for testing ActivityPub federation in Solar Network.
|
||||||
|
|
||||||
|
## 📁 File Overview
|
||||||
|
|
||||||
|
| File | Purpose | When to Use |
|
||||||
|
|------|---------|--------------|
|
||||||
|
| `setup-activitypub-test.sh` | One-command setup of test environment | First time setup |
|
||||||
|
| `test-activitypub.sh` | Quick validation of basic functionality | After setup, before detailed tests |
|
||||||
|
| `ACTIVITYPUB_TESTING_QUICKSTART.md` | Quick start reference | Getting started quickly |
|
||||||
|
| `ACTIVITYPUB_TESTING_GUIDE.md` | Comprehensive testing scenarios | Full testing workflow |
|
||||||
|
| `ACTIVITYPUB_TESTING_QUICKREF.md` | Command and query reference | Daily testing |
|
||||||
|
| `ACTIVITYPUB_TESTING_HELPER_API.md` | Helper API for testing | Programmatic testing |
|
||||||
|
| `ACTIVITYPUB_TEST_RESULTS_TEMPLATE.md` | Track test results | During testing |
|
||||||
|
| `ACTIVITYPUB_IMPLEMENTATION.md` | Implementation details | Understanding the code |
|
||||||
|
| `ACTIVITYPUB_SUMMARY.md` | Feature summary | Reviewing what's implemented |
|
||||||
|
|
||||||
|
## 🚀 Quick Start (5 Minutes)
|
||||||
|
|
||||||
|
### 1. Setup Test Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup-activitypub-test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- ✅ Configure `/etc/hosts`
|
||||||
|
- ✅ Start Mastodon via Docker
|
||||||
|
- ✅ Create test Mastodon account
|
||||||
|
- ✅ Apply database migrations
|
||||||
|
|
||||||
|
### 2. Validate Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test-activitypub.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This checks:
|
||||||
|
- ✅ WebFinger endpoint
|
||||||
|
- ✅ Actor profile
|
||||||
|
- ✅ Public keys
|
||||||
|
- ✅ Database tables
|
||||||
|
|
||||||
|
### 3. Start Solar Network
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd DysonNetwork.Sphere
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test Federation
|
||||||
|
|
||||||
|
1. Open http://mastodon.local:3001
|
||||||
|
2. Search for `@solaruser@solar.local`
|
||||||
|
3. Click Follow
|
||||||
|
4. Create a post in Solar Network
|
||||||
|
5. Verify it appears in Mastodon
|
||||||
|
|
||||||
|
## 📖 Recommended Reading Order
|
||||||
|
|
||||||
|
### For First-Time Testing
|
||||||
|
|
||||||
|
1. **Start Here**: `ACTIVITYPUB_TESTING_QUICKSTART.md`
|
||||||
|
- Overview of the setup
|
||||||
|
- Quick command reference
|
||||||
|
- Common commands
|
||||||
|
|
||||||
|
2. **Then**: `ACTIVITYPUB_TESTING_GUIDE.md`
|
||||||
|
- Detailed test scenarios
|
||||||
|
- Step-by-step instructions
|
||||||
|
- Troubleshooting
|
||||||
|
|
||||||
|
3. **Reference**: `ACTIVITYPUB_TESTING_QUICKREF.md`
|
||||||
|
- Command snippets
|
||||||
|
- Database queries
|
||||||
|
- Response codes
|
||||||
|
|
||||||
|
### During Testing
|
||||||
|
|
||||||
|
1. **Track Progress**: `ACTIVITYPUB_TEST_RESULTS_TEMPLATE.md`
|
||||||
|
- Checklists for each test
|
||||||
|
- Results tracking
|
||||||
|
- Issue logging
|
||||||
|
|
||||||
|
2. **Helper API**: `ACTIVITYPUB_TESTING_HELPER_API.md`
|
||||||
|
- Manual testing endpoints
|
||||||
|
- Debugging tools
|
||||||
|
- Status monitoring
|
||||||
|
|
||||||
|
### For Understanding
|
||||||
|
|
||||||
|
1. **Implementation**: `ACTIVITYPUB_IMPLEMENTATION.md`
|
||||||
|
- Architecture details
|
||||||
|
- Service descriptions
|
||||||
|
- Data flow diagrams
|
||||||
|
|
||||||
|
2. **Features**: `ACTIVITYPUB_SUMMARY.md`
|
||||||
|
- What's implemented
|
||||||
|
- Model relationships
|
||||||
|
- API endpoints
|
||||||
|
|
||||||
|
## 🔍 Test Scenarios Summary
|
||||||
|
|
||||||
|
### Basic Functionality (All instances must pass)
|
||||||
|
|
||||||
|
- [ ] WebFinger discovery works
|
||||||
|
- [ ] Actor profile is valid JSON-LD
|
||||||
|
- [ ] Public key is present
|
||||||
|
- [ ] Outbox returns public posts
|
||||||
|
- [ ] Inbox accepts activities
|
||||||
|
|
||||||
|
### Federation - Follow
|
||||||
|
|
||||||
|
- [ ] Remote user can follow local user
|
||||||
|
- [ ] Local user can follow remote user
|
||||||
|
- [ ] Accept activity is sent/received
|
||||||
|
- [ ] Relationship state is correct
|
||||||
|
- [ ] Unfollow works correctly
|
||||||
|
|
||||||
|
### Federation - Content
|
||||||
|
|
||||||
|
- [ ] Local posts federate to remote instances
|
||||||
|
- [ ] Remote posts appear in local database
|
||||||
|
- [ ] Post content is preserved
|
||||||
|
- [ ] Timestamps are correct
|
||||||
|
- [ ] Attachments are handled
|
||||||
|
- [ ] Content warnings are respected
|
||||||
|
|
||||||
|
### Federation - Interactions
|
||||||
|
|
||||||
|
- [ ] Likes federate correctly
|
||||||
|
- [ ] Likes appear in both instances
|
||||||
|
- [ ] Replies federate correctly
|
||||||
|
- [ ] Reply threading works
|
||||||
|
- [ ] Boosts/Announces work
|
||||||
|
- [ ] Undo activities work
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- [ ] HTTP signatures are verified
|
||||||
|
- [ ] Invalid signatures are rejected
|
||||||
|
- [ ] Keys are properly stored
|
||||||
|
- [ ] Private keys never exposed
|
||||||
|
|
||||||
|
## 🐛 Common Issues & Solutions
|
||||||
|
|
||||||
|
### Issue: "Failed to verify signature"
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
1. Signature header format is wrong
|
||||||
|
2. Public key doesn't match keyId
|
||||||
|
3. Date header is too old (>5 minutes)
|
||||||
|
4. Request body doesn't match digest
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Check signature format: `keyId="...",algorithm="...",headers="...",signature="..."`
|
||||||
|
2. Verify keyId in actor profile
|
||||||
|
3. Ensure Date header is recent
|
||||||
|
4. Check body is exactly what was signed
|
||||||
|
|
||||||
|
### Issue: "Target actor or inbox not found"
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
1. Actor hasn't been fetched yet
|
||||||
|
2. Actor URL is incorrect
|
||||||
|
3. Remote instance is inaccessible
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Manually fetch actor first
|
||||||
|
2. Verify actor URL is correct
|
||||||
|
3. Test accessibility with curl
|
||||||
|
|
||||||
|
### Issue: Activities not arriving
|
||||||
|
|
||||||
|
**Causes**:
|
||||||
|
1. Network connectivity issue
|
||||||
|
2. Remote instance is down
|
||||||
|
3. Activity wasn't queued properly
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Check network connectivity
|
||||||
|
2. Verify remote instance is running
|
||||||
|
3. Check fediverse_activities table for status
|
||||||
|
|
||||||
|
## 📊 Monitoring During Tests
|
||||||
|
|
||||||
|
### Check Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Solar Network ActivityPub logs
|
||||||
|
dotnet run --project DysonNetwork.Sphere 2>&1 | grep -i activitypub
|
||||||
|
|
||||||
|
# Mastodon federation logs
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml logs -f web | grep -i federation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitor Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Watch activity table
|
||||||
|
watch -n 2 'psql -d dyson_network -c \
|
||||||
|
"SELECT type, status, created_at FROM fediverse_activities ORDER BY created_at DESC LIMIT 5;"'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Network
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test connectivity between instances
|
||||||
|
curl -v http://mastodon.local:3001
|
||||||
|
curl -v http://solar.local:5000
|
||||||
|
|
||||||
|
# Test with traceroute (if available)
|
||||||
|
traceroute mastodon.local
|
||||||
|
traceroute solar.local
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Success Criteria
|
||||||
|
|
||||||
|
### Minimal Viable Federation
|
||||||
|
|
||||||
|
To consider ActivityPub implementation "working", all of these must pass:
|
||||||
|
|
||||||
|
- ✅ WebFinger returns actor links
|
||||||
|
- ✅ Actor profile has all required fields
|
||||||
|
- ✅ Follow relationships work bidirectionally
|
||||||
|
- ✅ Public posts federate to followers
|
||||||
|
- ✅ Incoming posts are stored correctly
|
||||||
|
- ✅ HTTP signatures are verified
|
||||||
|
- ✅ Basic interaction types work (Like, Reply)
|
||||||
|
|
||||||
|
### Full Production Ready
|
||||||
|
|
||||||
|
For production, also need:
|
||||||
|
|
||||||
|
- ✅ Activity queue with retry logic
|
||||||
|
- ✅ Rate limiting on outgoing deliveries
|
||||||
|
- ✅ Monitoring and alerting
|
||||||
|
- ✅ Admin interface for federation management
|
||||||
|
- ✅ Content filtering and moderation
|
||||||
|
- ✅ Instance blocking capabilities
|
||||||
|
- ✅ Performance optimization for high volume
|
||||||
|
|
||||||
|
## 🔐 Security Checklist
|
||||||
|
|
||||||
|
During testing, verify:
|
||||||
|
|
||||||
|
- [ ] Private keys are never logged
|
||||||
|
- [ ] Private keys are never returned in API responses
|
||||||
|
- [ ] Only public keys are in actor profiles
|
||||||
|
- [ ] All incoming activities are signature-verified
|
||||||
|
- [ ] Invalid signatures are rejected with 401
|
||||||
|
- [ ] TLS is used in production
|
||||||
|
- [ ] Host header is verified against request URL
|
||||||
|
|
||||||
|
## 📈 Performance Metrics
|
||||||
|
|
||||||
|
Track these during testing:
|
||||||
|
|
||||||
|
| Metric | Target | Actual |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| WebFinger response time | <500ms | ___ ms |
|
||||||
|
| Actor fetch time | <1s | ___ ms |
|
||||||
|
| Signature verification time | <100ms | ___ ms |
|
||||||
|
| Activity processing time | <500ms | ___ ms |
|
||||||
|
| Outgoing delivery success rate | >95% | ___% |
|
||||||
|
| Outgoing delivery time | <5s | ___ ms |
|
||||||
|
|
||||||
|
## 🧪 Testing Checklist
|
||||||
|
|
||||||
|
### Self-Hosted Instance Tests
|
||||||
|
|
||||||
|
**Setup**:
|
||||||
|
- [ ] Setup script completed
|
||||||
|
- [ ] Mast containers running
|
||||||
|
- [ ] Solar Network running
|
||||||
|
- [ ] /etc/hosts configured
|
||||||
|
- [ ] Database migrations applied
|
||||||
|
|
||||||
|
**Basic Federation**:
|
||||||
|
- [ ] WebFinger works
|
||||||
|
- [ ] Actor profile valid
|
||||||
|
- [ ] Public key present
|
||||||
|
- [ ] Outbox accessible
|
||||||
|
|
||||||
|
**Follow Flow**:
|
||||||
|
- [ ] Mastodon → Solar follow works
|
||||||
|
- [ ] Solar → Mastodon follow works
|
||||||
|
- [ ] Accept activity sent
|
||||||
|
- [ ] Relationship state correct
|
||||||
|
|
||||||
|
**Content Flow**:
|
||||||
|
- [ ] Solar posts appear in Mastodon
|
||||||
|
- [ ] Mastodon posts appear in Solar
|
||||||
|
- [ ] Content preserved correctly
|
||||||
|
- [ ] Timestamps correct
|
||||||
|
|
||||||
|
**Interactions**:
|
||||||
|
- [ ] Likes work both ways
|
||||||
|
- [ ] Replies work both ways
|
||||||
|
- [ ] Boosts work both ways
|
||||||
|
- [ ] Undo works
|
||||||
|
|
||||||
|
**Security**:
|
||||||
|
- [ ] HTTP signatures verified
|
||||||
|
- [ ] Invalid signatures rejected
|
||||||
|
- [ ] Keys properly managed
|
||||||
|
|
||||||
|
### Real Instance Tests
|
||||||
|
|
||||||
|
**Discovery**:
|
||||||
|
- [ ] Domain publicly accessible
|
||||||
|
- [ ] WebFinger works from public internet
|
||||||
|
- [ ] Actor discoverable from public instances
|
||||||
|
|
||||||
|
**Federation**:
|
||||||
|
- [ ] Posts federate to public instances
|
||||||
|
- [ ] Follows work with public instances
|
||||||
|
- [ ] Interactions work with public instances
|
||||||
|
|
||||||
|
## 📝 Testing Notes
|
||||||
|
|
||||||
|
### What Worked Well
|
||||||
|
1. _____________________
|
||||||
|
2. _____________________
|
||||||
|
3. _____________________
|
||||||
|
|
||||||
|
### What Needs Improvement
|
||||||
|
1. _____________________
|
||||||
|
2. _____________________
|
||||||
|
3. _____________________
|
||||||
|
|
||||||
|
### Bugs Found
|
||||||
|
| # | Description | Severity | Status |
|
||||||
|
|---|-------------|----------|--------|
|
||||||
|
| 1 | _____________________ | Low/Medium/High | ☐ Open/☐ Fixed |
|
||||||
|
| 2 | _____________________ | Low/Medium/High | ☐ Open/☐ Fixed |
|
||||||
|
| 3 | _____________________ | Low/Medium/High | ☐ Open/☐ Fixed |
|
||||||
|
|
||||||
|
## 🎓 Learning Resources
|
||||||
|
|
||||||
|
### ActivityPub Specification
|
||||||
|
- [W3C ActivityPub Recommendation](https://www.w3.org/TR/activitypub/)
|
||||||
|
- [ActivityStreams 2.0](https://www.w3.org/TR/activitystreams-core/)
|
||||||
|
- [HTTP Signatures Draft](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
|
||||||
|
|
||||||
|
### Implementation Guides
|
||||||
|
- [Mastodon Federation Guide](https://docs.joinmastodon.org/admin/federation/)
|
||||||
|
- [ActivityPub Testing Best Practices](https://blog.joinmastodon.org/2018/06/27/how-to-implement-a-basic-activitypub-server/)
|
||||||
|
- [Federation Testing Checklist](https://docs.joinmastodon.org/spec/activitypub/)
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
- [ActivityPub Playground](https://swicth.github.io/activity-pub-playground/)
|
||||||
|
- [FediTest](https://feditest.com/)
|
||||||
|
- [JSONPath Online Evaluator](https://jsonpath.com/)
|
||||||
|
|
||||||
|
## 🔄 Next Steps After Testing
|
||||||
|
|
||||||
|
### Phase 1: Fix Issues
|
||||||
|
- Address all bugs found during testing
|
||||||
|
- Improve error messages
|
||||||
|
- Add better logging
|
||||||
|
|
||||||
|
### Phase 2: Enhance Features
|
||||||
|
- Implement activity queue
|
||||||
|
- Add retry logic
|
||||||
|
- Add rate limiting
|
||||||
|
- Implement instance blocking
|
||||||
|
|
||||||
|
### Phase 3: Production Readiness
|
||||||
|
- Add monitoring and metrics
|
||||||
|
- Add admin interface
|
||||||
|
- Add content filtering
|
||||||
|
- Implement moderation tools
|
||||||
|
|
||||||
|
### Phase 4: Additional Features
|
||||||
|
- Support more activity types
|
||||||
|
- Support media attachments
|
||||||
|
- Support polls
|
||||||
|
- Support custom emojis
|
||||||
|
|
||||||
|
## 📞 Getting Help
|
||||||
|
|
||||||
|
If you encounter issues:
|
||||||
|
|
||||||
|
1. **Check logs**: See the logs section above
|
||||||
|
2. **Review troubleshooting**: See `ACTIVITYPUB_TESTING_GUIDE.md` Part 5
|
||||||
|
3. **Check database queries**: Use queries from `ACTIVITYPUB_TESTING_QUICKREF.md`
|
||||||
|
4. **Validate signatures**: Use helper API in `ACTIVITYPUB_TESTING_HELPER_API.md`
|
||||||
|
|
||||||
|
## ✨ Quick Test Commands
|
||||||
|
|
||||||
|
### All-in-One Test Sequence
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Setup
|
||||||
|
./setup-activitypub-test.sh
|
||||||
|
|
||||||
|
# 2. Validate
|
||||||
|
./test-activitypub.sh
|
||||||
|
|
||||||
|
# 3. Test WebFinger
|
||||||
|
curl "http://solar.local:5000/.well-known/webfinger?resource=acct:solaruser@solar.local"
|
||||||
|
|
||||||
|
# 4. Test Actor
|
||||||
|
curl -H "Accept: application/activity+json" \
|
||||||
|
http://solar.local:5000/activitypub/actors/solaruser
|
||||||
|
|
||||||
|
# 5. Test Follow (from Mastodon UI)
|
||||||
|
# Open http://mastodon.local:3001 and follow @solaruser@solar.local
|
||||||
|
|
||||||
|
# 6. Check database
|
||||||
|
psql -d dyson_network -c "SELECT * FROM fediverse_relationships;"
|
||||||
|
|
||||||
|
# 7. Test Post (create in Solar Network UI)
|
||||||
|
# Should appear in http://mastodon.local:3001
|
||||||
|
|
||||||
|
# 8. Verify content
|
||||||
|
psql -d dyson_network -c "SELECT * FROM fediverse_contents;"
|
||||||
|
|
||||||
|
# 9. Test Like (like from Mastodon UI)
|
||||||
|
# Should appear in fediverse_reactions table
|
||||||
|
|
||||||
|
# 10. Check activities
|
||||||
|
psql -d dyson_network -c "SELECT type, status FROM fediverse_activities;"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎉 Conclusion
|
||||||
|
|
||||||
|
You now have everything needed to test ActivityPub federation for Solar Network:
|
||||||
|
|
||||||
|
- ✅ Self-hosted test environment (Mastodon)
|
||||||
|
- ✅ Setup automation (setup script)
|
||||||
|
- ✅ Quick validation (test script)
|
||||||
|
- ✅ Comprehensive testing guide
|
||||||
|
- ✅ Helper API for programmatic testing
|
||||||
|
- ✅ Quick reference for daily use
|
||||||
|
- ✅ Results template for tracking progress
|
||||||
|
|
||||||
|
**Recommended workflow**:
|
||||||
|
1. Run `setup-activitypub-test.sh`
|
||||||
|
2. Run `test-activitypub.sh` for validation
|
||||||
|
3. Follow scenarios in `ACTIVITYPUB_TESTING_GUIDE.md`
|
||||||
|
4. Use `ACTIVITYPUB_TESTING_HELPER_API.md` for specific tests
|
||||||
|
5. Track results in `ACTIVITYPUB_TEST_RESULTS_TEMPLATE.md`
|
||||||
|
6. Reference `ACTIVITYPUB_TESTING_QUICKREF.md` for commands
|
||||||
|
|
||||||
|
Good luck with your federation testing! 🚀
|
||||||
356
docs/activitypub/ACTIVITYPUB_TESTING_QUICKREF.md
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
# ActivityPub Testing Quick Reference
|
||||||
|
|
||||||
|
## Quick Test Commands
|
||||||
|
|
||||||
|
### 1. Test WebFinger
|
||||||
|
```bash
|
||||||
|
curl "http://solar.local:5000/.well-known/webfinger?resource=acct:username@solar.local"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Fetch Actor Profile
|
||||||
|
```bash
|
||||||
|
curl -H "Accept: application/activity+json" \
|
||||||
|
http://solar.local:5000/activitypub/actors/username
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Get Outbox
|
||||||
|
```bash
|
||||||
|
curl -H "Accept: application/activity+json" \
|
||||||
|
http://solar.local:5000/activitypub/actors/username/outbox
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Send Test Follow (from remote)
|
||||||
|
```bash
|
||||||
|
curl -X POST http://solar.local:5000/activitypub/actors/username/inbox \
|
||||||
|
-H "Content-Type: application/activity+json" \
|
||||||
|
-d '{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "https://mastodon.local:3001/follow-123",
|
||||||
|
"type": "Follow",
|
||||||
|
"actor": "https://mastodon.local:3001/users/remoteuser",
|
||||||
|
"object": "https://solar.local:5000/activitypub/actors/username"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Send Test Create (post)
|
||||||
|
```bash
|
||||||
|
curl -X POST http://solar.local:5000/activitypub/actors/username/inbox \
|
||||||
|
-H "Content-Type: application/activity+json" \
|
||||||
|
-d '{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "https://mastodon.local:3001/post-123",
|
||||||
|
"type": "Create",
|
||||||
|
"actor": "https://mastodon.local:3001/users/remoteuser",
|
||||||
|
"object": {
|
||||||
|
"id": "https://mastodon.local:3001/objects/post-123",
|
||||||
|
"type": "Note",
|
||||||
|
"content": "Hello from Mastodon! @username@solar.local",
|
||||||
|
"attributedTo": "https://mastodon.local:3001/users/remoteuser",
|
||||||
|
"to": ["https://www.w3.org/ns/activitystreams#Public"]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Send Test Like
|
||||||
|
```bash
|
||||||
|
curl -X POST http://solar.local:5000/activitypub/actors/username/inbox \
|
||||||
|
-H "Content-Type: application/activity+json" \
|
||||||
|
-d '{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "https://mastodon.local:3001/like-123",
|
||||||
|
"type": "Like",
|
||||||
|
"actor": "https://mastodon.local:3001/users/remoteuser",
|
||||||
|
"object": "https://solar.local:5000/activitypub/objects/post-id"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Queries
|
||||||
|
|
||||||
|
### Check Actors
|
||||||
|
```sql
|
||||||
|
SELECT id, uri, username, display_name, instance_id
|
||||||
|
FROM fediverse_actors;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Contents
|
||||||
|
```sql
|
||||||
|
SELECT id, uri, type, content, actor_id, created_at
|
||||||
|
FROM fediverse_contents
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Relationships
|
||||||
|
```sql
|
||||||
|
SELECT r.id, a1.uri as actor, a2.uri as target, r.state, r.is_following
|
||||||
|
FROM fediverse_relationships r
|
||||||
|
JOIN fediverse_actors a1 ON r.actor_id = a1.id
|
||||||
|
JOIN fediverse_actors a2 ON r.target_actor_id = a2.id;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Activities
|
||||||
|
```sql
|
||||||
|
SELECT type, status, error_message, created_at
|
||||||
|
FROM fediverse_activities
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Reactions
|
||||||
|
```sql
|
||||||
|
SELECT r.type, c.uri as content_uri, a.uri as actor_uri
|
||||||
|
FROM fediverse_reactions r
|
||||||
|
JOIN fediverse_contents c ON r.content_id = c.id
|
||||||
|
JOIN fediverse_actors a ON r.actor_id = a.id;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check Keys in Publisher
|
||||||
|
```sql
|
||||||
|
SELECT id, name, meta
|
||||||
|
FROM publishers
|
||||||
|
WHERE meta IS NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Commands
|
||||||
|
|
||||||
|
### Start Mastodon
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.mastodon-test.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Mastodon Logs
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.mastodon-test.yml logs -f web
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop Mastodon
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.mastodon-test.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start GoToSocial
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.gotosocial.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Solar Network Commands
|
||||||
|
|
||||||
|
### Run Migrations
|
||||||
|
```bash
|
||||||
|
cd DysonNetwork.Sphere
|
||||||
|
dotnet ef database update
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run with Debug Logging
|
||||||
|
```bash
|
||||||
|
dotnet run --project DysonNetwork.Sphere -- --logging:LogLevel:DysonNetwork.Sphere.ActivityPub=Trace
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Response Codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 200 | Success |
|
||||||
|
| 202 | Accepted (activity queued) |
|
||||||
|
| 401 | Unauthorized (invalid signature) |
|
||||||
|
| 404 | Not found (user/post doesn't exist) |
|
||||||
|
| 400 | Bad request (invalid activity) |
|
||||||
|
|
||||||
|
## Activity Status Codes
|
||||||
|
|
||||||
|
| Status | Code | Meaning |
|
||||||
|
|--------|------|---------|
|
||||||
|
| Pending | 0 | Activity waiting to be processed |
|
||||||
|
| Processing | 1 | Activity being processed |
|
||||||
|
| Completed | 2 | Activity successfully processed |
|
||||||
|
| Failed | 3 | Activity processing failed |
|
||||||
|
|
||||||
|
## Relationship States
|
||||||
|
|
||||||
|
| State | Code | Meaning |
|
||||||
|
|--------|------|---------|
|
||||||
|
| Pending | 0 | Follow request sent, waiting for Accept |
|
||||||
|
| Accepted | 1 | Follow accepted, relationship active |
|
||||||
|
| Rejected | 2 | Follow rejected |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Failed to verify signature"
|
||||||
|
|
||||||
|
**Check**: Signature header format
|
||||||
|
```bash
|
||||||
|
# Should be:
|
||||||
|
Signature: keyId="...",algorithm="rsa-sha256",headers="...",signature="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check**: Public key in actor profile
|
||||||
|
```bash
|
||||||
|
curl -H "Accept: application/activity+json" \
|
||||||
|
http://solar.local:5000/activitypub/actors/username | jq '.publicKey'
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Actor not found"
|
||||||
|
|
||||||
|
**Check**: Actor exists in database
|
||||||
|
```bash
|
||||||
|
psql -d dyson_network -c \
|
||||||
|
"SELECT * FROM fediverse_actors WHERE uri = '...';"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check**: Actor URL is accessible
|
||||||
|
```bash
|
||||||
|
curl -v http://remote-instance.com/users/username
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Content already exists"
|
||||||
|
|
||||||
|
This is normal behavior - the system is deduplicating.
|
||||||
|
|
||||||
|
### "Target publisher not found"
|
||||||
|
|
||||||
|
**Check**: Publisher exists
|
||||||
|
```bash
|
||||||
|
psql -d dyson_network -c \
|
||||||
|
"SELECT * FROM publishers WHERE name = '...';"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Test Sequence
|
||||||
|
|
||||||
|
### Full Federation Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Start both instances
|
||||||
|
docker-compose -f docker-compose.mastodon-test.yml up -d
|
||||||
|
dotnet run --project DysonNetwork.Sphere
|
||||||
|
|
||||||
|
# 2. Test WebFinger
|
||||||
|
curl "http://solar.local:5000/.well-known/webfinger?resource=acct:solaruser@solar.local"
|
||||||
|
|
||||||
|
# 3. Get Actor
|
||||||
|
curl -H "Accept: application/activity+json" \
|
||||||
|
http://solar.local:5000/activitypub/actors/solaruser
|
||||||
|
|
||||||
|
# 4. Send Follow from Mastodon
|
||||||
|
# (Do this in Mastodon UI or use curl above)
|
||||||
|
|
||||||
|
# 5. Check database
|
||||||
|
psql -d dyson_network -c "SELECT * FROM fediverse_relationships;"
|
||||||
|
|
||||||
|
# 6. Send Create (post) from Mastodon
|
||||||
|
# (Use curl command above)
|
||||||
|
|
||||||
|
# 7. Check content
|
||||||
|
psql -d dyson_network -c "SELECT * FROM fediverse_contents;"
|
||||||
|
|
||||||
|
# 8. Send Like from Mastodon
|
||||||
|
# (Use curl command above)
|
||||||
|
|
||||||
|
# 9. Check reactions
|
||||||
|
psql -d dyson_network -c "SELECT * FROM fediverse_reactions;"
|
||||||
|
|
||||||
|
# 10. Check activities
|
||||||
|
psql -d dyson_network -c "SELECT type, status FROM fediverse_activities;"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test URLs
|
||||||
|
|
||||||
|
| Instance | Web | API | ActivityPub |
|
||||||
|
|----------|-----|-----|-----------|
|
||||||
|
| Solar Network | http://solar.local:5000 | http://solar.local:5000/api | http://solar.local:5000/activitypub |
|
||||||
|
| Mastodon | http://mastodon.local:3001 | http://mastodon.local:3001/api/v1 | http://mastodon.local:3001/inbox |
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Solar Network
|
||||||
|
```bash
|
||||||
|
export SOLAR_DOMAIN="solar.local"
|
||||||
|
export SOLAR_URL="http://solar.local:5000"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mastodon
|
||||||
|
```bash
|
||||||
|
export MASTODON_DOMAIN="mastodon.local"
|
||||||
|
export MASTODON_URL="http://mastodon.local:3001"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful jq Commands
|
||||||
|
|
||||||
|
### Extract Actor ID
|
||||||
|
```bash
|
||||||
|
curl ... | jq '.id'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extract Inbox URL
|
||||||
|
```bash
|
||||||
|
curl ... | jq '.inbox'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extract Public Key
|
||||||
|
```bash
|
||||||
|
curl ... | jq '.publicKey.publicKeyPem'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pretty Print Activity
|
||||||
|
```bash
|
||||||
|
curl ... | jq '.'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extract Activity Type
|
||||||
|
```bash
|
||||||
|
curl ... | jq '.type'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Network Setup
|
||||||
|
|
||||||
|
### /etc/hosts
|
||||||
|
```
|
||||||
|
127.0.0.1 solar.local
|
||||||
|
127.0.0.1 mastodon.local
|
||||||
|
127.0.0.1 gotosocial.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ports Used
|
||||||
|
- Solar Network: 5000
|
||||||
|
- Mastodon: 3001 (web), 4000 (streaming)
|
||||||
|
- GoToSocial: 3002
|
||||||
|
- PostgreSQL: 5432
|
||||||
|
- Redis: 6379
|
||||||
|
- Elasticsearch: 9200
|
||||||
|
|
||||||
|
## File Locations
|
||||||
|
|
||||||
|
### Docker Compose Files
|
||||||
|
- `docker-compose.mastodon-test.yml`
|
||||||
|
- `docker-compose.gotosocial.yml`
|
||||||
|
|
||||||
|
### Environment Files
|
||||||
|
- `.env.mastodon`
|
||||||
|
|
||||||
|
### Data Volumes
|
||||||
|
- `./mastodon-data/`
|
||||||
|
- `./gotosocial-data/`
|
||||||
|
|
||||||
|
## Clean Up Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Reset database
|
||||||
|
psql -d dyson_network <<EOF
|
||||||
|
TRUNCATE fediverse_activities CASCADE;
|
||||||
|
TRUNCATE fediverse_relationships CASCADE;
|
||||||
|
TRUNCATE fediverse_reactions CASCADE;
|
||||||
|
TRUNCATE fediverse_contents CASCADE;
|
||||||
|
TRUNCATE fediverse_actors CASCADE;
|
||||||
|
TRUNCATE fediverse_instances CASCADE;
|
||||||
|
UPDATE publishers SET meta = NULL WHERE meta IS NOT NULL;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Reset everything
|
||||||
|
docker-compose -f docker-compose.mastodon-test.yml down -v
|
||||||
|
docker-compose -f docker-compose.gotosocial.yml down -v
|
||||||
|
psql -d dyson_network <<EOF
|
||||||
|
DROP SCHEMA public CASCADE;
|
||||||
|
CREATE SCHEMA public;
|
||||||
|
EOF
|
||||||
|
dotnet ef database drop
|
||||||
|
dotnet ef database update
|
||||||
|
```
|
||||||
289
docs/activitypub/ACTIVITYPUB_TESTING_QUICKSTART.md
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
# ActivityPub Testing - Quick Start
|
||||||
|
|
||||||
|
This directory contains everything you need to test ActivityPub federation for Solar Network.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Run the Setup Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup-activitypub-test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- ✅ Check prerequisites (Docker, PostgreSQL)
|
||||||
|
- ✅ Update `/etc/hosts` with test domains
|
||||||
|
- ✅ Generate Mastodon environment file
|
||||||
|
- ✅ Create Docker Compose file
|
||||||
|
- ✅ Start Mastodon containers
|
||||||
|
- ✅ Create test Mastodon account
|
||||||
|
- ✅ Apply Solar Network migrations
|
||||||
|
|
||||||
|
### 2. Start Solar Network
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd DysonNetwork.Sphere
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Federation
|
||||||
|
|
||||||
|
Follow the scenarios in [ACTIVITYPUB_TESTING_GUIDE.md](ACTIVITYPUB_TESTING_GUIDE.md)
|
||||||
|
|
||||||
|
## Test Instances
|
||||||
|
|
||||||
|
| Service | URL | Notes |
|
||||||
|
|---------|-----|-------|
|
||||||
|
| Solar Network | http://solar.local:5000 | Your implementation |
|
||||||
|
| Mastodon | http://mastodon.local:3001 | Test instance |
|
||||||
|
| Mastodon Streaming | http://mastodon.local:4000 | WebSocket |
|
||||||
|
|
||||||
|
## Test Accounts
|
||||||
|
|
||||||
|
### Solar Network
|
||||||
|
- Create via UI or API
|
||||||
|
- Username: `solaruser` (or your choice)
|
||||||
|
|
||||||
|
### Mastodon
|
||||||
|
- Username: `testuser@mastodon.local`
|
||||||
|
- Password: `TestPassword123!`
|
||||||
|
- Role: Admin
|
||||||
|
|
||||||
|
## Quick Test Commands
|
||||||
|
|
||||||
|
### Test WebFinger
|
||||||
|
```bash
|
||||||
|
curl "http://solar.local:5000/.well-known/webfinger?resource=acct:solaruser@solar.local"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Actor
|
||||||
|
```bash
|
||||||
|
curl -H "Accept: application/activity+json" \
|
||||||
|
http://solar.local:5000/activitypub/actors/solaruser
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Outbox
|
||||||
|
```bash
|
||||||
|
curl -H "Accept: application/activity+json" \
|
||||||
|
http://solar.local:5000/activitypub/actors/solaruser/outbox
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Follow (from Mastodon)
|
||||||
|
1. Open http://mastodon.local:3001
|
||||||
|
2. Log in as `testuser@mastodon.local`
|
||||||
|
3. Search for `@solaruser@solar.local`
|
||||||
|
4. Click Follow
|
||||||
|
|
||||||
|
### Test Follow (from Solar Network to Mastodon)
|
||||||
|
```bash
|
||||||
|
# Send Follow activity to Solar Network
|
||||||
|
curl -X POST http://solar.local:5000/activitypub/actors/solaruser/inbox \
|
||||||
|
-H "Content-Type: application/activity+json" \
|
||||||
|
-d '{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "http://solar.local:5000/follow-1",
|
||||||
|
"type": "Follow",
|
||||||
|
"actor": "https://solar.local:5000/activitypub/actors/solaruser",
|
||||||
|
"object": "http://mastodon.local:3001/users/testuser"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `ACTIVITYPUB_TESTING_GUIDE.md` | Comprehensive testing guide |
|
||||||
|
| `ACTIVITYPUB_TESTING_QUICKREF.md` | Quick command reference |
|
||||||
|
| `ACTIVITYPUB_IMPLEMENTATION.md` | Implementation details |
|
||||||
|
| `ACTIVITYPUB_SUMMARY.md` | Feature summary |
|
||||||
|
| `ACTIVITYPUB_PLAN.md` | Original implementation plan |
|
||||||
|
|
||||||
|
## Database Checks
|
||||||
|
|
||||||
|
### Connect to Database
|
||||||
|
```bash
|
||||||
|
psql -d dyson_network
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Actors
|
||||||
|
```sql
|
||||||
|
SELECT uri, username, display_name, created_at
|
||||||
|
FROM fediverse_actors;
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Contents
|
||||||
|
```sql
|
||||||
|
SELECT uri, type, content, actor_id, created_at
|
||||||
|
FROM fediverse_contents
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Relationships
|
||||||
|
```sql
|
||||||
|
SELECT state, is_following, is_followed_by, created_at
|
||||||
|
FROM fediverse_relationships;
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Activities
|
||||||
|
```sql
|
||||||
|
SELECT type, status, error_message, created_at
|
||||||
|
FROM fediverse_activities
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 10;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
### Solar Network Logs
|
||||||
|
```bash
|
||||||
|
# Live logs
|
||||||
|
dotnet run --project DysonNetwork.Sphere
|
||||||
|
|
||||||
|
# Follow ActivityPub activity
|
||||||
|
dotnet run --project DysonNetwork.Sphere 2>&1 | grep -i activitypub
|
||||||
|
|
||||||
|
# Debug logging
|
||||||
|
dotnet run --project DysonNetwork.Sphere --logging:LogLevel:DysonNetwork.Sphere.ActivityPub=Trace
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mastodon Logs
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml logs -f
|
||||||
|
|
||||||
|
# Web service only
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml logs -f web
|
||||||
|
|
||||||
|
# Filter for federation
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml logs -f web | grep -i federation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stopping Everything
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop Mastodon
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml down
|
||||||
|
|
||||||
|
# Stop with volume cleanup
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml down -v
|
||||||
|
|
||||||
|
# Restore /etc/hosts
|
||||||
|
sudo mv /etc/hosts.backup /etc/hosts
|
||||||
|
|
||||||
|
# Remove test databases (optional)
|
||||||
|
psql -d dyson_network <<EOF
|
||||||
|
TRUNCATE fediverse_activities CASCADE;
|
||||||
|
TRUNCATE fediverse_relationships CASCADE;
|
||||||
|
TRUNCATE fediverse_reactions CASCADE;
|
||||||
|
TRUNCATE fediverse_contents CASCADE;
|
||||||
|
TRUNCATE fediverse_actors CASCADE;
|
||||||
|
TRUNCATE fediverse_instances CASCADE;
|
||||||
|
UPDATE publishers SET meta = NULL WHERE meta IS NOT NULL;
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Mastodon won't start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml logs -f web
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml restart
|
||||||
|
|
||||||
|
# Recreate
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml down
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can't connect to Solar Network
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if running
|
||||||
|
curl http://solar.local:5000
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
dotnet run --project DysonNetwork.Sphere 2>&1 | grep -i error
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
# Ctrl+C in terminal and run again
|
||||||
|
```
|
||||||
|
|
||||||
|
### Activities not arriving
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check database
|
||||||
|
psql -d dyson_network -c "SELECT * FROM fediverse_activities WHERE status = 3;"
|
||||||
|
|
||||||
|
# Check signature verification logs
|
||||||
|
dotnet run --project DysonNetwork.Sphere 2>&1 | grep -i "signature"
|
||||||
|
|
||||||
|
# Verify actor keys
|
||||||
|
curl -H "Accept: application/activity+json" \
|
||||||
|
http://solar.local:5000/activitypub/actors/solaruser | jq '.publicKey'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Setup script completed successfully
|
||||||
|
- [ ] Mastodon is running and accessible
|
||||||
|
- [ ] Solar Network is running and accessible
|
||||||
|
- [ ] WebFinger returns correct data
|
||||||
|
- [ ] Actor profile includes public key
|
||||||
|
- [ ] Follow from Mastodon to Solar Network works
|
||||||
|
- [ ] Follow from Solar Network to Mastodon works
|
||||||
|
- [ ] Posts from Solar Network appear in Mastodon
|
||||||
|
- [ ] Posts from Mastodon appear in Solar Network database
|
||||||
|
- [ ] Likes federate correctly
|
||||||
|
- [ ] Replies federate correctly
|
||||||
|
- [ ] HTTP signatures are verified
|
||||||
|
- [ ] No errors in logs
|
||||||
|
- [ ] Database contains expected data
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Test with a real instance**:
|
||||||
|
- Get a public domain or use ngrok
|
||||||
|
- Update `ActivityPub:Domain` in appsettings.json
|
||||||
|
- Test with mastodon.social or other public instances
|
||||||
|
|
||||||
|
2. **Add more features**:
|
||||||
|
- Activity queue for async processing
|
||||||
|
- Retry logic for failed deliveries
|
||||||
|
- Metrics and monitoring
|
||||||
|
- Admin interface for federation management
|
||||||
|
|
||||||
|
3. **Test with more instances**:
|
||||||
|
- Pleroma
|
||||||
|
- Pixelfed
|
||||||
|
- Lemmy
|
||||||
|
- PeerTube
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
If something doesn't work:
|
||||||
|
|
||||||
|
1. Check the logs (see Logs section above)
|
||||||
|
2. Review the troubleshooting section in [ACTIVITYPUB_TESTING_GUIDE.md](ACTIVITYPUB_TESTING_GUIDE.md)
|
||||||
|
3. Verify all prerequisites are installed
|
||||||
|
4. Check network connectivity between instances
|
||||||
|
5. Review the [ACTIVITYPUB_IMPLEMENTATION.md](ACTIVITYPUB_IMPLEMENTATION.md) for architecture details
|
||||||
|
|
||||||
|
## Useful URLs
|
||||||
|
|
||||||
|
### Test Instances
|
||||||
|
- Mastodon: http://mastodon.local:3001
|
||||||
|
- Solar Network: http://solar.local:5000
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- ActivityPub W3C Spec: https://www.w3.org/TR/activitypub/
|
||||||
|
- Mastodon Federation Docs: https://docs.joinmastodon.org/admin/federation/
|
||||||
|
- ActivityPub Playground: https://swicth.github.io/activity-pub-playground/
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
- jq: JSON processor (https://stedolan.github.io/jq/)
|
||||||
|
- httpie: HTTP client (https://httpie.io/)
|
||||||
|
- Docker Compose: (https://docs.docker.com/compose/)
|
||||||
275
docs/activitypub/ACTIVITYPUB_TESTING_README.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# ActivityPub Testing Guide
|
||||||
|
|
||||||
|
Complete guide for testing ActivityPub federation in Solar Network.
|
||||||
|
|
||||||
|
## 📚 Documentation Files
|
||||||
|
|
||||||
|
| File | Description | Size |
|
||||||
|
|------|-------------|-------|
|
||||||
|
| `ACTIVITYPUB_TESTING_INDEX.md` | **START HERE** - Master guide with overview | 12K |
|
||||||
|
| `ACTIVITYPUB_TESTING_QUICKSTART.md` | Quick reference for common tasks | 7K |
|
||||||
|
| `ACTIVITYPUB_TESTING_GUIDE.md` | Comprehensive testing scenarios (10 parts) | 19K |
|
||||||
|
| `ACTIVITYPUB_TESTING_QUICKREF.md` | Command and query reference | 8K |
|
||||||
|
| `ACTIVITYPUB_TESTING_HELPER_API.md` | Helper API for programmatic testing | 12K |
|
||||||
|
| `ACTIVITYPUB_TESTING_RESULTS_TEMPLATE.md` | Template to track test results | 10K |
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Option A: One-Command Setup (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Run setup script
|
||||||
|
./setup-activitypub-test.sh
|
||||||
|
|
||||||
|
# 2. Run validation
|
||||||
|
./test-activitypub.sh
|
||||||
|
|
||||||
|
# 3. Start Solar Network
|
||||||
|
cd DysonNetwork.Sphere
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: Manual Setup
|
||||||
|
|
||||||
|
1. **Read**: `ACTIVITYPUB_TESTING_QUICKSTART.md`
|
||||||
|
2. **Configure**: Copy `.env.testing.example` to `.env` and adjust
|
||||||
|
3. **Follow**: Step-by-step in `ACTIVITYPUB_TESTING_GUIDE.md`
|
||||||
|
|
||||||
|
## 🎯 What You Can Test
|
||||||
|
|
||||||
|
### With Self-Hosted Instance
|
||||||
|
- ✅ WebFinger discovery
|
||||||
|
- ✅ Actor profile retrieval
|
||||||
|
- ✅ Follow relationships (bidirectional)
|
||||||
|
- ✅ Post federation (Solar → Mastodon)
|
||||||
|
- ✅ Content reception (Mastodon → Solar)
|
||||||
|
- ✅ Like interactions
|
||||||
|
- ✅ Reply threading
|
||||||
|
- ✅ HTTP signature verification
|
||||||
|
- ✅ Content deletion
|
||||||
|
|
||||||
|
### With Real Instance
|
||||||
|
- ✅ Public domain setup (via ngrok or VPS)
|
||||||
|
- ✅ Federation with public instances (mastodon.social, etc.)
|
||||||
|
- ✅ Real-world compatibility testing
|
||||||
|
- ✅ Performance under real load
|
||||||
|
|
||||||
|
## 📋 Testing Workflow
|
||||||
|
|
||||||
|
### Day 1: Basic Functionality
|
||||||
|
- Setup test environment
|
||||||
|
- Test WebFinger and Actor endpoints
|
||||||
|
- Verify HTTP signatures
|
||||||
|
- Test basic follow/unfollow
|
||||||
|
|
||||||
|
### Day 2: Content Federation
|
||||||
|
- Test post creation and delivery
|
||||||
|
- Test content reception
|
||||||
|
- Test media attachments
|
||||||
|
- Test content warnings
|
||||||
|
|
||||||
|
### Day 3: Interactions
|
||||||
|
- Test likes (both directions)
|
||||||
|
- Test replies and threading
|
||||||
|
- Test boosts/announces
|
||||||
|
- Test undo activities
|
||||||
|
|
||||||
|
### Day 4: Real Instance
|
||||||
|
- Set up public domain
|
||||||
|
- Test with mastodon.social
|
||||||
|
- Test with other instances
|
||||||
|
- Verify cross-instance compatibility
|
||||||
|
|
||||||
|
### Day 5: Edge Cases
|
||||||
|
- Test error handling
|
||||||
|
- Test failed deliveries
|
||||||
|
- Test invalid signatures
|
||||||
|
- Test malformed activities
|
||||||
|
|
||||||
|
## 🛠️ Setup Scripts
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `setup-activitypub-test.sh` | One-command setup of Mastodon + Solar Network |
|
||||||
|
| `test-activitypub.sh` | Quick validation of core functionality |
|
||||||
|
|
||||||
|
Both scripts are executable (`chmod +x`).
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Required Tools
|
||||||
|
- ✅ Docker (for Mastodon)
|
||||||
|
- ✅ .NET 10 SDK (for Solar Network)
|
||||||
|
- ✅ PostgreSQL client (psql)
|
||||||
|
- ✅ curl (for API testing)
|
||||||
|
|
||||||
|
### Quick Setup
|
||||||
|
```bash
|
||||||
|
# 1. Install dependencies (Ubuntu/Debian)
|
||||||
|
sudo apt-get install docker.io docker-compose postgresql-client curl jq
|
||||||
|
|
||||||
|
# 2. Run setup
|
||||||
|
./setup-activitypub-test.sh
|
||||||
|
|
||||||
|
# 3. Validate
|
||||||
|
./test-activitypub.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Progress Tracking
|
||||||
|
|
||||||
|
Use the template to track your testing progress:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy the template
|
||||||
|
cp ACTIVITYPUB_TESTING_RESULTS_TEMPLATE.md my-test-results.md
|
||||||
|
|
||||||
|
# Edit as you test
|
||||||
|
nano my-test-results.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Quick Fixes
|
||||||
|
|
||||||
|
**Mastodon won't start**:
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml logs -f
|
||||||
|
|
||||||
|
# Restart containers
|
||||||
|
docker compose -f docker-compose.mastodon-test.yml restart
|
||||||
|
```
|
||||||
|
|
||||||
|
**Can't reach Solar Network**:
|
||||||
|
```bash
|
||||||
|
# Check if running
|
||||||
|
curl http://solar.local:5000
|
||||||
|
|
||||||
|
# Check /etc/hosts
|
||||||
|
cat /etc/hosts | grep solar.local
|
||||||
|
```
|
||||||
|
|
||||||
|
**Activities not arriving**:
|
||||||
|
```bash
|
||||||
|
# Check database
|
||||||
|
psql -d dyson_network -c "SELECT * FROM fediverse_activities;"
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
dotnet run --project DysonNetwork.Sphere | grep -i activitypub
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed troubleshooting, see `ACTIVITYPUB_TESTING_GUIDE.md` Part 5.
|
||||||
|
|
||||||
|
## 📖 Learning Path
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
1. Read `ACTIVITYPUB_IMPLEMENTATION.md` to understand the architecture
|
||||||
|
2. Read `ACTIVITYPUB_SUMMARY.md` to see what's implemented
|
||||||
|
3. Follow test scenarios in `ACTIVITYPUB_TESTING_GUIDE.md`
|
||||||
|
4. Use helper API in `ACTIVITYPUB_TESTING_HELPER_API.md` for testing
|
||||||
|
|
||||||
|
### For Testers
|
||||||
|
1. Start with `ACTIVITYPUB_TESTING_QUICKSTART.md`
|
||||||
|
2. Use command reference in `ACTIVITYPUB_TESTING_QUICKREF.md`
|
||||||
|
3. Track results with `ACTIVITYPUB_TESTING_RESULTS_TEMPLATE.md`
|
||||||
|
4. Report issues with details from logs
|
||||||
|
|
||||||
|
## 🎓 Success Criteria
|
||||||
|
|
||||||
|
### Minimum Viable
|
||||||
|
- WebFinger works
|
||||||
|
- Actor profile valid
|
||||||
|
- Follow relationships work
|
||||||
|
- Posts federate correctly
|
||||||
|
- HTTP signatures verified
|
||||||
|
|
||||||
|
### Production Ready
|
||||||
|
- Activity queue with retry
|
||||||
|
- Rate limiting
|
||||||
|
- Monitoring/alerting
|
||||||
|
- Admin interface
|
||||||
|
- Instance blocking
|
||||||
|
- Content moderation
|
||||||
|
|
||||||
|
## 🚨 Common Pitfalls
|
||||||
|
|
||||||
|
### Don't Forget
|
||||||
|
- ✅ Update `/etc/hosts` with both instances
|
||||||
|
- ✅ Run migrations before testing
|
||||||
|
- ✅ Check both instances are accessible
|
||||||
|
- ✅ Verify PostgreSQL is running
|
||||||
|
- ✅ Check logs when something fails
|
||||||
|
|
||||||
|
### Watch Out For
|
||||||
|
- ❌ Using `localhost` instead of `solar.local`
|
||||||
|
- ❌ Forgetting to restart after config changes
|
||||||
|
- ❌ Not waiting for Mastodon to start (2-5 minutes)
|
||||||
|
- ❌ Ignoring CORS errors in browser testing
|
||||||
|
- ❌ Testing with deleted/invisible posts
|
||||||
|
|
||||||
|
## 📚 Additional Resources
|
||||||
|
|
||||||
|
### Official Specs
|
||||||
|
- [ActivityPub W3C](https://www.w3.org/TR/activitypub/)
|
||||||
|
- [ActivityStreams](https://www.w3.org/TR/activitystreams-core/)
|
||||||
|
- [HTTP Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
|
||||||
|
|
||||||
|
### Community Guides
|
||||||
|
- [Mastodon Federation](https://docs.joinmastodon.org/admin/federation/)
|
||||||
|
- [Federation Testing](https://docs.joinmastodon.org/spec/activitypub/)
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
- [ActivityPub Playground](https://swicth.github.io/activity-pub-playground/)
|
||||||
|
- [FediTest](https://feditest.com/)
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
If you encounter issues:
|
||||||
|
|
||||||
|
1. Check logs (both Solar Network and Mastodon)
|
||||||
|
2. Review troubleshooting section in the guide
|
||||||
|
3. Validate against success criteria
|
||||||
|
4. Check database state with queries
|
||||||
|
5. Review implementation docs
|
||||||
|
|
||||||
|
## ✨ Next Steps
|
||||||
|
|
||||||
|
After testing with self-hosted instance:
|
||||||
|
|
||||||
|
1. Get a public domain or use ngrok
|
||||||
|
2. Update `ActivityPub:Domain` in appsettings.json
|
||||||
|
3. Test with public Mastodon instances
|
||||||
|
4. Add more ActivityPub features (queue, retry, etc.)
|
||||||
|
5. Implement admin interface
|
||||||
|
6. Add monitoring and metrics
|
||||||
|
|
||||||
|
## 📞 File Reference
|
||||||
|
|
||||||
|
All files are in the root of the DysonNetwork project:
|
||||||
|
|
||||||
|
```
|
||||||
|
DysonNetwork/
|
||||||
|
├── ACTIVITYPUB_TESTING_INDEX.md # Start here!
|
||||||
|
├── ACTIVITYPUB_TESTING_QUICKSTART.md # Quick reference
|
||||||
|
├── ACTIVITYPUB_TESTING_GUIDE.md # Full guide
|
||||||
|
├── ACTIVITYPUB_TESTING_QUICKREF.md # Commands
|
||||||
|
├── ACTIVITYPUB_TESTING_HELPER_API.md # Test API
|
||||||
|
├── ACTIVITYPUB_TESTING_RESULTS_TEMPLATE.md
|
||||||
|
├── setup-activitypub-test.sh # Setup script
|
||||||
|
├── test-activitypub.sh # Test script
|
||||||
|
└── .env.testing.example # Config template
|
||||||
|
```
|
||||||
|
|
||||||
|
**Documentation files** (for reference):
|
||||||
|
```
|
||||||
|
DysonNetwork/
|
||||||
|
├── ACTIVITYPUB_IMPLEMENTATION.md # How it's implemented
|
||||||
|
├── ACTIVITYPUB_SUMMARY.md # Feature summary
|
||||||
|
└── ACTIVITYPUB_PLAN.md # Original plan
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Start here**: `ACTIVITYPUB_TESTING_INDEX.md`
|
||||||
|
|
||||||
|
**Good luck with your testing!** 🚀
|
||||||
282
docs/activitypub/ACTIVITYPUB_TEST_RESULTS_TEMPLATE.md
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
# ActivityPub Testing Results Template
|
||||||
|
|
||||||
|
Use this template to track your testing progress.
|
||||||
|
|
||||||
|
## Test Environment
|
||||||
|
|
||||||
|
**Date**: ________________
|
||||||
|
|
||||||
|
**Test Configuration**:
|
||||||
|
- Solar Network URL: `http://solar.local:5000`
|
||||||
|
- Mastodon URL: `http://mastodon.local:3001`
|
||||||
|
- Database: `dyson_network`
|
||||||
|
|
||||||
|
**Solar Network User**:
|
||||||
|
- Username: `_______________`
|
||||||
|
- Publisher ID: `_______________`
|
||||||
|
|
||||||
|
**Mastodon User**:
|
||||||
|
- Username: `testuser@mastodon.local`
|
||||||
|
- Password: `TestPassword123!`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
### ✅ Part 1: Infrastructure Setup
|
||||||
|
|
||||||
|
| Test | Status | Notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| Setup script ran successfully | ☐ ☑ | |
|
||||||
|
| /etc/hosts updated | ☐ ☑ | |
|
||||||
|
| Docker containers started | ☐ ☑ | |
|
||||||
|
| Mastodon web accessible | ☐ ☑ | |
|
||||||
|
| Mastodon admin account created | ☐ ☑ | |
|
||||||
|
| Database migrations applied | ☐ ☑ | |
|
||||||
|
| Solar Network started | ☐ ☑ | |
|
||||||
|
|
||||||
|
### ✅ Part 2: WebFinger & Actor Discovery
|
||||||
|
|
||||||
|
| Test | Status | Expected | Actual |
|
||||||
|
|------|--------|---------|--------|
|
||||||
|
| WebFinger for Solar Network user | ☐ ☑ | Returns subject + links | _______________ |
|
||||||
|
| Actor profile JSON is valid | ☐ ☑ | Has id, type, inbox, outbox | _______________ |
|
||||||
|
| Public key present in actor | ☐ ☑ | publicKey.publicKeyPem exists | _______________ |
|
||||||
|
| Outbox returns public posts | ☐ ☑ | OrderedCollection with items | _______________ |
|
||||||
|
| Outbox totalItems count | ☐ ☑ | Matches public posts | _______________ |
|
||||||
|
|
||||||
|
### ✅ Part 3: Follow Relationships
|
||||||
|
|
||||||
|
| Test | Status | Expected Result | Actual Result |
|
||||||
|
|------|--------|----------------|---------------|
|
||||||
|
| Mastodon follows Solar Network user | ☐ ☑ | Relationship created in DB | _______________ |
|
||||||
|
| Accept sent to Mastodon | ☐ ☑ | Mastodon receives Accept | _______________ |
|
||||||
|
| Solar Network follows Mastodon user | ☐ ☑ | Relationship created | _______________ |
|
||||||
|
| Follow appears in Mastodon UI | ☐ ☑ | Mastodon shows "Following" | _______________ |
|
||||||
|
| Follow appears in Solar Network DB | ☐ ☑ | is_following = true | _______________ |
|
||||||
|
| Follow state is Accepted | ☐ ☑ | state = 1 (Accepted) | _______________ |
|
||||||
|
| Unfollow works correctly | ☐ ☑ | Relationship deleted/updated | _______________ |
|
||||||
|
|
||||||
|
### ✅ Part 4: Content Federation (Create)
|
||||||
|
|
||||||
|
| Test | Status | Expected Result | Actual Result |
|
||||||
|
|------|--------|----------------|---------------|
|
||||||
|
| Post created in Solar Network | ☐ ☑ | Post in sn_posts table | _______________ |
|
||||||
|
| Activity sent to Mastodon | ☐ ☑ | Logged as successful | _______________ |
|
||||||
|
| Post appears in Mastodon timeline | ☐ ☑ | Visible in federated timeline | _______________ |
|
||||||
|
| Post content matches | ☐ ☑ | Same text/HTML | _______________ |
|
||||||
|
| Post author is correct | ☐ ☑ | Shows Solar Network user | _______________ |
|
||||||
|
| Post timestamp is correct | ☐ ☑ | Same published time | _______________ |
|
||||||
|
| Multiple posts federate | ☐ ☑ | All posts appear | _______________ |
|
||||||
|
|
||||||
|
### ✅ Part 5: Content Reception (Incoming Create)
|
||||||
|
|
||||||
|
| Test | Status | Expected Result | Actual Result |
|
||||||
|
|------|--------|----------------|---------------|
|
||||||
|
| Create activity received | ☐ ☑ | Activity logged in DB | _______________ |
|
||||||
|
| Content stored in fediverse_contents | ☐ ☑ | Record with correct type | _______________ |
|
||||||
|
| Content not duplicated | ☐ ☑ | Only one entry per URI | _______________ |
|
||||||
|
| Actor created/retrieved | ☐ ☑ | Actor in fediverse_actors | _______________ |
|
||||||
|
| Instance created/retrieved | ☐ ☑ | Instance in fediverse_instances | _______________ |
|
||||||
|
| Content HTML preserved | ☐ ☑ | contentHtml field populated | _______________ |
|
||||||
|
|
||||||
|
### ✅ Part 6: Reaction Federation (Like)
|
||||||
|
|
||||||
|
| Test | Status | Expected Result | Actual Result |
|
||||||
|
|------|--------|----------------|---------------|
|
||||||
|
| Like from Mastodon to Solar post | ☐ ☑ | Like activity received | _______________ |
|
||||||
|
| Reaction stored in fediverse_reactions | ☐ ☑ | Record with type = 0 (Like) | _______________ |
|
||||||
|
| Like count incremented | ☐ ☑ | like_count increased | _______________ |
|
||||||
|
| Like appears in UI | ☐ ☑ | Visible on Solar Network | _______________ |
|
||||||
|
| Like appears in Mastodon | ☐ ☑ | Visible on Mastodon | _______________ |
|
||||||
|
| Unlike works correctly | ☐ ☑ | Like removed | _______________ |
|
||||||
|
|
||||||
|
### ✅ Part 7: Reply Federation
|
||||||
|
|
||||||
|
| Test | Status | Expected Result | Actual Result |
|
||||||
|
|------|--------|----------------|---------------|
|
||||||
|
| Reply from Mastodon to Solar post | ☐ ☑ | Create activity with inReplyTo | _______________ |
|
||||||
|
| Reply stored with parent reference | ☐ ☑ | in_reply_to field set | _______________ |
|
||||||
|
| Reply appears in Solar Network | ☐ ☑ | Visible as comment | _______________ |
|
||||||
|
| Reply shows parent context | ☐ ☑ | Links to original post | _______________ |
|
||||||
|
|
||||||
|
### ✅ Part 8: Content Deletion
|
||||||
|
|
||||||
|
| Test | Status | Expected Result | Actual Result |
|
||||||
|
|------|--------|----------------|---------------|
|
||||||
|
| Delete from Mastodon | ☐ ☑ | Delete activity received | _______________ |
|
||||||
|
| Content soft-deleted | ☐ ☑ | deleted_at timestamp set | _______________ |
|
||||||
|
| Content no longer visible | ☐ ☑ | Hidden from timelines | _______________ |
|
||||||
|
|
||||||
|
### ✅ Part 9: HTTP Signature Verification
|
||||||
|
|
||||||
|
| Test | Status | Expected Result | Actual Result |
|
||||||
|
|------|--------|----------------|---------------|
|
||||||
|
| Valid signature accepted | ☐ ☑ | Activity processed | _______________ |
|
||||||
|
| Invalid signature rejected | ☐ ☑ | 401 Unauthorized | _______________ |
|
||||||
|
| Missing signature rejected | ☐ ☑ | 401 Unauthorized | _______________ |
|
||||||
|
| Signature format correct | ☐ ☑ | keyId, algorithm, headers, signature | _______________ |
|
||||||
|
| Signing string correct | ☐ ☑ | Matches HTTP-Signatures draft | _______________ |
|
||||||
|
|
||||||
|
### ✅ Part 10: Error Handling
|
||||||
|
|
||||||
|
| Test | Status | Expected Result | Actual Result |
|
||||||
|
|------|--------|----------------|---------------|
|
||||||
|
| Invalid activity type rejected | ☐ ☑ | 400 Bad Request | _______________ |
|
||||||
|
| Malformed JSON rejected | ☐ ☑ | 400 Bad Request | _______________ |
|
||||||
|
| Non-existent actor rejected | ☐ ☑ | 404 Not Found | _______________ |
|
||||||
|
| Errors logged correctly | ☐ ☑ | error_message populated | _______________ |
|
||||||
|
| Activity status = Failed | ☐ ☑ | status = 3 | _______________ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database State After Tests
|
||||||
|
|
||||||
|
### Actors Table
|
||||||
|
```sql
|
||||||
|
SELECT COUNT(*) as total_actors,
|
||||||
|
SUM(CASE WHEN is_local_actor THEN 1 ELSE 0 END) as local,
|
||||||
|
SUM(CASE WHEN NOT is_local_actor THEN 1 ELSE 0 END) as remote
|
||||||
|
FROM fediverse_relationships;
|
||||||
|
```
|
||||||
|
- Total Actors: _______________
|
||||||
|
- Local Actors: _______________
|
||||||
|
- Remote Actors: _______________
|
||||||
|
|
||||||
|
### Contents Table
|
||||||
|
```sql
|
||||||
|
SELECT COUNT(*) as total_contents,
|
||||||
|
AVG(LENGTH(content)) as avg_content_length
|
||||||
|
FROM fediverse_contents WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
- Total Contents: _______________
|
||||||
|
- Avg Content Length: _______________
|
||||||
|
|
||||||
|
### Activities Table
|
||||||
|
```sql
|
||||||
|
SELECT type, status, COUNT(*)
|
||||||
|
FROM fediverse_activities
|
||||||
|
GROUP BY type, status
|
||||||
|
ORDER BY type, status;
|
||||||
|
```
|
||||||
|
- Activities by Type/Status:
|
||||||
|
- Create: Pending ___, Completed ____, Failed ___
|
||||||
|
- Follow: Pending ___, Completed ____, Failed ___
|
||||||
|
- Like: Pending ___, Completed ____, Failed ___
|
||||||
|
- Accept: Pending ___, Completed ____, Failed ___
|
||||||
|
|
||||||
|
### Relationships Table
|
||||||
|
```sql
|
||||||
|
SELECT state, COUNT(*) as count
|
||||||
|
FROM fediverse_relationships
|
||||||
|
GROUP BY state;
|
||||||
|
```
|
||||||
|
- Pending: _______________
|
||||||
|
- Accepted: _______________
|
||||||
|
- Rejected: _______________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logs Analysis
|
||||||
|
|
||||||
|
### Solar Network Errors Found:
|
||||||
|
1. _______________
|
||||||
|
2. _______________
|
||||||
|
3. _______________
|
||||||
|
|
||||||
|
### Mastodon Errors Found:
|
||||||
|
1. _______________
|
||||||
|
2. _______________
|
||||||
|
3. _______________
|
||||||
|
|
||||||
|
### Warnings Found:
|
||||||
|
1. _______________
|
||||||
|
2. _______________
|
||||||
|
3. _______________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issues & Bugs Found
|
||||||
|
|
||||||
|
| # | Severity | Description | Status |
|
||||||
|
|---|----------|-------------|--------|
|
||||||
|
| 1 | ☐ Low/Medium/High/Critical | _____________________ | ☐ Open/☐ Fixed |
|
||||||
|
| 2 | ☐ Low/Medium/High/Critical | _____________________ | ☐ Open/☐ Fixed |
|
||||||
|
| 3 | ☐ Low/Medium/High/Critical | _____________________ | ☐ Open/☐ Fixed |
|
||||||
|
| 4 | ☐ Low/Medium/High/Critical | _____________________ | ☐ Open/☐ Fixed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
|
| Metric | Value | Notes |
|
||||||
|
|--------|-------|-------|
|
||||||
|
| Average activity processing time | __________ ms | |
|
||||||
|
| Average HTTP signature verification time | __________ ms | |
|
||||||
|
| Outgoing delivery success rate | __________% | |
|
||||||
|
| Average WebFinger response time | __________ ms | |
|
||||||
|
| Database query performance | __________ | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Compatibility Notes
|
||||||
|
|
||||||
|
| Instance | Version | Works | Notes |
|
||||||
|
|----------|---------|--------|-------|
|
||||||
|
| Mastodon (self-hosted) | latest | ☐ ☑ | |
|
||||||
|
| Mastodon.social | ~4.0 | ☐ ☑ | |
|
||||||
|
| Pleroma | ~2.5 | ☐ ☑ | |
|
||||||
|
| GoToSocial | ~0.15 | ☐ ☑ | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### What Worked Well:
|
||||||
|
1. _____________________
|
||||||
|
2. _____________________
|
||||||
|
3. _____________________
|
||||||
|
|
||||||
|
### What Needs Improvement:
|
||||||
|
1. _____________________
|
||||||
|
2. _____________________
|
||||||
|
3. _____________________
|
||||||
|
|
||||||
|
### Features to Add:
|
||||||
|
1. _____________________
|
||||||
|
2. _____________________
|
||||||
|
3. _____________________
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Testing Phase
|
||||||
|
|
||||||
|
- [ ] Test with public Mastodon instance
|
||||||
|
- [ ] Test with Pleroma instance
|
||||||
|
- [ ] Test media attachment federation
|
||||||
|
- [ ] Test with high-volume posts
|
||||||
|
- [ ] Test concurrent activity processing
|
||||||
|
- [ ] Test with different visibility levels
|
||||||
|
- [ ] Test with long posts (>500 chars)
|
||||||
|
- [ ] Test with special characters/emojis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sign-off
|
||||||
|
|
||||||
|
**Tested By**: _____________________
|
||||||
|
|
||||||
|
**Test Date**: _____________________
|
||||||
|
|
||||||
|
**Overall Result**: ☐ Pass / ☐ Fail
|
||||||
|
|
||||||
|
**Ready for Production**: ☐ Yes / ☐ No
|
||||||
|
|
||||||
|
**Notes**: ___________________________________________________________________________
|
||||||
|
|
||||||
|
__________________________________________________________________________
|
||||||
|
|
||||||
|
__________________________________________________________________________
|
||||||
|
|
||||||
|
__________________________________________________________________________
|
||||||
|
|
||||||
425
docs/activitypub/FOLLOWING_USERS_GUIDE.md
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
# Follow Feature - User Guide
|
||||||
|
|
||||||
|
## Quick Start: How to Follow Fediverse Users
|
||||||
|
|
||||||
|
### Method 1: Via Search (Recommended)
|
||||||
|
|
||||||
|
1. Go to the search bar in Solar Network
|
||||||
|
2. Type the user's full address: `@username@domain.com`
|
||||||
|
- Example: `@alice@mastodon.social`
|
||||||
|
- Example: `@bob@pleroma.site`
|
||||||
|
3. Click on their profile in search results
|
||||||
|
4. Click the "Follow" button
|
||||||
|
5. Wait for acceptance (usually immediate)
|
||||||
|
6. ✅ Done! Their posts will now appear in your timeline
|
||||||
|
|
||||||
|
### Method 2: Via Profile URL
|
||||||
|
|
||||||
|
1. If you know their profile URL, visit it directly:
|
||||||
|
- Example: `https://mastodon.social/@alice`
|
||||||
|
2. Look for the "Follow" button on their profile
|
||||||
|
3. Click it to follow
|
||||||
|
4. ✅ You're now following them!
|
||||||
|
|
||||||
|
## What Happens When You Follow Someone
|
||||||
|
|
||||||
|
### The Technical Flow
|
||||||
|
```
|
||||||
|
You click "Follow"
|
||||||
|
↓
|
||||||
|
Solar Network creates Follow Activity
|
||||||
|
↓
|
||||||
|
Follow Activity is signed with your private key
|
||||||
|
↓
|
||||||
|
Solar Network sends Follow to their instance's inbox
|
||||||
|
↓
|
||||||
|
Their instance verifies your signature
|
||||||
|
↓
|
||||||
|
Their instance processes the Follow
|
||||||
|
↓
|
||||||
|
Their instance sends Accept Activity back
|
||||||
|
↓
|
||||||
|
Solar Network receives and processes Accept
|
||||||
|
↓
|
||||||
|
Relationship is stored in database
|
||||||
|
↓
|
||||||
|
Their public posts federate to Solar Network
|
||||||
|
```
|
||||||
|
|
||||||
|
### What You'll See
|
||||||
|
|
||||||
|
- ✅ **"Following..."** (while waiting for acceptance)
|
||||||
|
- ✅ **"Following" ✓** (when accepted)
|
||||||
|
- ✅ **Their posts** in your home timeline
|
||||||
|
- ✅ **Their likes, replies, boosts** on your posts
|
||||||
|
|
||||||
|
## Different Types of Accounts
|
||||||
|
|
||||||
|
### Regular Users
|
||||||
|
- Full ActivityPub support
|
||||||
|
- Follows work both ways
|
||||||
|
- Content federates normally
|
||||||
|
- Example: `@alice@mastodon.social`
|
||||||
|
|
||||||
|
### Locked Accounts
|
||||||
|
- User must manually approve followers
|
||||||
|
- You'll see "Pending" after clicking follow
|
||||||
|
- User receives notification to approve/deny
|
||||||
|
- Example: `@private@pleroma.site`
|
||||||
|
|
||||||
|
### Bot/Service Accounts
|
||||||
|
- Automated content accounts
|
||||||
|
- Often auto-accept follows
|
||||||
|
- Example: `@newsbot@botsin.space`
|
||||||
|
|
||||||
|
### Organizational Accounts
|
||||||
|
- Group or team accounts
|
||||||
|
- Example: `@team@company.social`
|
||||||
|
|
||||||
|
## Managing Your Follows
|
||||||
|
|
||||||
|
### View Who You're Following
|
||||||
|
|
||||||
|
**Go to**: Following page or `GET /api/activitypub/following`
|
||||||
|
|
||||||
|
You'll see:
|
||||||
|
- Username
|
||||||
|
- Display name
|
||||||
|
- Profile picture
|
||||||
|
- When you followed them
|
||||||
|
- Their instance (e.g., "Mastodon")
|
||||||
|
|
||||||
|
### Unfollowing Someone
|
||||||
|
|
||||||
|
**Method 1: Via UI**
|
||||||
|
1. Go to their profile
|
||||||
|
2. Click "Following" button (shows as active)
|
||||||
|
3. Click to unfollow
|
||||||
|
|
||||||
|
**Method 2: Via API**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://solar.local:5000/api/activitypub/unfollow \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"targetActorUri": "https://mastodon.social/users/alice"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Unfollowed successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Your Followers
|
||||||
|
|
||||||
|
**Go to**: Followers page or `GET /api/activitypub/followers`
|
||||||
|
|
||||||
|
You'll see:
|
||||||
|
- Users following you
|
||||||
|
- Their instance
|
||||||
|
- When they started following
|
||||||
|
- Whether they're local or from another instance
|
||||||
|
|
||||||
|
## Searching Fediverse Users
|
||||||
|
|
||||||
|
### How Search Works
|
||||||
|
|
||||||
|
1. **Type in search bar**: `@username@domain.com`
|
||||||
|
2. **Solar Network queries their instance**:
|
||||||
|
- Fetches their actor profile
|
||||||
|
- Checks if they're discoverable
|
||||||
|
3. **Shows results**:
|
||||||
|
- Profile picture
|
||||||
|
- Display name
|
||||||
|
- Bio
|
||||||
|
- Instance name
|
||||||
|
|
||||||
|
### Supported Search Formats
|
||||||
|
|
||||||
|
| Format | Example | Works? |
|
||||||
|
|--------|---------|--------|
|
||||||
|
| Full handle | `@alice@mastodon.social` | ✅ Yes |
|
||||||
|
| Username only | `alice` | ⚠️ May search local users first |
|
||||||
|
| Full URL | `https://mastodon.social/@alice` | ✅ Yes |
|
||||||
|
|
||||||
|
## Privacy Considerations
|
||||||
|
|
||||||
|
### Public Posts
|
||||||
|
- **What**: Posts visible to everyone
|
||||||
|
- **Federation**: ✅ Federates to all followers
|
||||||
|
- **Timeline**: Visible in public federated timelines
|
||||||
|
- **Example**: General updates, thoughts, content you want to share
|
||||||
|
|
||||||
|
### Private Posts
|
||||||
|
- **What**: Posts only visible to followers
|
||||||
|
- **Federation**: ✅ Federates to followers (including remote)
|
||||||
|
- **Timeline**: Only visible to your followers
|
||||||
|
- **Example**: Personal updates, questions
|
||||||
|
|
||||||
|
### Unlisted Posts
|
||||||
|
- **What**: Posts not in public timelines
|
||||||
|
- **Federation**: ✅ Federates but marked unlisted
|
||||||
|
- **Timeline**: Only followers see it
|
||||||
|
- **Example**: Limited audience content
|
||||||
|
|
||||||
|
### Followers-Only Posts
|
||||||
|
- **What**: Posts only to followers, no federated boost
|
||||||
|
- **Federation**: ⚠️ May not federate fully
|
||||||
|
- **Timeline**: Only your followers
|
||||||
|
- **Example**: Very sensitive content
|
||||||
|
|
||||||
|
## Following Etiquette
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **Check before following**:
|
||||||
|
- Read their bio and recent posts
|
||||||
|
- Make sure they're who you think they are
|
||||||
|
- Check if their content aligns with your interests
|
||||||
|
|
||||||
|
2. **Start with interactions**:
|
||||||
|
- Like a few posts first
|
||||||
|
- Reply thoughtfully
|
||||||
|
- Share interesting content
|
||||||
|
- Then follow if you want to see more
|
||||||
|
|
||||||
|
3. **Respect instance culture**:
|
||||||
|
- Each instance has its own norms
|
||||||
|
- Read their community guidelines
|
||||||
|
- Be mindful of local rules
|
||||||
|
|
||||||
|
4. **Don't spam**:
|
||||||
|
- Don't mass-follow users
|
||||||
|
- Don't send unwanted DMs
|
||||||
|
- Don't repeatedly like old posts
|
||||||
|
|
||||||
|
5. **Use appropriate post visibility**:
|
||||||
|
- Public for general content
|
||||||
|
- Unlisted for updates to followers
|
||||||
|
- Private for sensitive topics
|
||||||
|
|
||||||
|
### Red Flags to Watch
|
||||||
|
|
||||||
|
1. **Suspicious accounts**:
|
||||||
|
- Newly created with generic content
|
||||||
|
- Only posting promotional links
|
||||||
|
- Unusual following patterns
|
||||||
|
|
||||||
|
2. **Instances with poor moderation**:
|
||||||
|
- Lots of spam in public timelines
|
||||||
|
- Harassment goes unaddressed
|
||||||
|
- You may want to block the instance
|
||||||
|
|
||||||
|
3. **Content warnings not respected**:
|
||||||
|
- Users posting unmarked sensitive content
|
||||||
|
- You can report/block these users
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Follow button doesn't work"
|
||||||
|
|
||||||
|
**Possible causes**:
|
||||||
|
1. User doesn't exist
|
||||||
|
2. Instance is down
|
||||||
|
3. Network connectivity issue
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
1. Verify the username/domain is correct
|
||||||
|
2. Try searching for them again
|
||||||
|
3. Check your internet connection
|
||||||
|
4. Try again in a few minutes
|
||||||
|
|
||||||
|
### "User doesn't appear in Following list"
|
||||||
|
|
||||||
|
**Possible causes**:
|
||||||
|
1. Follow was rejected (locked account)
|
||||||
|
2. Follow is still pending
|
||||||
|
3. Error in federation
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
1. Check if their account is locked
|
||||||
|
2. Wait a few minutes for acceptance
|
||||||
|
3. Check your ActivityPub logs
|
||||||
|
4. Try following again
|
||||||
|
|
||||||
|
### "Can't find a user via search"
|
||||||
|
|
||||||
|
**Possible causes**:
|
||||||
|
1. Username/domain is wrong
|
||||||
|
2. User's instance is blocking your instance
|
||||||
|
3. User's profile is not discoverable
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
1. Double-check the spelling
|
||||||
|
2. Try their full URL: `https://instance.com/@username`
|
||||||
|
3. Check if they're from a blocked instance
|
||||||
|
4. Contact them directly for their handle
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Follow a Remote User
|
||||||
|
|
||||||
|
**Endpoint**: `POST /api/activitypub/follow`
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"targetActorUri": "https://mastodon.social/users/alice"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Follow request sent. Waiting for acceptance.",
|
||||||
|
"targetActorUri": "https://mastodon.social/users/alice"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Your Following
|
||||||
|
|
||||||
|
**Endpoint**: `GET /api/activitypub/following?limit=50`
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"actorUri": "https://mastodon.social/users/alice",
|
||||||
|
"username": "alice",
|
||||||
|
"displayName": "Alice Smith",
|
||||||
|
"bio": "I love tech!",
|
||||||
|
"avatarUrl": "https://...",
|
||||||
|
"followedAt": "2024-01-15T10:30:00Z",
|
||||||
|
"isLocal": false,
|
||||||
|
"instanceDomain": "mastodon.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Your Followers
|
||||||
|
|
||||||
|
**Endpoint**: `GET /api/activitypub/followers?limit=50`
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"actorUri": "https://pleroma.site/users/bob",
|
||||||
|
"username": "bob",
|
||||||
|
"displayName": "Bob Jones",
|
||||||
|
"bio": "Federated user following me",
|
||||||
|
"avatarUrl": "https://...",
|
||||||
|
"followedAt": "2024-01-10T14:20:00Z",
|
||||||
|
"isLocal": false,
|
||||||
|
"instanceDomain": "pleroma.site"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Users
|
||||||
|
|
||||||
|
**Endpoint**: `GET /api/activitypub/search?query=@alice@domain.com&limit=20`
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"actorUri": "https://mastodon.social/users/alice",
|
||||||
|
"username": "alice",
|
||||||
|
"displayName": "Alice Smith",
|
||||||
|
"bio": "Tech enthusiast",
|
||||||
|
"avatarUrl": "https://...",
|
||||||
|
"isLocal": false,
|
||||||
|
"instanceDomain": "mastodon.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real Examples
|
||||||
|
|
||||||
|
### Example 1: Following a Mastodon User
|
||||||
|
|
||||||
|
**What you do**:
|
||||||
|
1. Search for `@alice@mastodon.social`
|
||||||
|
2. Click on Alice's profile
|
||||||
|
3. Click "Follow" button
|
||||||
|
4. Wait 1-2 seconds
|
||||||
|
5. ✅ Alice appears in your "Following" list
|
||||||
|
6. ✅ Alice's public posts appear in your timeline
|
||||||
|
|
||||||
|
**What happens technically**:
|
||||||
|
- Solar Network sends Follow to Alice's Mastodon instance
|
||||||
|
- Alice's Mastodon auto-accepts (unless locked)
|
||||||
|
- Mastodon sends Accept back to Solar Network
|
||||||
|
- Relationship stored in both databases
|
||||||
|
- Alice's future posts federate to Solar Network
|
||||||
|
|
||||||
|
### Example 2: Following a Locked Account
|
||||||
|
|
||||||
|
**What you do**:
|
||||||
|
1. Search for `@private@pleroma.site`
|
||||||
|
2. Click "Follow" button
|
||||||
|
3. ✅ See "Following..." (pending)
|
||||||
|
4. Wait for user to approve
|
||||||
|
|
||||||
|
**What happens technically**:
|
||||||
|
- Solar Network sends Follow to private@pleroma.site
|
||||||
|
- Private user receives notification
|
||||||
|
- Private user manually approves the request
|
||||||
|
- Private user's instance sends Accept
|
||||||
|
- ✅ Now following!
|
||||||
|
|
||||||
|
### Example 3: Following a Bot Account
|
||||||
|
|
||||||
|
**What you do**:
|
||||||
|
1. Search for `@news@botsin.space`
|
||||||
|
2. Click "Follow" button
|
||||||
|
3. ✅ Immediately following (bots auto-accept)
|
||||||
|
|
||||||
|
**What happens technically**:
|
||||||
|
- Follow is auto-accepted
|
||||||
|
- News posts appear in your timeline
|
||||||
|
- Regular updates from the bot
|
||||||
|
|
||||||
|
## Key Differences from Traditional Social Media
|
||||||
|
|
||||||
|
| Aspect | Traditional Social | ActivityPub |
|
||||||
|
|---------|------------------|-------------|
|
||||||
|
| Central server | ❌ No | ✅ Yes (per instance) |
|
||||||
|
| Multiple platforms | ❌ No | ✅ Yes (Mastodon, Pleroma, etc.) |
|
||||||
|
| Data ownership | ❌ On their servers | ✅ On your server |
|
||||||
|
| Blocking | ❌ One platform | ✅ Per instance |
|
||||||
|
| Migration | ❌ Difficult | ✅ Use your own domain |
|
||||||
|
| Federation | ❌ No | ✅ Built-in |
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
If you have issues following users:
|
||||||
|
|
||||||
|
1. **Check the main guide**: See `HOW_TO_FOLLOW_FEDIVERSE_USERS.md`
|
||||||
|
2. **Check your logs**: Look for ActivityPub errors
|
||||||
|
3. **Test the API**: Use curl to test follow endpoints directly
|
||||||
|
4. **Verify the user**: Make sure the user exists on their instance
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Following fediverse users in Solar Network:
|
||||||
|
|
||||||
|
1. **Simple**: Just search and click "Follow"
|
||||||
|
2. **Works both ways**: You can follow them, they can follow you
|
||||||
|
3. **Works across instances**: Mastodon, Pleroma, Lemmy, etc.
|
||||||
|
4. **Federated content**: Their posts appear in your timeline
|
||||||
|
5. **Full interactions**: Like, reply, boost their posts
|
||||||
|
|
||||||
|
It works just like following on any other social platform, but with the added benefit of being able to follow users on completely different services! 🌍
|
||||||
406
docs/activitypub/FOLLOW_FEDIVERSE_USER.md
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
# How to Follow (Subscribe to) Fediverse Users in Solar Network
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
In ActivityPub terminology, "subscribing" to a user is called **"following"**. This guide explains how users in Solar Network can follow users from other federated services (Mastodon, Pleroma, etc.).
|
||||||
|
|
||||||
|
## User Guide: How to Follow Fediverse Users
|
||||||
|
|
||||||
|
### Method 1: Via Search (Recommended)
|
||||||
|
|
||||||
|
1. **Search for the user**:
|
||||||
|
- Type their full address in the search bar: `@username@domain.com`
|
||||||
|
- Example: `@alice@mastodon.social`
|
||||||
|
- Example: `@bob@pleroma.site`
|
||||||
|
|
||||||
|
2. **View their profile**:
|
||||||
|
- Click on the search result
|
||||||
|
- You'll see their profile, bio, and recent posts
|
||||||
|
|
||||||
|
3. **Click "Follow" button**:
|
||||||
|
- Solar Network sends a Follow activity to their instance
|
||||||
|
- The remote instance will send back an Accept
|
||||||
|
- The user now appears in your "Following" list
|
||||||
|
|
||||||
|
### Method 2: Via Profile URL
|
||||||
|
|
||||||
|
1. **Visit their profile directly**:
|
||||||
|
- If you know their profile URL, visit it directly
|
||||||
|
- Example: `https://mastodon.social/@alice`
|
||||||
|
|
||||||
|
2. **Look for "Follow" button**:
|
||||||
|
- Click it to follow
|
||||||
|
|
||||||
|
3. **Confirm the follow**:
|
||||||
|
- Solar Network will send the follow request
|
||||||
|
- Wait for acceptance (usually immediate)
|
||||||
|
|
||||||
|
## What Happens Behind the Scenes
|
||||||
|
|
||||||
|
### The Follow Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User clicks "Follow"
|
||||||
|
↓
|
||||||
|
Solar Network creates Follow Activity
|
||||||
|
↓
|
||||||
|
Solar Network signs with publisher's private key
|
||||||
|
↓
|
||||||
|
Solar Network sends to remote user's inbox
|
||||||
|
↓
|
||||||
|
Remote instance verifies signature
|
||||||
|
↓
|
||||||
|
Remote instance processes the Follow
|
||||||
|
↓
|
||||||
|
Remote instance sends Accept Activity back
|
||||||
|
↓
|
||||||
|
Solar Network receives and processes Accept
|
||||||
|
↓
|
||||||
|
Relationship is established!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Timeline Integration
|
||||||
|
|
||||||
|
Once you're following a user:
|
||||||
|
- ✅ Their public posts appear in your "Home" timeline
|
||||||
|
- ✅ Their posts are federated to your followers
|
||||||
|
- ✅ Their likes, replies, and boosts are visible
|
||||||
|
- ✅ You can interact with their content
|
||||||
|
|
||||||
|
## Following Different Types of Accounts
|
||||||
|
|
||||||
|
### Individual Users
|
||||||
|
- **What**: Regular users like you
|
||||||
|
- **Example**: `@alice@mastodon.social`
|
||||||
|
- **Works**: ✅ Full support
|
||||||
|
|
||||||
|
### Organizational/Bot Accounts
|
||||||
|
- **What**: Groups, bots, or organizations
|
||||||
|
- **Example**: `@official@newsbot.site`
|
||||||
|
- **Works**: ✅ Full support
|
||||||
|
|
||||||
|
### Locked Accounts
|
||||||
|
- **What**: Users who manually approve followers
|
||||||
|
- **Example**: `@private@pleroma.site`
|
||||||
|
- **Works**: ✅ Follow request sent, waits for approval
|
||||||
|
|
||||||
|
## Managing Your Follows
|
||||||
|
|
||||||
|
### View Who You're Following
|
||||||
|
|
||||||
|
**API Endpoint**: `GET /api/activitypub/following`
|
||||||
|
|
||||||
|
**Response Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"actorUri": "https://mastodon.social/users/alice",
|
||||||
|
"username": "alice",
|
||||||
|
"displayName": "Alice Smith",
|
||||||
|
"bio": "I love tech and coffee! ☕",
|
||||||
|
"avatarUrl": "https://cdn.mastodon.social/avatars/...",
|
||||||
|
"followedAt": "2024-01-15T10:30:00Z",
|
||||||
|
"isLocal": false,
|
||||||
|
"instanceDomain": "mastodon.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unfollowing Someone
|
||||||
|
|
||||||
|
**API Endpoint**: `POST /api/activitypub/unfollow`
|
||||||
|
|
||||||
|
**Request Body**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"targetActorUri": "https://mastodon.social/users/alice"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Unfollowed successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Searching Fediverse Users
|
||||||
|
|
||||||
|
**API Endpoint**: `GET /api/activitypub/search?query=@username@domain.com`
|
||||||
|
|
||||||
|
**Response Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"actorUri": "https://mastodon.social/users/alice",
|
||||||
|
"username": "alice",
|
||||||
|
"displayName": "Alice Smith",
|
||||||
|
"bio": "Software developer | Mastodon user",
|
||||||
|
"avatarUrl": "https://cdn.mastodon.social/avatars/...",
|
||||||
|
"isLocal": false,
|
||||||
|
"instanceDomain": "mastodon.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Follow States
|
||||||
|
|
||||||
|
| State | Meaning | What User Sees |
|
||||||
|
|--------|---------|----------------|
|
||||||
|
| Pending | Follow request sent, waiting for response | "Following..." (loading) |
|
||||||
|
| Accepted | Remote user accepted | "Following" ✓ |
|
||||||
|
| Rejected | Remote user declined | "Follow" button available again |
|
||||||
|
| Failed | Error occurred | "Error following" message |
|
||||||
|
|
||||||
|
## Privacy & Visibility
|
||||||
|
|
||||||
|
### Public Posts
|
||||||
|
- ✅ Federate to your followers automatically
|
||||||
|
- ✅ Appear in remote instances' timelines
|
||||||
|
- ✅ Can be boosted/liked by remote users
|
||||||
|
|
||||||
|
### Private Posts
|
||||||
|
- ❌ Do not federate
|
||||||
|
- ❌ Only visible to your local followers
|
||||||
|
- ❌ Not sent to remote instances
|
||||||
|
|
||||||
|
### Unlisted Posts
|
||||||
|
- ⚠️ Federate but not in public timelines
|
||||||
|
- ⚠️ Only visible to followers
|
||||||
|
|
||||||
|
## Best Practices for Users
|
||||||
|
|
||||||
|
### When Following Someone
|
||||||
|
|
||||||
|
1. **Check their profile first**:
|
||||||
|
- Make sure they're who you think they are
|
||||||
|
- Read their bio to understand their content
|
||||||
|
|
||||||
|
2. **Start with a few interactions**:
|
||||||
|
- Like a few posts
|
||||||
|
- Reply to something interesting
|
||||||
|
- Don't overwhelm their timeline
|
||||||
|
|
||||||
|
3. **Respect their instance's rules**:
|
||||||
|
- Each instance has its own guidelines
|
||||||
|
- Read community rules before interacting
|
||||||
|
|
||||||
|
4. **Report spam/harassment**:
|
||||||
|
- Use instance blocking features
|
||||||
|
- Report to instance admins
|
||||||
|
|
||||||
|
### Following Across Instances
|
||||||
|
|
||||||
|
1. **Use their full address**:
|
||||||
|
- `@username@instance.com`
|
||||||
|
- This helps identify which instance they're on
|
||||||
|
|
||||||
|
2. **Be aware of instance culture**:
|
||||||
|
- Each instance has its own norms
|
||||||
|
- Some are more technical, others more casual
|
||||||
|
|
||||||
|
3. **Check if they're from your instance**:
|
||||||
|
- Local users show `isLocal: true`
|
||||||
|
- Usually faster interaction
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Follow button doesn't work"
|
||||||
|
|
||||||
|
**Possible Causes**:
|
||||||
|
1. User doesn't exist
|
||||||
|
2. Instance is down
|
||||||
|
3. Network issue
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Verify the user's address is correct
|
||||||
|
2. Check if the instance is accessible
|
||||||
|
3. Check your internet connection
|
||||||
|
4. Try again in a few minutes
|
||||||
|
|
||||||
|
### "User doesn't appear in Following list"
|
||||||
|
|
||||||
|
**Possible Causes**:
|
||||||
|
1. Follow was rejected
|
||||||
|
2. Still waiting for acceptance (locked accounts)
|
||||||
|
3. Error in federation
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Check the follow status via API
|
||||||
|
2. Try following again
|
||||||
|
3. Check if their account is locked
|
||||||
|
4. Contact support if issue persists
|
||||||
|
|
||||||
|
### "Can't find a user"
|
||||||
|
|
||||||
|
**Possible Causes**:
|
||||||
|
1. Wrong username or domain
|
||||||
|
2. User doesn't exist
|
||||||
|
3. Instance blocking your instance
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. Double-check the address
|
||||||
|
2. Try searching from a different instance
|
||||||
|
3. Contact the user directly for their handle
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Follow a Remote User
|
||||||
|
|
||||||
|
**Endpoint**: `POST /api/activitypub/follow`
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"targetActorUri": "https://mastodon.social/users/alice"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**: `200 OK`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Follow request sent. Waiting for acceptance.",
|
||||||
|
"targetActorUri": "https://mastodon.social/users/alice"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Following List
|
||||||
|
|
||||||
|
**Endpoint**: `GET /api/activitypub/following?limit=50`
|
||||||
|
|
||||||
|
**Response**: `200 OK`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"actorUri": "https://mastodon.social/users/alice",
|
||||||
|
"username": "alice",
|
||||||
|
"displayName": "Alice Smith",
|
||||||
|
"bio": "...",
|
||||||
|
"avatarUrl": "...",
|
||||||
|
"followedAt": "2024-01-15T10:30:00Z",
|
||||||
|
"isLocal": false,
|
||||||
|
"instanceDomain": "mastodon.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Followers List
|
||||||
|
|
||||||
|
**Endpoint**: `GET /api/activitypub/followers?limit=50`
|
||||||
|
|
||||||
|
**Response**: `200 OK`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"actorUri": "https://mastodon.social/users/alice",
|
||||||
|
"username": "alice",
|
||||||
|
"displayName": "Alice Smith",
|
||||||
|
"bio": "...",
|
||||||
|
"avatarUrl": "...",
|
||||||
|
"isLocal": false,
|
||||||
|
"instanceDomain": "mastodon.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Users
|
||||||
|
|
||||||
|
**Endpoint**: `GET /api/activitypub/search?query=alice&limit=20`
|
||||||
|
|
||||||
|
**Response**: `200 OK`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"actorUri": "https://mastodon.social/users/alice",
|
||||||
|
"username": "alice",
|
||||||
|
"displayName": "Alice Smith",
|
||||||
|
"bio": "...",
|
||||||
|
"avatarUrl": "...",
|
||||||
|
"isLocal": false,
|
||||||
|
"instanceDomain": "mastodon.social"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## What's Different About ActivityPub Following?
|
||||||
|
|
||||||
|
Unlike traditional social media:
|
||||||
|
|
||||||
|
| Feature | Traditional Social | ActivityPub |
|
||||||
|
|---------|------------------|-------------|
|
||||||
|
| Central server | ✅ Yes | ❌ No - federated |
|
||||||
|
| All users on same platform | ✅ Yes | ❌ No - multiple platforms |
|
||||||
|
| Blocked instances | ❌ No | ✅ Yes - instance blocking |
|
||||||
|
| Following across platforms | ❌ No | ✅ Yes - works with Mastodon, Pleroma, etc. |
|
||||||
|
| Your data stays on your server | ❌ Maybe | ✅ Yes - you control your data |
|
||||||
|
|
||||||
|
## User Experience Considerations
|
||||||
|
|
||||||
|
### Making It Easy
|
||||||
|
|
||||||
|
1. **Auto-discovery**:
|
||||||
|
- When users search for `@username`, suggest `@username@domain.com`
|
||||||
|
- Offer to search the fediverse
|
||||||
|
|
||||||
|
2. **Clear UI feedback**:
|
||||||
|
- Show "Follow request sent..."
|
||||||
|
- Show "They accepted!" notification
|
||||||
|
- Show "Follow request rejected" message
|
||||||
|
|
||||||
|
3. **Helpful tooltips**:
|
||||||
|
- Explain what ActivityPub is
|
||||||
|
- Show which instance a user is from
|
||||||
|
- Explain locked accounts
|
||||||
|
|
||||||
|
4. **Profile badges**:
|
||||||
|
- Show instance icon/logo
|
||||||
|
- Show if user is from same instance
|
||||||
|
- Show if user is verified
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Following a Mastodon User
|
||||||
|
|
||||||
|
**User searches**: `@alice@mastodon.social`
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
1. Solar Network fetches Alice's actor profile
|
||||||
|
2. Solar Network stores Alice in `fediverse_actors`
|
||||||
|
3. Solar Network sends Follow to Alice's inbox
|
||||||
|
4. Alice's instance accepts
|
||||||
|
5. Solar Network stores relationship in `fediverse_relationships`
|
||||||
|
6. Alice's posts now appear in user's timeline
|
||||||
|
|
||||||
|
### Following a Local User
|
||||||
|
|
||||||
|
**User searches**: `@bob`
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
1. Solar Network finds Bob's publisher
|
||||||
|
2. Relationship created locally (no federation needed)
|
||||||
|
3. Bob's posts appear in user's timeline immediately
|
||||||
|
4. Same as traditional social media following
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Following fediverse users in Solar Network:
|
||||||
|
|
||||||
|
1. **Search by `@username@domain.com`** - Works for any ActivityPub instance
|
||||||
|
2. **Click "Follow"** - Sends federated follow request
|
||||||
|
3. **Wait for acceptance** - Remote user can approve or auto-accept
|
||||||
|
4. **See their posts in your timeline** - Content federates to you
|
||||||
|
5. **Interact normally** - Like, reply, boost, etc.
|
||||||
|
|
||||||
|
All of this is handled automatically by the ActivityPub implementation!
|
||||||
298
docs/activitypub/UI_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
# ActivityPub UI Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Complete UI implementation for ActivityPub features in Solian client, including search, following, and followers screens.
|
||||||
|
|
||||||
|
## Created Files
|
||||||
|
|
||||||
|
### 1. Widgets (`lib/widgets/activitypub/`)
|
||||||
|
|
||||||
|
#### `activitypub.dart`
|
||||||
|
- **Purpose**: Export file for ActivityPub widgets
|
||||||
|
- **Exports**: `ActivityPubUserListItem`
|
||||||
|
|
||||||
|
#### `user_list_item.dart`
|
||||||
|
- **Purpose**: Reusable list item widget for displaying ActivityPub users
|
||||||
|
- **Features**:
|
||||||
|
- Avatar with remote instance indicator (public icon)
|
||||||
|
- Display name with instance badge (e.g., "mastodon.social")
|
||||||
|
- Bio with truncation (max 2 lines)
|
||||||
|
- Followed at timestamp (relative time)
|
||||||
|
- Follow/Unfollow buttons with loading states
|
||||||
|
- Tap callback for navigation to profile
|
||||||
|
|
||||||
|
### 2. Screens (`lib/screens/activitypub/`)
|
||||||
|
|
||||||
|
#### `activitypub.dart`
|
||||||
|
- **Purpose**: Export file for ActivityPub screens
|
||||||
|
- **Exports**: `ActivityPubSearchScreen`, `ActivityPubListScreen`
|
||||||
|
|
||||||
|
#### `search.dart`
|
||||||
|
- **Purpose**: Search and follow ActivityPub users from other instances
|
||||||
|
- **Features**:
|
||||||
|
- Search bar with 500ms debounce
|
||||||
|
- Real-time search results
|
||||||
|
- Instant follow/unfollow actions
|
||||||
|
- Local tracking of followed users
|
||||||
|
- Empty states for no search and no results
|
||||||
|
- Refresh support via pull-to-refresh
|
||||||
|
- User feedback via snack bars
|
||||||
|
- **User Flow**:
|
||||||
|
1. User enters search query (e.g., `@alice@mastodon.social`)
|
||||||
|
2. Results appear after debounce
|
||||||
|
3. User taps "Follow" → Follow request sent
|
||||||
|
4. Success message shown
|
||||||
|
5. Button updates to "Unfollow"
|
||||||
|
|
||||||
|
#### `list.dart`
|
||||||
|
- **Purpose**: Display following/followers lists
|
||||||
|
- **Features**:
|
||||||
|
- Reusable for both Following and Followers
|
||||||
|
- Local state management
|
||||||
|
- Per-user loading states during actions
|
||||||
|
- Empty states with helpful hints
|
||||||
|
- Refresh support
|
||||||
|
- Auto-update lists when actions occur
|
||||||
|
- **Types**:
|
||||||
|
- `ActivityPubListType.following`: Shows users you follow
|
||||||
|
- `ActivityPubListType.followers`: Shows users who follow you
|
||||||
|
- **User Flow**:
|
||||||
|
1. User opens Following/Followers screen
|
||||||
|
2. List loads from API
|
||||||
|
3. User can unfollow (Following tab) or follow (Followers tab)
|
||||||
|
4. List updates automatically
|
||||||
|
5. Success/error messages shown
|
||||||
|
|
||||||
|
## Design Patterns
|
||||||
|
|
||||||
|
### Follows Project Conventions
|
||||||
|
|
||||||
|
1. **Material 3 Design**: All widgets use Material 3 components
|
||||||
|
2. **Styled Widget Package**: Used for `.padding()`, `.textColor()`, etc.
|
||||||
|
3. **Riverpod State Management**: Hooks for local state, providers for global state
|
||||||
|
4. **Error Handling**: `showErrorAlert()` from `alert.dart` for user feedback
|
||||||
|
5. **Success Feedback**: `showSnackBar()` for quick notifications
|
||||||
|
6. **Localization**: All strings use `.tr()` with placeholder args
|
||||||
|
|
||||||
|
### Color Scheme & Theming
|
||||||
|
|
||||||
|
- **Remote Badge**: Uses `Theme.colorScheme.primary` for indicator
|
||||||
|
- **Instance Tag**: Uses `Theme.colorScheme.secondaryContainer`
|
||||||
|
- **Text Colors**: Adaptive based on theme (dark/light)
|
||||||
|
- **States**: Loading indicators with standard `CircularProgressIndicator`
|
||||||
|
|
||||||
|
### Spacing & Layout
|
||||||
|
|
||||||
|
- **List Item Padding**: `EdgeInsets.only(left: 16, right: 12)`
|
||||||
|
- **Avatar Size**: 24px radius (48px diameter)
|
||||||
|
- **Badge Size**: Small (10px font) with 6px horizontal padding
|
||||||
|
- **Button Size**: Minimum 88px width, 36px height
|
||||||
|
|
||||||
|
## Translations Added
|
||||||
|
|
||||||
|
### New Keys in `assets/i18n/en-US.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"searchFediverse": "Search Fediverse",
|
||||||
|
"searchFediverseHint": "Search by address, e.g. {}",
|
||||||
|
"searchFediverseEmpty": "Search for users on other ActivityPub instances",
|
||||||
|
"searchFediverseNoResults": "No users found for this search",
|
||||||
|
"following": "Following",
|
||||||
|
"followers": "Followers",
|
||||||
|
"follow": "Follow",
|
||||||
|
"unfollow": "Unfollow",
|
||||||
|
"followedUser": "Followed @{}",
|
||||||
|
"unfollowedUser": "Unfollowed @{}",
|
||||||
|
"followingEmpty": "You're not following anyone yet",
|
||||||
|
"followersEmpty": "No followers yet",
|
||||||
|
"followingEmptyHint": "Start by searching for users or explore other instances"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Using Search Screen
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:island/screens/activitypub/activitypub.dart';
|
||||||
|
|
||||||
|
// In navigation or route
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const ActivityPubSearchScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Or using go_router
|
||||||
|
context.push('/activitypub/search');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using List Screen
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Following
|
||||||
|
ActivityPubListScreen(
|
||||||
|
type: ActivityPubListType.following,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Followers
|
||||||
|
ActivityPubListScreen(
|
||||||
|
type: ActivityPubListType.followers,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using User List Item Widget
|
||||||
|
|
||||||
|
```dart
|
||||||
|
ActivityPubUserListItem(
|
||||||
|
user: user,
|
||||||
|
isFollowing: isFollowing,
|
||||||
|
isLoading: isLoading,
|
||||||
|
onFollow: () => handleFollow(user),
|
||||||
|
onUnfollow: () => handleUnfollow(user),
|
||||||
|
onTap: () => navigateToProfile(user),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
### Navigation Integration
|
||||||
|
|
||||||
|
To add ActivityPub screens to navigation:
|
||||||
|
|
||||||
|
1. **Option A**: Add to existing tab/navigation structure
|
||||||
|
2. **Option B**: Add as standalone routes in `go_router`
|
||||||
|
3. **Option C**: Add to profile menu overflow menu
|
||||||
|
|
||||||
|
### Service Integration
|
||||||
|
|
||||||
|
All screens use `activityPubServiceProvider`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:island/services/activitypub_service.dart';
|
||||||
|
|
||||||
|
final service = ref.read(activityPubServiceProvider);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
All errors are caught and displayed using:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
try {
|
||||||
|
// API call
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Search for existing Mastodon user
|
||||||
|
- [ ] Search for Pleroma user
|
||||||
|
- [ ] Follow a user
|
||||||
|
- [ ] Unfollow a user
|
||||||
|
- [ ] View following list
|
||||||
|
- [ ] View followers list
|
||||||
|
- [ ] Test empty states
|
||||||
|
- [ ] Test loading states
|
||||||
|
- [ ] Test error handling
|
||||||
|
- [ ] Test dark mode
|
||||||
|
- [ ] Test RTL languages (if supported)
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
**Already in project**:
|
||||||
|
- ✅ `cached_network_image` - For avatar images
|
||||||
|
- ✅ `easy_localization` - For translations
|
||||||
|
- ✅ `hooks_riverpod` - For state management
|
||||||
|
- ✅ `flutter_hooks` - For hooks (useState, useEffect, etc.)
|
||||||
|
- ✅ `material_symbols_icons` - For icons
|
||||||
|
- ✅ `relative_time` - For timestamp formatting
|
||||||
|
- ✅ `island/services/activitypub_service.dart` - API service (created earlier)
|
||||||
|
- ✅ `island/widgets/alert.dart` - Error/success dialogs
|
||||||
|
- ✅ `island/models/activitypub.dart` - Data models (created earlier)
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
|
||||||
|
1. **Debounced Search**: 500ms delay prevents excessive API calls
|
||||||
|
2. **Local State Tracking**: `followingUris` Set prevents duplicate API calls
|
||||||
|
3. **Conditional Rebuilds**: Widget only rebuilds when necessary
|
||||||
|
4. **Image Caching**: Uses `CachedNetworkImageProvider` for avatars
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
1. **Semantic Labels**: All ListTile widgets have proper content
|
||||||
|
2. **Touch Targets**: Minimum 44px touch targets for buttons
|
||||||
|
3. **Color Contrast**: Follows Material 3 color guidelines
|
||||||
|
4. **Loading Indicators**: Visual feedback during async operations
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Potential Additions
|
||||||
|
|
||||||
|
1. **Profile Integration**: Show ActivityPub profile details
|
||||||
|
2. **Post Timeline**: Show federated posts from followed users
|
||||||
|
3. **Instance Blocking**: Block entire ActivityPub instances
|
||||||
|
4. **Advanced Search**: Filter by instance, user type, etc.
|
||||||
|
5. **Batch Actions**: Follow/unfollow multiple users at once
|
||||||
|
6. **Suggested Users**: Show recommended users to follow
|
||||||
|
7. **Recent Activity**: Show recent interactions
|
||||||
|
8. **Notifications**: Follow/unfollow notifications
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
Need to add same keys to other language files:
|
||||||
|
- `es-ES.json`
|
||||||
|
- `ja-JP.json`
|
||||||
|
- `ko-KR.json`
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
## Browser Testing
|
||||||
|
|
||||||
|
Test with real ActivityPub instances:
|
||||||
|
- mastodon.social
|
||||||
|
- pixelfed.social
|
||||||
|
- lemmy.world
|
||||||
|
- pleroma.site
|
||||||
|
- fosstodon.org
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Search returns no results**
|
||||||
|
- Check if user exists on remote instance
|
||||||
|
- Verify instance is accessible
|
||||||
|
- Try full URL instead of handle
|
||||||
|
|
||||||
|
2. **Follow button not working**
|
||||||
|
- Check if user is already following
|
||||||
|
- Verify server is online
|
||||||
|
- Check API logs
|
||||||
|
|
||||||
|
3. **Avatar not loading**
|
||||||
|
- Check remote avatar URL
|
||||||
|
- Verify network connection
|
||||||
|
- Check image cache
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
✅ **Fully functional ActivityPub UI** with:
|
||||||
|
- Search screen for discovering fediverse users
|
||||||
|
- Following/Followers list screens
|
||||||
|
- Reusable user list item component
|
||||||
|
- Proper error handling and user feedback
|
||||||
|
- Material 3 design
|
||||||
|
- Responsive layout
|
||||||
|
- Local state management
|
||||||
|
- Debounced search
|
||||||
|
- Empty states and loading indicators
|
||||||
|
|
||||||
|
**Ready for integration into main app navigation!** 🎉
|
||||||
1
drift_schemas/app_database/drift_schema_v7.json
Normal file
15
ios/Podfile
@@ -1,4 +1,3 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
|
||||||
platform :ios, '15.0'
|
platform :ios, '15.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
@@ -32,6 +31,8 @@ target 'Runner' do
|
|||||||
use_modular_headers!
|
use_modular_headers!
|
||||||
|
|
||||||
pod 'Alamofire'
|
pod 'Alamofire'
|
||||||
|
pod 'Kingfisher', '~> 8.0'
|
||||||
|
pod 'KingfisherWebP'
|
||||||
|
|
||||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
|
||||||
@@ -41,8 +42,6 @@ target 'Runner' do
|
|||||||
|
|
||||||
target 'SolianNotificationService' do
|
target 'SolianNotificationService' do
|
||||||
inherit! :search_paths
|
inherit! :search_paths
|
||||||
pod 'Kingfisher', '~> 8.0'
|
|
||||||
pod 'Alamofire'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'SolianShareExtension' do
|
target 'SolianShareExtension' do
|
||||||
@@ -50,6 +49,16 @@ target 'Runner' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
target 'Solian Watch App' do
|
||||||
|
platform :watchos, '11.0'
|
||||||
|
|
||||||
|
use_frameworks!
|
||||||
|
use_modular_headers!
|
||||||
|
|
||||||
|
pod 'Kingfisher', '~> 8.0'
|
||||||
|
pod 'KingfisherWebP'
|
||||||
|
end
|
||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
flutter_additional_ios_build_settings(target)
|
flutter_additional_ios_build_settings(target)
|
||||||
|
|||||||
272
ios/Podfile.lock
@@ -1,5 +1,5 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.10.2)
|
- Alamofire (5.11.0)
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- croppy (0.0.1):
|
- croppy (0.0.1):
|
||||||
@@ -42,83 +42,83 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- file_saver (0.0.1):
|
- file_saver (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Firebase/CoreOnly (12.2.0):
|
- Firebase/CoreOnly (12.6.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.6.0)
|
||||||
- Firebase/Crashlytics (12.2.0):
|
- Firebase/Crashlytics (12.6.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseCrashlytics (~> 12.2.0)
|
- FirebaseCrashlytics (~> 12.6.0)
|
||||||
- Firebase/Messaging (12.2.0):
|
- Firebase/Messaging (12.6.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 12.2.0)
|
- FirebaseMessaging (~> 12.6.0)
|
||||||
- firebase_analytics (12.0.2):
|
- firebase_analytics (12.1.0):
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- FirebaseAnalytics (= 12.2.0)
|
- FirebaseAnalytics (= 12.6.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_core (4.1.1):
|
- firebase_core (4.3.0):
|
||||||
- Firebase/CoreOnly (= 12.2.0)
|
- Firebase/CoreOnly (= 12.6.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_crashlytics (5.0.2):
|
- firebase_crashlytics (5.0.6):
|
||||||
- Firebase/Crashlytics (= 12.2.0)
|
- Firebase/Crashlytics (= 12.6.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (16.0.2):
|
- firebase_messaging (16.1.0):
|
||||||
- Firebase/Messaging (= 12.2.0)
|
- Firebase/Messaging (= 12.6.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- FirebaseAnalytics (12.2.0):
|
- FirebaseAnalytics (12.6.0):
|
||||||
- FirebaseAnalytics/Default (= 12.2.0)
|
- FirebaseAnalytics/Default (= 12.6.0)
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.6.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.6.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseAnalytics/Default (12.2.0):
|
- FirebaseAnalytics/Default (12.6.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.6.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.6.0)
|
||||||
- GoogleAppMeasurement/Default (= 12.2.0)
|
- GoogleAppMeasurement/Default (= 12.6.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseCore (12.2.0):
|
- FirebaseCore (12.6.0):
|
||||||
- FirebaseCoreInternal (~> 12.2.0)
|
- FirebaseCoreInternal (~> 12.6.0)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Logger (~> 8.1)
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
- FirebaseCoreExtension (12.2.0):
|
- FirebaseCoreExtension (12.6.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.6.0)
|
||||||
- FirebaseCoreInternal (12.2.0):
|
- FirebaseCoreInternal (12.6.0):
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- FirebaseCrashlytics (12.2.0):
|
- FirebaseCrashlytics (12.6.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.6.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.6.0)
|
||||||
- FirebaseRemoteConfigInterop (~> 12.2.0)
|
- FirebaseRemoteConfigInterop (~> 12.6.0)
|
||||||
- FirebaseSessions (~> 12.2.0)
|
- FirebaseSessions (~> 12.6.0)
|
||||||
- GoogleDataTransport (~> 10.1)
|
- GoogleDataTransport (~> 10.1)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseInstallations (12.2.0):
|
- FirebaseInstallations (12.6.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.6.0)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseMessaging (12.2.0):
|
- FirebaseMessaging (12.6.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.6.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.6.0)
|
||||||
- GoogleDataTransport (~> 10.1)
|
- GoogleDataTransport (~> 10.1)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Reachability (~> 8.1)
|
- GoogleUtilities/Reachability (~> 8.1)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseRemoteConfigInterop (12.2.0)
|
- FirebaseRemoteConfigInterop (12.6.0)
|
||||||
- FirebaseSessions (12.2.0):
|
- FirebaseSessions (12.6.0):
|
||||||
- FirebaseCore (~> 12.2.0)
|
- FirebaseCore (~> 12.6.0)
|
||||||
- FirebaseCoreExtension (~> 12.2.0)
|
- FirebaseCoreExtension (~> 12.6.0)
|
||||||
- FirebaseInstallations (~> 12.2.0)
|
- FirebaseInstallations (~> 12.6.0)
|
||||||
- GoogleDataTransport (~> 10.1)
|
- GoogleDataTransport (~> 10.1)
|
||||||
- GoogleUtilities/Environment (~> 8.1)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
@@ -140,42 +140,42 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- flutter_native_splash (2.4.3):
|
- flutter_native_splash (2.4.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_platform_alert (0.0.1):
|
- flutter_secure_storage_darwin (10.0.0):
|
||||||
- Flutter
|
|
||||||
- flutter_secure_storage (6.0.0):
|
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- flutter_timezone (0.0.1):
|
- flutter_timezone (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- KeychainAccess
|
||||||
- flutter_webrtc (1.2.0):
|
- flutter_webrtc (1.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (= 137.7151.04)
|
- WebRTC-SDK (= 137.7151.04)
|
||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- GoogleAdsOnDeviceConversion (2.3.0):
|
- GoogleAdsOnDeviceConversion (3.2.0):
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- GoogleUtilities/Logger (~> 8.1)
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/Core (12.2.0):
|
- GoogleAppMeasurement/Core (12.6.0):
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/Default (12.2.0):
|
- GoogleAppMeasurement/Default (12.6.0):
|
||||||
- GoogleAdsOnDeviceConversion (= 2.3.0)
|
- GoogleAdsOnDeviceConversion (~> 3.2.0)
|
||||||
- GoogleAppMeasurement/Core (= 12.2.0)
|
- GoogleAppMeasurement/Core (= 12.6.0)
|
||||||
- GoogleAppMeasurement/IdentitySupport (= 12.2.0)
|
- GoogleAppMeasurement/IdentitySupport (= 12.6.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/IdentitySupport (12.2.0):
|
- GoogleAppMeasurement/IdentitySupport (12.6.0):
|
||||||
- GoogleAppMeasurement/Core (= 12.2.0)
|
- GoogleAppMeasurement/Core (= 12.6.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- GoogleUtilities/Network (~> 8.1)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
@@ -213,10 +213,28 @@ PODS:
|
|||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- in_app_review (2.0.0):
|
||||||
|
- Flutter
|
||||||
- irondash_engine_context (0.0.1):
|
- irondash_engine_context (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.5.0)
|
- KeychainAccess (4.2.2)
|
||||||
- livekit_client (2.5.0):
|
- Kingfisher (8.6.2)
|
||||||
|
- KingfisherWebP (1.7.2):
|
||||||
|
- Kingfisher (~> 8.0)
|
||||||
|
- libwebp (>= 1.1.0)
|
||||||
|
- libwebp (1.5.0):
|
||||||
|
- libwebp/demux (= 1.5.0)
|
||||||
|
- libwebp/mux (= 1.5.0)
|
||||||
|
- libwebp/sharpyuv (= 1.5.0)
|
||||||
|
- libwebp/webp (= 1.5.0)
|
||||||
|
- libwebp/demux (1.5.0):
|
||||||
|
- libwebp/webp
|
||||||
|
- libwebp/mux (1.5.0):
|
||||||
|
- libwebp/demux
|
||||||
|
- libwebp/sharpyuv (1.5.0)
|
||||||
|
- libwebp/webp (1.5.0):
|
||||||
|
- libwebp/sharpyuv
|
||||||
|
- livekit_client (2.5.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- WebRTC-SDK (= 137.7151.04)
|
- WebRTC-SDK (= 137.7151.04)
|
||||||
@@ -242,19 +260,24 @@ PODS:
|
|||||||
- path_provider_foundation (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- permission_handler_apple (9.3.0):
|
||||||
|
- Flutter
|
||||||
- pointer_interceptor_ios (0.0.1):
|
- pointer_interceptor_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- PromisesSwift (2.4.0):
|
- PromisesSwift (2.4.0):
|
||||||
- PromisesObjC (= 2.4.0)
|
- PromisesObjC (= 2.4.0)
|
||||||
|
- protocol_handler_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- receive_sharing_intent (1.8.1):
|
- receive_sharing_intent (1.8.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- record_ios (1.1.0):
|
- record_ios (1.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain (1.5.3)
|
- SDWebImage (5.21.5):
|
||||||
- SDWebImage (5.21.2):
|
- SDWebImage/Core (= 5.21.5)
|
||||||
- SDWebImage/Core (= 5.21.2)
|
- SDWebImage/Core (5.21.5)
|
||||||
- SDWebImage/Core (5.21.2)
|
- sensors_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@@ -265,25 +288,25 @@ PODS:
|
|||||||
- sqflite_darwin (0.0.4):
|
- sqflite_darwin (0.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqlite3 (3.50.4):
|
- sqlite3 (3.51.1):
|
||||||
- sqlite3/common (= 3.50.4)
|
- sqlite3/common (= 3.51.1)
|
||||||
- sqlite3/common (3.50.4)
|
- sqlite3/common (3.51.1)
|
||||||
- sqlite3/dbstatvtab (3.50.4):
|
- sqlite3/dbstatvtab (3.51.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/fts5 (3.50.4):
|
- sqlite3/fts5 (3.51.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/math (3.50.4):
|
- sqlite3/math (3.51.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/perf-threadsafe (3.50.4):
|
- sqlite3/perf-threadsafe (3.51.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/rtree (3.50.4):
|
- sqlite3/rtree (3.51.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/session (3.50.4):
|
- sqlite3/session (3.51.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3_flutter_libs (0.0.1):
|
- sqlite3_flutter_libs (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqlite3 (~> 3.50.4)
|
- sqlite3 (~> 3.51.1)
|
||||||
- sqlite3/dbstatvtab
|
- sqlite3/dbstatvtab
|
||||||
- sqlite3/fts5
|
- sqlite3/fts5
|
||||||
- sqlite3/math
|
- sqlite3/math
|
||||||
@@ -293,9 +316,9 @@ PODS:
|
|||||||
- super_native_extensions (0.0.1):
|
- super_native_extensions (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SwiftyGif (5.4.5)
|
- SwiftyGif (5.4.5)
|
||||||
- url_launcher_ios (0.0.1):
|
- syncfusion_flutter_pdfviewer (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- volume_controller (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -318,15 +341,16 @@ DEPENDENCIES:
|
|||||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`)
|
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
|
||||||
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
|
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||||
- Kingfisher (~> 8.0)
|
- Kingfisher (~> 8.0)
|
||||||
|
- KingfisherWebP
|
||||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
||||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||||
@@ -335,17 +359,20 @@ DEPENDENCIES:
|
|||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
||||||
|
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
||||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||||
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
||||||
|
- sensors_plus (from `.symlinks/plugins/sensors_plus/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
|
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||||
|
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
@@ -367,12 +394,14 @@ SPEC REPOS:
|
|||||||
- GoogleAppMeasurement
|
- GoogleAppMeasurement
|
||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
|
- KeychainAccess
|
||||||
- Kingfisher
|
- Kingfisher
|
||||||
|
- KingfisherWebP
|
||||||
|
- libwebp
|
||||||
- nanopb
|
- nanopb
|
||||||
- OrderedSet
|
- OrderedSet
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- PromisesSwift
|
- PromisesSwift
|
||||||
- SAMKeychain
|
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- sqlite3
|
- sqlite3
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
@@ -409,10 +438,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_platform_alert:
|
flutter_secure_storage_darwin:
|
||||||
:path: ".symlinks/plugins/flutter_platform_alert/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
|
||||||
flutter_secure_storage:
|
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
|
||||||
flutter_timezone:
|
flutter_timezone:
|
||||||
:path: ".symlinks/plugins/flutter_timezone/ios"
|
:path: ".symlinks/plugins/flutter_timezone/ios"
|
||||||
flutter_udid:
|
flutter_udid:
|
||||||
@@ -423,6 +450,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/gal/darwin"
|
:path: ".symlinks/plugins/gal/darwin"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
|
in_app_review:
|
||||||
|
:path: ".symlinks/plugins/in_app_review/ios"
|
||||||
irondash_engine_context:
|
irondash_engine_context:
|
||||||
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
||||||
livekit_client:
|
livekit_client:
|
||||||
@@ -441,12 +470,18 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/pasteboard/ios"
|
:path: ".symlinks/plugins/pasteboard/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
|
permission_handler_apple:
|
||||||
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
pointer_interceptor_ios:
|
pointer_interceptor_ios:
|
||||||
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
||||||
|
protocol_handler_ios:
|
||||||
|
:path: ".symlinks/plugins/protocol_handler_ios/ios"
|
||||||
receive_sharing_intent:
|
receive_sharing_intent:
|
||||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||||
record_ios:
|
record_ios:
|
||||||
:path: ".symlinks/plugins/record_ios/ios"
|
:path: ".symlinks/plugins/record_ios/ios"
|
||||||
|
sensors_plus:
|
||||||
|
:path: ".symlinks/plugins/sensors_plus/ios"
|
||||||
share_plus:
|
share_plus:
|
||||||
:path: ".symlinks/plugins/share_plus/ios"
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
@@ -459,15 +494,15 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||||
super_native_extensions:
|
super_native_extensions:
|
||||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||||
|
syncfusion_flutter_pdfviewer:
|
||||||
|
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
volume_controller:
|
|
||||||
:path: ".symlinks/plugins/volume_controller/ios"
|
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
Alamofire: bd5e7b23a1a750975288482c1831d71e74415f86
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||||
@@ -475,40 +510,43 @@ SPEC CHECKSUMS:
|
|||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||||
Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1
|
Firebase: a451a7b61536298fd5cbfe3a746fd40443a50679
|
||||||
firebase_analytics: 8c78ce6224e0623152379d6cc7ef3d9098477b7e
|
firebase_analytics: 4f9cca09e65f6c2944a862c6dc86f6bed9fb769c
|
||||||
firebase_core: dfc4bd142bee4bc53a5d482397ca322c2dd3165d
|
firebase_core: ba00a168e719694f38960502ceb560285603d073
|
||||||
firebase_crashlytics: e55dcf895eed0dd87c447dd5aff8db7f1bb8bbdb
|
firebase_crashlytics: 13f4b77e9ce2a84b1f8ea07f293db5b6213ce1cf
|
||||||
firebase_messaging: 38c66c1184695b0c87abe51d40fc590718abed1a
|
firebase_messaging: bf0e29321927edc02a563c984dbfa5b063864b15
|
||||||
FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8
|
FirebaseAnalytics: d0a97a0db6425e5a5d966340b87f92ca7b13a557
|
||||||
FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd
|
FirebaseCore: 0e38ad5d62d980a47a64b8e9301ffa311457be04
|
||||||
FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5
|
FirebaseCoreExtension: 032fd6f8509e591fda8cb76f6651f20d926b121f
|
||||||
FirebaseCoreInternal: 56ea29f3dad2894f81b060f706f9d53509b6ed3b
|
FirebaseCoreInternal: 69bf1306a05b8ac43004f6cc1f804bb7b05b229e
|
||||||
FirebaseCrashlytics: f83cbf176d5c637ade108c0aacf1ccbd5ec499bf
|
FirebaseCrashlytics: 3d6248c50726ee7832aef0e53cb84c9e64d9fa7e
|
||||||
FirebaseInstallations: 3e884b01feabdf67582a80f3250425a00979b4ed
|
FirebaseInstallations: 631b38da2e11a83daa4bfb482f79d286a5dfa7ad
|
||||||
FirebaseMessaging: 43ec73bbfedd0c385a849bb91593ab4ad4b9e48e
|
FirebaseMessaging: a61bc42dcab3f7a346d94bbb54dab2c9435b18b2
|
||||||
FirebaseRemoteConfigInterop: 0896fd52ab72586a355c8f389ff85aaa9e5375e1
|
FirebaseRemoteConfigInterop: 3443b8cb8fffd76bb3e03b2a84bfd3db952fcda4
|
||||||
FirebaseSessions: f4692789e770bec66ce17d772c0e9561c4f11737
|
FirebaseSessions: 2e8f808347e665dff3e5843f275715f07045297d
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
||||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||||
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
|
||||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
|
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
||||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
|
||||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
|
||||||
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
flutter_webrtc: c3e21fc0dcd9d8eb246ae4d5256fcbeb2f5ecd22
|
||||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||||
GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe
|
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
|
||||||
GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
|
GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
|
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
|
||||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||||
Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c
|
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||||
livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4
|
Kingfisher: 23d18f54677d973b713e54ce6a8f5eef6e7056ba
|
||||||
|
KingfisherWebP: 38b9721821947f547afb78f933f75f4f9e0ae402
|
||||||
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
|
livekit_client: 53ca658779b78710fb458cccee28b53a13356c15
|
||||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||||
@@ -517,27 +555,29 @@ SPEC CHECKSUMS:
|
|||||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
|
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
|
protocol_handler_ios: 59f23ee71f3ec602d67902ca7f669a80957888d5
|
||||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
|
||||||
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
|
sensors_plus: 6a11ed0c2e1d0bd0b20b4029d3bad27d96e0c65b
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
|
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b
|
||||||
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
|
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41
|
||||||
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
|
||||||
|
|
||||||
PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f
|
PODFILE CHECKSUM: 585198f58dca90ac6492607c83a8d17045ab3852
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
5D8143680678FCD1D1827271 /* Pods_Solian_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */; };
|
||||||
|
7310A7DF2EB10963002C0FD3 /* Solian Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 7310A7D42EB10962002C0FD3 /* Solian Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
|
73ACDFAD2E3D0E6100B63535 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */; };
|
||||||
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
73ACDFC32E3D0E6100B63535 /* SolianBroadcastExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
@@ -58,6 +60,17 @@
|
|||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
7310A7DE2EB10963002C0FD3 /* Embed Watch Content */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 12;
|
||||||
|
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
|
||||||
|
dstSubfolderSpec = 16;
|
||||||
|
files = (
|
||||||
|
7310A7DF2EB10963002C0FD3 /* Solian Watch App.app in Embed Watch Content */,
|
||||||
|
);
|
||||||
|
name = "Embed Watch Content";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */ = {
|
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -84,6 +97,8 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
0ECC3D56D018DD87FC342699 /* Pods-Solian Watch App.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Solian Watch App.profile.xcconfig"; path = "Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.profile.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
@@ -91,15 +106,18 @@
|
|||||||
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
192FDACE67D7CB6AED15C634 /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
192FDACE67D7CB6AED15C634 /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
1C14F71D23E4371602065522 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
1C14F71D23E4371602065522 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
2440CEDEAAD6D51FDA95FA62 /* Pods-Solian Watch App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Solian Watch App.release.xcconfig"; path = "Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
252A83CE6862573BB856ED8E /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
252A83CE6862573BB856ED8E /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.release.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.release.xcconfig"; sourceTree = "<group>"; };
|
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.release.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
2D2457F8B2E6EF9C0F935035 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
2D2457F8B2E6EF9C0F935035 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
31EA49B10397BD4145AD765E /* Pods-Solian Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Solian Watch App.debug.xcconfig"; path = "Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianNotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Solian Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||||
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianBroadcastExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
|
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
|
||||||
@@ -111,6 +129,7 @@
|
|||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.debug.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.profile.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.profile.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -120,10 +139,12 @@
|
|||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||||
|
A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WatchRunner Watch App.release.xcconfig"; path = "Target Support Files/Pods-WatchRunner Watch App/Pods-WatchRunner Watch App.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.release.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.release.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Solian_Watch_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
F6D834CA86410B09796B312B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
F6D834CA86410B09796B312B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.debug.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.debug.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
@@ -162,6 +183,13 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
7310A7D52EB10962002C0FD3 /* Solian Watch App */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
);
|
||||||
|
path = "Solian Watch App";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
73268D272DEB012A0076E970 /* Services */ = {
|
73268D272DEB012A0076E970 /* Services */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
exceptions = (
|
||||||
@@ -205,6 +233,14 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
7310A7D12EB10962002C0FD3 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
5D8143680678FCD1D1827271 /* Pods_Solian_Watch_App.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
73ACDFA82E3D0E6100B63535 /* Frameworks */ = {
|
73ACDFA82E3D0E6100B63535 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -258,6 +294,7 @@
|
|||||||
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */,
|
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */,
|
||||||
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
|
73ACDFAC2E3D0E6100B63535 /* ReplayKit.framework */,
|
||||||
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
|
73ACDFB82E3D0E6100B63535 /* UIKit.framework */,
|
||||||
|
C9C046CF867AE03DC170F861 /* Pods_Solian_Watch_App.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -280,6 +317,12 @@
|
|||||||
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */,
|
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */,
|
||||||
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */,
|
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */,
|
||||||
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */,
|
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */,
|
||||||
|
86D60BA96DA647E1B11AA7F0 /* Pods-WatchRunner Watch App.debug.xcconfig */,
|
||||||
|
A2EB1DAFDE9B8E6D88BBF7A3 /* Pods-WatchRunner Watch App.release.xcconfig */,
|
||||||
|
103EA2362B9E9F127016A1F1 /* Pods-WatchRunner Watch App.profile.xcconfig */,
|
||||||
|
31EA49B10397BD4145AD765E /* Pods-Solian Watch App.debug.xcconfig */,
|
||||||
|
2440CEDEAAD6D51FDA95FA62 /* Pods-Solian Watch App.release.xcconfig */,
|
||||||
|
0ECC3D56D018DD87FC342699 /* Pods-Solian Watch App.profile.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -303,6 +346,7 @@
|
|||||||
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
|
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
|
||||||
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
|
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
|
||||||
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
73ACDFAE2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
||||||
|
7310A7D52EB10962002C0FD3 /* Solian Watch App */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
91E124CE95BCB4DCD890160D /* Pods */,
|
91E124CE95BCB4DCD890160D /* Pods */,
|
||||||
@@ -319,6 +363,7 @@
|
|||||||
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
|
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
|
||||||
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
|
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
|
||||||
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
|
73ACDFAB2E3D0E6100B63535 /* SolianBroadcastExtension.appex */,
|
||||||
|
7310A7D42EB10962002C0FD3 /* Solian Watch App.app */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -363,6 +408,28 @@
|
|||||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
};
|
};
|
||||||
|
7310A7D32EB10962002C0FD3 /* Solian Watch App */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */;
|
||||||
|
buildPhases = (
|
||||||
|
DDEDA1BA6278B94F0F7B9B61 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
7310A7D02EB10962002C0FD3 /* Sources */,
|
||||||
|
7310A7D12EB10962002C0FD3 /* Frameworks */,
|
||||||
|
7310A7D22EB10962002C0FD3 /* Resources */,
|
||||||
|
E29ECA5954168075BDB000DC /* [CP] Embed Pods Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
7310A7D52EB10962002C0FD3 /* Solian Watch App */,
|
||||||
|
);
|
||||||
|
name = "Solian Watch App";
|
||||||
|
productName = "WatchRunner Watch App";
|
||||||
|
productReference = 7310A7D42EB10962002C0FD3 /* Solian Watch App.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */ = {
|
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */;
|
buildConfigurationList = 73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */;
|
||||||
@@ -434,6 +501,7 @@
|
|||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */,
|
73268D1D2DEAFD670076E970 /* Embed Foundation Extensions */,
|
||||||
|
7310A7DE2EB10963002C0FD3 /* Embed Watch Content */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
@@ -463,7 +531,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 1640;
|
LastSwiftUpdateCheck = 2600;
|
||||||
LastUpgradeCheck = 1510;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
@@ -471,6 +539,9 @@
|
|||||||
CreatedOnToolsVersion = 14.0;
|
CreatedOnToolsVersion = 14.0;
|
||||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
};
|
};
|
||||||
|
7310A7D32EB10962002C0FD3 = {
|
||||||
|
CreatedOnToolsVersion = 26.0.1;
|
||||||
|
};
|
||||||
73ACDFAA2E3D0E6100B63535 = {
|
73ACDFAA2E3D0E6100B63535 = {
|
||||||
CreatedOnToolsVersion = 16.4;
|
CreatedOnToolsVersion = 16.4;
|
||||||
};
|
};
|
||||||
@@ -504,6 +575,7 @@
|
|||||||
73CDD6792DEC00480059D95D /* SolianNotificationService */,
|
73CDD6792DEC00480059D95D /* SolianNotificationService */,
|
||||||
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
|
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
|
||||||
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
73ACDFAA2E3D0E6100B63535 /* SolianBroadcastExtension */,
|
||||||
|
7310A7D32EB10962002C0FD3 /* Solian Watch App */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -516,6 +588,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
7310A7D22EB10962002C0FD3 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
73ACDFA92E3D0E6100B63535 /* Resources */ = {
|
73ACDFA92E3D0E6100B63535 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -683,6 +762,45 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
};
|
};
|
||||||
|
DDEDA1BA6278B94F0F7B9B61 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Solian Watch App-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
E29ECA5954168075BDB000DC /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Solian Watch App/Pods-Solian Watch App-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
|
E86CDE9D6464F4F52B910856 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -734,6 +852,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
7310A7D02EB10962002C0FD3 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
73ACDFA72E3D0E6100B63535 /* Sources */ = {
|
73ACDFA72E3D0E6100B63535 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -873,6 +998,7 @@
|
|||||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
|
EXCLUDED_SOURCE_FILE_NAMES = "";
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
@@ -883,10 +1009,12 @@
|
|||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||||
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
};
|
};
|
||||||
@@ -894,6 +1022,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */;
|
baseConfigurationReference = 14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -902,6 +1031,8 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||||
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -913,6 +1044,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */;
|
baseConfigurationReference = 14118AC858B441AB16B7309E /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -921,6 +1053,8 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||||
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
};
|
};
|
||||||
@@ -930,6 +1064,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */;
|
baseConfigurationReference = E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -938,15 +1073,166 @@
|
|||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||||
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
};
|
};
|
||||||
|
7310A7E02EB10963002C0FD3 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 31EA49B10397BD4145AD765E /* Pods-Solian Watch App.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian;
|
||||||
|
INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
7310A7E12EB10963002C0FD3 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 2440CEDEAAD6D51FDA95FA62 /* Pods-Solian Watch App.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian;
|
||||||
|
INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "watchsimulator watchos";
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
7310A7E22EB10963002C0FD3 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 0ECC3D56D018DD87FC342699 /* Pods-Solian Watch App.profile.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppIcon;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = "WatchRunner-Watch-App-Info.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = dev.solsynth.solian;
|
||||||
|
INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.watchkitapp;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "watchsimulator watchos";
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
73ACDFC42E3D0E6100B63535 /* Debug */ = {
|
73ACDFC42E3D0E6100B63535 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -976,6 +1262,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@@ -987,7 +1274,7 @@
|
|||||||
73ACDFC52E3D0E6100B63535 /* Release */ = {
|
73ACDFC52E3D0E6100B63535 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -1016,6 +1303,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@@ -1025,7 +1313,7 @@
|
|||||||
73ACDFC62E3D0E6100B63535 /* Profile */ = {
|
73ACDFC62E3D0E6100B63535 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -1054,6 +1342,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianBroadcastExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@@ -1064,7 +1353,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */;
|
baseConfigurationReference = 17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -1095,6 +1384,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||||
@@ -1108,7 +1398,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */;
|
baseConfigurationReference = 27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -1138,6 +1428,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -1149,7 +1440,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */;
|
baseConfigurationReference = A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -1179,6 +1470,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -1190,7 +1482,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */;
|
baseConfigurationReference = F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -1232,7 +1524,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */;
|
baseConfigurationReference = B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -1271,7 +1563,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */;
|
baseConfigurationReference = 8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@@ -1428,6 +1720,7 @@
|
|||||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
|
EXCLUDED_SOURCE_FILE_NAMES = "";
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
@@ -1443,6 +1736,7 @@
|
|||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -1457,6 +1751,7 @@
|
|||||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
|
EXCLUDED_SOURCE_FILE_NAMES = "";
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
INFOPLIST_KEY_CFBundleDisplayName = Solian;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
@@ -1465,12 +1760,15 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
ONLY_ACTIVE_ARCH = NO;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||||
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
SWIFT_ENABLE_EXPLICIT_MODULES = "$(SWIFT_USE_INTEGRATED_DRIVER)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 11.6;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -1487,6 +1785,16 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
7310A7E32EB10963002C0FD3 /* Build configuration list for PBXNativeTarget "Solian Watch App" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
7310A7E02EB10963002C0FD3 /* Debug */,
|
||||||
|
7310A7E12EB10963002C0FD3 /* Release */,
|
||||||
|
7310A7E22EB10963002C0FD3 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */ = {
|
73ACDFCB2E3D0E6100B63535 /* Build configuration list for PBXNativeTarget "SolianBroadcastExtension" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@@ -20,6 +20,20 @@
|
|||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "7310A7D32EB10962002C0FD3"
|
||||||
|
BuildableName = "Solian Watch App.app"
|
||||||
|
BlueprintName = "Solian Watch App"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction
|
<TestAction
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import Flutter
|
import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import WatchConnectivity
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
let notifyDelegate = NotifyDelegate()
|
let notifyDelegate = NotifyDelegate()
|
||||||
|
private static var sharedWatchConnectivityService: WatchConnectivityService?
|
||||||
|
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
UNUserNotificationCenter.current().delegate = notifyDelegate
|
UNUserNotificationCenter.current().delegate = notifyDelegate
|
||||||
|
|
||||||
let replyableMessageCategory = UNNotificationCategory(
|
let replyableMessageCategory = UNNotificationCategory(
|
||||||
identifier: "REPLYABLE_MESSAGE",
|
identifier: "CHAT_MESSAGE",
|
||||||
actions: [
|
actions: [
|
||||||
UNTextInputNotificationAction(
|
UNTextInputNotificationAction(
|
||||||
identifier: "reply_action",
|
identifier: "reply_action",
|
||||||
@@ -23,11 +25,85 @@ import UIKit
|
|||||||
intentIdentifiers: [],
|
intentIdentifiers: [],
|
||||||
options: []
|
options: []
|
||||||
)
|
)
|
||||||
|
|
||||||
UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory])
|
UNUserNotificationCenter.current().setNotificationCategories([replyableMessageCategory])
|
||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
|
||||||
|
// Always initialize and retain a strong reference
|
||||||
|
if WCSession.isSupported() {
|
||||||
|
AppDelegate.sharedWatchConnectivityService = WatchConnectivityService.shared
|
||||||
|
} else {
|
||||||
|
print("[iOS] WCSession not supported on this device.")
|
||||||
|
}
|
||||||
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
||||||
|
static let shared = WatchConnectivityService()
|
||||||
|
private let session: WCSession = .default
|
||||||
|
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
print("[iOS] Activating WCSession...")
|
||||||
|
session.delegate = self
|
||||||
|
session.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - WCSessionDelegate
|
||||||
|
|
||||||
|
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||||
|
if let error = error {
|
||||||
|
print("[iOS] WCSession activation failed: \(error.localizedDescription)")
|
||||||
|
} else {
|
||||||
|
print("[iOS] WCSession activated with state: \(activationState.rawValue)")
|
||||||
|
if activationState == .activated {
|
||||||
|
sendDataToWatch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionDidBecomeInactive(_ session: WCSession) {}
|
||||||
|
|
||||||
|
func sessionDidDeactivate(_ session: WCSession) {
|
||||||
|
session.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
|
||||||
|
print("[iOS] Received message: \(message)")
|
||||||
|
if let request = message["request"] as? String, request == "data" {
|
||||||
|
let token = UserDefaults.standard.getFlutterToken()
|
||||||
|
let serverUrl = UserDefaults.standard.getServerUrl()
|
||||||
|
|
||||||
|
var data: [String: Any] = ["serverUrl": serverUrl ?? ""]
|
||||||
|
if let token = token {
|
||||||
|
data["token"] = token
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[iOS] Replying with data: \(data)")
|
||||||
|
replyHandler(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendDataToWatch() {
|
||||||
|
guard session.activationState == .activated else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = UserDefaults.standard.getFlutterToken()
|
||||||
|
let serverUrl = UserDefaults.standard.getServerUrl()
|
||||||
|
|
||||||
|
var data: [String: Any] = ["serverUrl": serverUrl ?? ""]
|
||||||
|
if let token = token {
|
||||||
|
data["token"] = token
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try session.updateApplicationContext(data)
|
||||||
|
print("[iOS] Sent application context: \(data)")
|
||||||
|
} catch {
|
||||||
|
print("[iOS] Failed to send application context: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 762 B |
@@ -1,98 +1,110 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>AppGroupId</key>
|
<key>AppGroupId</key>
|
||||||
<string>$(CUSTOM_GROUP_ID)</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<key>BUNDLE_ID</key>
|
<key>BUNDLE_ID</key>
|
||||||
<string>dev.solsynth.solian</string>
|
<string>dev.solsynth.solian</string>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Solian</string>
|
<string>Solian</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>solian</string>
|
<string>solian</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Editor</string>
|
<string>Editor</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
<dict>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>Editor</string>
|
||||||
<key>CLIENT_ID</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
<string></string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<false/>
|
<array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<string>solian</string>
|
||||||
<true/>
|
</array>
|
||||||
<key>NSCalendarsUsageDescription</key>
|
</dict>
|
||||||
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
</array>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>CLIENT_ID</key>
|
||||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
|
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
<false/>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
|
<true/>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSCalendarsUsageDescription</key>
|
||||||
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
|
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<array>
|
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
||||||
<string>INStartCallIntent</string>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>INSendMessageIntent</string>
|
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
|
||||||
</array>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<key>PLIST_VERSION</key>
|
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
||||||
<string>1</string>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<key>REVERSED_CLIENT_ID</key>
|
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
|
||||||
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
|
||||||
<true/>
|
<key>NSUserActivityTypes</key>
|
||||||
<key>UIBackgroundModes</key>
|
<array>
|
||||||
<array>
|
<string>INStartCallIntent</string>
|
||||||
<string>fetch</string>
|
<string>INSendMessageIntent</string>
|
||||||
<string>audio</string>
|
</array>
|
||||||
<string>remote-notification</string>
|
<key>PLIST_VERSION</key>
|
||||||
<string>voip</string>
|
<string>1</string>
|
||||||
</array>
|
<key>REVERSED_CLIENT_ID</key>
|
||||||
<key>UILaunchStoryboardName</key>
|
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
||||||
<string>LaunchScreen</string>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<key>UIMainStoryboardFile</key>
|
<true/>
|
||||||
<string>Main</string>
|
<key>UIBackgroundModes</key>
|
||||||
<key>UIStatusBarHidden</key>
|
<array>
|
||||||
<false/>
|
<string>fetch</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<string>audio</string>
|
||||||
<array>
|
<string>remote-notification</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>voip</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
</array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UILaunchStoryboardName</key>
|
||||||
</array>
|
<string>LaunchScreen</string>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<array>
|
<string>Main</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<key>UIStatusBarHidden</key>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<false/>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<array>
|
||||||
</array>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</dict>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
</array>
|
||||||
|
<key>WKCompanionAppBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let serverUrl = UserDefaults.standard.getServerUrl()
|
let serverUrl = UserDefaults.standard.getServerUrl()
|
||||||
let url = "\(serverUrl)/sphere/chat/\(metadata["room_id"] ?? "")/messages"
|
let url = "\(serverUrl)/messager/chat/\(metadata["room_id"] ?? "")/messages"
|
||||||
|
|
||||||
let parameters: [String: Any?] = [
|
let parameters: [String: Any?] = [
|
||||||
"content": textResponse.userText,
|
"content": textResponse.userText,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
func getAttachmentUrl(for identifier: String) -> String {
|
func getAttachmentUrl(for identifier: String) -> String {
|
||||||
let serverBaseUrl = "https://api.solian.app"
|
let serverBaseUrl = UserDefaults.standard.getServerUrl()
|
||||||
|
|
||||||
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
|
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ extension UserDefaults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getServerUrl(forKey key: String = "app_server_url") -> String {
|
func getServerUrl(forKey key: String = "app_server_url") -> String {
|
||||||
return self.getFlutterValue(forKey: key) ?? "https://nt.solian.app"
|
return self.getFlutterValue(forKey: key) ?? "https://api.solian.app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"platform" : "universal",
|
||||||
|
"reference" : "systemIndigoColor"
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-20x20@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-20x20@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-29x29@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-29x29@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-38x38@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-38x38@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "38x38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-40x40@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-40x40@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-60x60@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-60x60@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-64x64@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-64x64@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "64x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-68x68@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "68x68"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-76x76@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-83.5x83.5@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-ios-1024x1024.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-16x16.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-16x16@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-32x32.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-32x32@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-128x128.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-128x128@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-256x256.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-256x256@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-512x512.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-mac-512x512@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-22x22@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "22x22"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-24x24@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "24x24"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-27.5x27.5@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "27.5x27.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-29x29@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-30x30@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "30x30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-32x32@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-33x33@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "33x33"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-40x40@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-43.5x43.5@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "43.5x43.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-44x44@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "44x44"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-46x46@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "46x46"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-50x50@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "50x50"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-51x51@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "51x51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-54x54@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "54x54"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-86x86@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "86x86"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-98x98@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "98x98"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-108x108@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "108x108"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-117x117@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "117x117"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-129x129@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "129x129"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon-watchos-1024x1024.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 473 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
6
ios/Solian Watch App/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
21
ios/Solian Watch App/Assets.xcassets/Logo.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Solian Watch App/Assets.xcassets/Logo.imageset/icon.png
vendored
Normal file
|
After Width: | Height: | Size: 70 KiB |
58
ios/Solian Watch App/ContentView.swift
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// ContentView.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/28.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// The root view of the app.
|
||||||
|
struct ContentView: View {
|
||||||
|
@StateObject private var appState = AppState()
|
||||||
|
@State private var selection: Panel? = .explore
|
||||||
|
|
||||||
|
enum Panel: Hashable {
|
||||||
|
case explore
|
||||||
|
case chat
|
||||||
|
case notifications
|
||||||
|
case account
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationSplitView {
|
||||||
|
List(selection: $selection) {
|
||||||
|
AppInfoHeaderView()
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
.environmentObject(appState)
|
||||||
|
|
||||||
|
Label("Explore", systemImage: "globe.fill").tag(Panel.explore)
|
||||||
|
Label("Chat", systemImage: "message.fill").tag(Panel.chat)
|
||||||
|
Label("Notifications", systemImage: "bell.fill").tag(Panel.notifications)
|
||||||
|
Label("Account", systemImage: "person.circle.fill").tag(Panel.account)
|
||||||
|
}
|
||||||
|
.listStyle(.automatic)
|
||||||
|
} detail: {
|
||||||
|
switch selection {
|
||||||
|
case .explore:
|
||||||
|
ExploreView().environmentObject(appState)
|
||||||
|
case .chat:
|
||||||
|
ChatView().environmentObject(appState)
|
||||||
|
case .notifications:
|
||||||
|
NotificationView().environmentObject(appState)
|
||||||
|
case .account:
|
||||||
|
AccountView().environmentObject(appState)
|
||||||
|
case .none:
|
||||||
|
Text("Select a panel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Placeholder Implementations for Preview ---
|
||||||
|
|
||||||
|
struct ContentView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ContentView()
|
||||||
|
}
|
||||||
|
}
|
||||||
88
ios/Solian Watch App/Layouts/FlowLayout.swift
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
//
|
||||||
|
// FlowLayout.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Custom Layouts
|
||||||
|
|
||||||
|
struct FlowLayout: Layout {
|
||||||
|
var alignment: HorizontalAlignment = .leading
|
||||||
|
var spacing: CGFloat = 10
|
||||||
|
|
||||||
|
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||||
|
let containerWidth = proposal.width ?? 0
|
||||||
|
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
|
||||||
|
|
||||||
|
var currentX: CGFloat = 0
|
||||||
|
var currentY: CGFloat = 0
|
||||||
|
var lineHeight: CGFloat = 0
|
||||||
|
var totalHeight: CGFloat = 0
|
||||||
|
|
||||||
|
for size in sizes {
|
||||||
|
if currentX + size.width > containerWidth {
|
||||||
|
// New line
|
||||||
|
currentX = 0
|
||||||
|
currentY += lineHeight + spacing
|
||||||
|
totalHeight = currentY + size.height
|
||||||
|
lineHeight = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
currentX += size.width + spacing
|
||||||
|
lineHeight = max(lineHeight, size.height)
|
||||||
|
}
|
||||||
|
totalHeight = currentY + lineHeight
|
||||||
|
|
||||||
|
return CGSize(width: containerWidth, height: totalHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
||||||
|
let containerWidth = bounds.width
|
||||||
|
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
|
||||||
|
|
||||||
|
var currentX: CGFloat = 0
|
||||||
|
var currentY: CGFloat = 0
|
||||||
|
var lineHeight: CGFloat = 0
|
||||||
|
var lineElements: [(offset: Int, size: CGSize)] = []
|
||||||
|
|
||||||
|
func placeLine() {
|
||||||
|
let lineWidth = lineElements.map { $0.size.width }.reduce(0, +) + CGFloat(lineElements.count - 1) * spacing
|
||||||
|
var startX: CGFloat = 0
|
||||||
|
switch alignment {
|
||||||
|
case .leading:
|
||||||
|
startX = bounds.minX
|
||||||
|
case .center:
|
||||||
|
startX = bounds.minX + (containerWidth - lineWidth) / 2
|
||||||
|
case .trailing:
|
||||||
|
startX = bounds.maxX - lineWidth
|
||||||
|
default:
|
||||||
|
startX = bounds.minX
|
||||||
|
}
|
||||||
|
|
||||||
|
var xOffset = startX
|
||||||
|
for (offset, size) in lineElements {
|
||||||
|
subviews[offset].place(at: CGPoint(x: xOffset, y: bounds.minY + currentY), proposal: ProposedViewSize(size)) // Use bounds.minY + currentY
|
||||||
|
xOffset += size.width + spacing
|
||||||
|
}
|
||||||
|
lineElements.removeAll() // Clear elements for the next line
|
||||||
|
}
|
||||||
|
|
||||||
|
for (offset, size) in sizes.enumerated() {
|
||||||
|
if currentX + size.width > containerWidth && !lineElements.isEmpty {
|
||||||
|
// New line
|
||||||
|
placeLine()
|
||||||
|
currentX = 0
|
||||||
|
currentY += lineHeight + spacing
|
||||||
|
lineHeight = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
lineElements.append((offset, size))
|
||||||
|
currentX += size.width + spacing
|
||||||
|
lineHeight = max(lineHeight, size.height)
|
||||||
|
}
|
||||||
|
placeLine() // Place the last line
|
||||||
|
}
|
||||||
|
}
|
||||||
365
ios/Solian Watch App/Models/Models.swift
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
// Models.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Models
|
||||||
|
|
||||||
|
struct AppToken: Codable {
|
||||||
|
let token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnActivity: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let type: String
|
||||||
|
let data: ActivityData?
|
||||||
|
let createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ActivityData: Codable {
|
||||||
|
case post(SnPost)
|
||||||
|
case discovery(DiscoveryData)
|
||||||
|
case unknown
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
if let post = try? container.decode(SnPost.self) {
|
||||||
|
self = .post(post)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let discoveryData = try? container.decode(DiscoveryData.self) {
|
||||||
|
self = .discovery(discoveryData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self = .unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
// Not needed for decoding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnPost: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let title: String?
|
||||||
|
let content: String?
|
||||||
|
let publisher: SnPublisher
|
||||||
|
let attachments: [SnCloudFile]
|
||||||
|
let tags: [SnPostTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiscoveryData: Codable {
|
||||||
|
let items: [DiscoveryItem]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiscoveryItem: Codable, Identifiable {
|
||||||
|
var id = UUID()
|
||||||
|
let type: String
|
||||||
|
let data: DiscoveryItemData
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case type, data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DiscoveryItemData: Codable {
|
||||||
|
case realm(SnRealm)
|
||||||
|
case publisher(SnPublisher)
|
||||||
|
case article(SnWebArticle)
|
||||||
|
case unknown
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
if let realm = try? container.decode(SnRealm.self) {
|
||||||
|
self = .realm(realm)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let publisher = try? container.decode(SnPublisher.self) {
|
||||||
|
self = .publisher(publisher)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let article = try? container.decode(SnWebArticle.self) {
|
||||||
|
self = .article(article)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self = .unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
// Not needed for decoding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnRealm: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let description: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnPublisher: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let nick: String?
|
||||||
|
let description: String?
|
||||||
|
let picture: SnCloudFile?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnCloudFile: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let mimeType: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnPostTag: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let slug: String
|
||||||
|
let name: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnWebArticle: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let title: String
|
||||||
|
let url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnNotification: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let topic: String
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
let content: String
|
||||||
|
let meta: [String: AnyCodable]?
|
||||||
|
let priority: Int
|
||||||
|
let viewedAt: Date?
|
||||||
|
let accountId: String
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case topic
|
||||||
|
case title
|
||||||
|
case subtitle
|
||||||
|
case content
|
||||||
|
case meta
|
||||||
|
case priority
|
||||||
|
case viewedAt = "viewedAt"
|
||||||
|
case accountId = "accountId"
|
||||||
|
case createdAt = "createdAt"
|
||||||
|
case updatedAt = "updatedAt"
|
||||||
|
case deletedAt = "deletedAt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AnyCodable: Codable {
|
||||||
|
let value: Any
|
||||||
|
|
||||||
|
init(_ value: Any) {
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
if let intValue = try? container.decode(Int.self) {
|
||||||
|
value = intValue
|
||||||
|
} else if let doubleValue = try? container.decode(Double.self) {
|
||||||
|
value = doubleValue
|
||||||
|
} else if let boolValue = try? container.decode(Bool.self) {
|
||||||
|
value = boolValue
|
||||||
|
} else if let stringValue = try? container.decode(String.self) {
|
||||||
|
value = stringValue
|
||||||
|
} else if let arrayValue = try? container.decode([AnyCodable].self) {
|
||||||
|
value = arrayValue
|
||||||
|
} else if let dictValue = try? container.decode([String: AnyCodable].self) {
|
||||||
|
value = dictValue
|
||||||
|
} else {
|
||||||
|
value = NSNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
switch value {
|
||||||
|
case let intValue as Int:
|
||||||
|
try container.encode(intValue)
|
||||||
|
case let doubleValue as Double:
|
||||||
|
try container.encode(doubleValue)
|
||||||
|
case let boolValue as Bool:
|
||||||
|
try container.encode(boolValue)
|
||||||
|
case let stringValue as String:
|
||||||
|
try container.encode(stringValue)
|
||||||
|
case let arrayValue as [AnyCodable]:
|
||||||
|
try container.encode(arrayValue)
|
||||||
|
case let dictValue as [String: AnyCodable]:
|
||||||
|
try container.encode(dictValue)
|
||||||
|
default:
|
||||||
|
try container.encodeNil()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotificationResponse {
|
||||||
|
let notifications: [SnNotification]
|
||||||
|
let total: Int
|
||||||
|
let hasMore: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ActivityResponse {
|
||||||
|
let activities: [SnActivity]
|
||||||
|
let hasMore: Bool
|
||||||
|
let nextCursor: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnAccount: Codable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let nick: String
|
||||||
|
let profile: SnUserProfile
|
||||||
|
let createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnUserProfile: Codable {
|
||||||
|
let bio: String?
|
||||||
|
let picture: SnCloudFile?
|
||||||
|
let background: SnCloudFile?
|
||||||
|
let level: Int
|
||||||
|
let experience: Int
|
||||||
|
let levelingProgress: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnAccountStatus: Codable {
|
||||||
|
let id: String
|
||||||
|
let attitude: Int
|
||||||
|
let isOnline: Bool
|
||||||
|
let isInvisible: Bool
|
||||||
|
let isNotDisturb: Bool
|
||||||
|
let isCustomized: Bool
|
||||||
|
let label: String
|
||||||
|
let meta: [String: AnyCodable]?
|
||||||
|
let clearedAt: Date?
|
||||||
|
let accountId: String
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Chat Models
|
||||||
|
|
||||||
|
struct SnChatRoom: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let name: String?
|
||||||
|
let description: String?
|
||||||
|
let type: Int
|
||||||
|
let isPublic: Bool
|
||||||
|
let isCommunity: Bool
|
||||||
|
let picture: SnCloudFile?
|
||||||
|
let background: SnCloudFile?
|
||||||
|
let realmId: String?
|
||||||
|
let realm: SnRealm?
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
let members: [SnChatMember]?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnChatMessage: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let type: String
|
||||||
|
let content: String?
|
||||||
|
let nonce: String?
|
||||||
|
let meta: [String: AnyCodable]
|
||||||
|
let membersMentioned: [String]?
|
||||||
|
let editedAt: Date?
|
||||||
|
let attachments: [SnCloudFile]
|
||||||
|
let reactions: [SnChatReaction]
|
||||||
|
let repliedMessageId: String?
|
||||||
|
let forwardedMessageId: String?
|
||||||
|
let senderId: String
|
||||||
|
let sender: SnChatMember
|
||||||
|
let chatRoomId: String
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id, type, content, nonce, meta, membersMentioned, editedAt, attachments, reactions, repliedMessageId, forwardedMessageId, senderId, sender, chatRoomId, createdAt, updatedAt, deletedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try container.decode(String.self, forKey: .id)
|
||||||
|
type = try container.decode(String.self, forKey: .type)
|
||||||
|
content = try container.decodeIfPresent(String.self, forKey: .content)
|
||||||
|
nonce = try container.decodeIfPresent(String.self, forKey: .nonce)
|
||||||
|
meta = try container.decode([String: AnyCodable].self, forKey: .meta)
|
||||||
|
membersMentioned = try container.decodeIfPresent([String].self, forKey: .membersMentioned) ?? []
|
||||||
|
editedAt = try container.decodeIfPresent(Date.self, forKey: .editedAt)
|
||||||
|
attachments = try container.decode([SnCloudFile].self, forKey: .attachments)
|
||||||
|
reactions = try container.decode([SnChatReaction].self, forKey: .reactions)
|
||||||
|
repliedMessageId = try container.decodeIfPresent(String.self, forKey: .repliedMessageId)
|
||||||
|
forwardedMessageId = try container.decodeIfPresent(String.self, forKey: .forwardedMessageId)
|
||||||
|
senderId = try container.decode(String.self, forKey: .senderId)
|
||||||
|
sender = try container.decode(SnChatMember.self, forKey: .sender)
|
||||||
|
chatRoomId = try container.decode(String.self, forKey: .chatRoomId)
|
||||||
|
createdAt = try container.decode(Date.self, forKey: .createdAt)
|
||||||
|
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
|
||||||
|
deletedAt = try container.decodeIfPresent(Date.self, forKey: .deletedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnChatReaction: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let messageId: String
|
||||||
|
let senderId: String
|
||||||
|
let sender: SnChatMember
|
||||||
|
let symbol: String
|
||||||
|
let attitude: Int
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnChatMember: Codable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let chatRoomId: String
|
||||||
|
let chatRoom: SnChatRoom?
|
||||||
|
let accountId: String
|
||||||
|
let account: SnAccount
|
||||||
|
let nick: String?
|
||||||
|
let role: Int
|
||||||
|
let notify: Int
|
||||||
|
let joinedAt: Date?
|
||||||
|
let breakUntil: Date?
|
||||||
|
let timeoutUntil: Date?
|
||||||
|
let isBot: Bool
|
||||||
|
let status: SnAccountStatus?
|
||||||
|
let createdAt: Date
|
||||||
|
let updatedAt: Date
|
||||||
|
let deletedAt: Date?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnChatSummary: Codable {
|
||||||
|
let unreadCount: Int
|
||||||
|
let lastMessage: SnChatMessage?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatRoomsResponse {
|
||||||
|
let rooms: [SnChatRoom]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatInvitesResponse {
|
||||||
|
let invites: [SnChatMember]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MessageSyncResponse: Codable {
|
||||||
|
let messages: [SnChatMessage]
|
||||||
|
let currentTimestamp: Date
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case messages
|
||||||
|
case currentTimestamp = "current_timestamp"
|
||||||
|
}
|
||||||
|
}
|
||||||
95
ios/Solian Watch App/Services/ImageLoader.swift
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
//
|
||||||
|
// ImageLoader.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
import KingfisherWebP
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
// MARK: - Image Loader
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class ImageLoader: ObservableObject {
|
||||||
|
@Published var image: Image?
|
||||||
|
@Published var errorMessage: String?
|
||||||
|
@Published var isLoading = false
|
||||||
|
|
||||||
|
private var currentTask: DownloadTask?
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
currentTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadImage(from initialUrl: URL, token: String) async {
|
||||||
|
isLoading = true
|
||||||
|
errorMessage = nil
|
||||||
|
image = nil
|
||||||
|
|
||||||
|
// Create request modifier for authorization
|
||||||
|
let modifier = AnyModifier { request in
|
||||||
|
var r = request
|
||||||
|
r.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
r.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use WebP processor as default since the app seems to handle WebP images
|
||||||
|
let processor = WebPProcessor.default
|
||||||
|
|
||||||
|
// Use KingfisherManager to retrieve image with caching
|
||||||
|
currentTask = KingfisherManager.shared.retrieveImage(
|
||||||
|
with: initialUrl,
|
||||||
|
options: [
|
||||||
|
.requestModifier(modifier),
|
||||||
|
.processor(processor),
|
||||||
|
.cacheOriginalImage, // Cache the original image data
|
||||||
|
.loadDiskFileSynchronously // Load from disk cache synchronously if available
|
||||||
|
]
|
||||||
|
) { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
switch result {
|
||||||
|
case .success(let value):
|
||||||
|
self.image = Image(uiImage: value.image)
|
||||||
|
self.isLoading = false
|
||||||
|
case .failure(_):
|
||||||
|
// If WebP processor fails (likely due to format), try with default processor
|
||||||
|
let defaultProcessor = DefaultImageProcessor.default
|
||||||
|
self.currentTask = KingfisherManager.shared.retrieveImage(
|
||||||
|
with: initialUrl,
|
||||||
|
options: [
|
||||||
|
.requestModifier(modifier),
|
||||||
|
.processor(defaultProcessor),
|
||||||
|
.cacheOriginalImage,
|
||||||
|
.loadDiskFileSynchronously
|
||||||
|
]
|
||||||
|
) { [weak self] fallbackResult in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
switch fallbackResult {
|
||||||
|
case .success(let value):
|
||||||
|
self.image = Image(uiImage: value.image)
|
||||||
|
case .failure(let fallbackError):
|
||||||
|
self.errorMessage = fallbackError.localizedDescription
|
||||||
|
print("[watchOS] Image loading failed: \(fallbackError.localizedDescription)")
|
||||||
|
}
|
||||||
|
self.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
currentTask?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
643
ios/Solian Watch App/Services/NetworkService.swift
Normal file
@@ -0,0 +1,643 @@
|
|||||||
|
//
|
||||||
|
// NetworkService.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29. //
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - WebSocket Data Structures
|
||||||
|
|
||||||
|
enum WebSocketState: Equatable {
|
||||||
|
case connected
|
||||||
|
case connecting
|
||||||
|
case disconnected
|
||||||
|
case serverDown
|
||||||
|
case duplicateDevice
|
||||||
|
case error(String)
|
||||||
|
|
||||||
|
// Equatable conformance
|
||||||
|
static func == (lhs: WebSocketState, rhs: WebSocketState) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.connected, .connected),
|
||||||
|
(.connecting, .connecting),
|
||||||
|
(.disconnected, .disconnected),
|
||||||
|
(.serverDown, .serverDown),
|
||||||
|
(.duplicateDevice, .duplicateDevice):
|
||||||
|
return true
|
||||||
|
case let (.error(a), .error(b)):
|
||||||
|
return a == b
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WebSocketPacket {
|
||||||
|
let type: String
|
||||||
|
let data: [String: Any]?
|
||||||
|
let endpoint: String?
|
||||||
|
let errorMessage: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Network Service
|
||||||
|
|
||||||
|
class NetworkService {
|
||||||
|
private let session: URLSession
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let config = URLSessionConfiguration.ephemeral
|
||||||
|
config.waitsForConnectivity = true
|
||||||
|
session = URLSession(configuration: config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a serial queue for WebSocket operations
|
||||||
|
private let webSocketQueue = DispatchQueue(label: "com.solian.websocketQueue")
|
||||||
|
|
||||||
|
func fetchActivities(filter: String, cursor: String? = nil, token: String, serverUrl: String) async throws -> ActivityResponse {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
var components = URLComponents(url: baseURL.appendingPathComponent("/sphere/activities"), resolvingAgainstBaseURL: false)!
|
||||||
|
var queryItems = [URLQueryItem(name: "take", value: "20")]
|
||||||
|
if filter.lowercased() != "explore" {
|
||||||
|
queryItems.append(URLQueryItem(name: "filter", value: filter.lowercased()))
|
||||||
|
}
|
||||||
|
if let cursor = cursor {
|
||||||
|
queryItems.append(URLQueryItem(name: "cursor", value: cursor))
|
||||||
|
}
|
||||||
|
components.queryItems = queryItems
|
||||||
|
|
||||||
|
var request = URLRequest(url: components.url!)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
let activities = try decoder.decode([SnActivity].self, from: data)
|
||||||
|
|
||||||
|
let hasMore = (activities.first?.type ?? "empty") != "empty"
|
||||||
|
let nextCursor = activities.isEmpty ? nil : activities.map { $0.createdAt }.min()?.ISO8601Format()
|
||||||
|
|
||||||
|
return ActivityResponse(activities: activities, hasMore: hasMore, nextCursor: nextCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPost(title: String, content: String, token: String, serverUrl: String) async throws {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/sphere/posts")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let body: [String: Any] = ["title": title, "content": content]
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 {
|
||||||
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
print("[watchOS] createPost failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchNotifications(offset: Int = 0, take: Int = 20, token: String, serverUrl: String) async throws -> NotificationResponse {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
var components = URLComponents(url: baseURL.appendingPathComponent("/ring/notifications"), resolvingAgainstBaseURL: false)!
|
||||||
|
let queryItems = [URLQueryItem(name: "offset", value: String(offset)), URLQueryItem(name: "take", value: String(take))]
|
||||||
|
components.queryItems = queryItems
|
||||||
|
|
||||||
|
var request = URLRequest(url: components.url!)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
let notifications = try decoder.decode([SnNotification].self, from: data)
|
||||||
|
|
||||||
|
let httpResponse = response as? HTTPURLResponse
|
||||||
|
let total = Int(httpResponse?.value(forHTTPHeaderField: "X-Total") ?? "0") ?? 0
|
||||||
|
let hasMore = offset + notifications.count < total
|
||||||
|
|
||||||
|
return NotificationResponse(notifications: notifications, total: total, hasMore: hasMore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchUserProfile(token: String, serverUrl: String) async throws -> SnAccount {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/pass/accounts/me")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
return try decoder.decode(SnAccount.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAccountStatus(token: String, serverUrl: String) async throws -> SnAccountStatus? {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
return try decoder.decode(SnAccountStatus.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createOrUpdateStatus(attitude: Int, isInvisible: Bool, isNotDisturb: Bool, label: String?, token: String, serverUrl: String) async throws -> SnAccountStatus {
|
||||||
|
// Check if there\'s already a customized status
|
||||||
|
let existingStatus = try? await fetchAccountStatus(token: token, serverUrl: serverUrl)
|
||||||
|
let method = (existingStatus?.isCustomized == true) ? "PATCH" : "POST"
|
||||||
|
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = method
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
var body: [String: Any] = [
|
||||||
|
"attitude": attitude,
|
||||||
|
"is_invisible": isInvisible,
|
||||||
|
"is_not_disturb": isNotDisturb,
|
||||||
|
]
|
||||||
|
|
||||||
|
if let label = label, !label.isEmpty {
|
||||||
|
body["label"] = label
|
||||||
|
}
|
||||||
|
|
||||||
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 201 && httpResponse.statusCode != 200 {
|
||||||
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
print("[watchOS] createOrUpdateStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
return try decoder.decode(SnAccountStatus.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearStatus(token: String, serverUrl: String) async throws {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/pass/accounts/me/statuses")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "DELETE"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 204 {
|
||||||
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
print("[watchOS] clearStatus failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Chat API Methods
|
||||||
|
|
||||||
|
func fetchChatRooms(token: String, serverUrl: String) async throws -> ChatRoomsResponse {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/messager/chat")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
let rooms = try decoder.decode([SnChatRoom].self, from: data)
|
||||||
|
return ChatRoomsResponse(rooms: rooms)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchChatRoom(identifier: String, token: String, serverUrl: String) async throws -> SnChatRoom {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/messager/chat/\(identifier)")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
|
||||||
|
throw URLError(.resourceUnavailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
return try decoder.decode(SnChatRoom.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchChatInvites(token: String, serverUrl: String) async throws -> ChatInvitesResponse {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/messager/chat/invites")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, _) = try await session.data(for: request)
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
let invites = try decoder.decode([SnChatMember].self, from: data)
|
||||||
|
return ChatInvitesResponse(invites: invites)
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/messager/chat/invites/\(chatRoomId)/accept")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
||||||
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
print("[watchOS] acceptChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func declineChatInvite(chatRoomId: String, token: String, serverUrl: String) async throws {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
let url = baseURL.appendingPathComponent("/messager/chat/invites/\(chatRoomId)/decline")
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
|
||||||
|
let responseBody = String(data: data, encoding: .utf8) ?? ""
|
||||||
|
print("[watchOS] declineChatInvite failed with status code: \(httpResponse.statusCode), body: \(responseBody)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Message API Methods
|
||||||
|
|
||||||
|
func fetchChatMessages(chatRoomId: String, token: String, serverUrl: String, before: Date? = nil, take: Int = 50) async throws -> [SnChatMessage] {
|
||||||
|
guard let baseURL = URL(string: serverUrl) else {
|
||||||
|
throw URLError(.badURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try a different pattern: /messager/chat/messages with roomId as query param
|
||||||
|
var components = URLComponents(
|
||||||
|
url: baseURL.appendingPathComponent("/messager/chat/\(chatRoomId)/messages"),
|
||||||
|
resolvingAgainstBaseURL: false
|
||||||
|
)!
|
||||||
|
var queryItems = [
|
||||||
|
URLQueryItem(name: "take", value: String(take)),
|
||||||
|
]
|
||||||
|
if let before = before {
|
||||||
|
queryItems.append(URLQueryItem(name: "before", value: ISO8601DateFormatter().string(from: before)))
|
||||||
|
}
|
||||||
|
components.queryItems = queryItems
|
||||||
|
|
||||||
|
var request = URLRequest(url: components.url!)
|
||||||
|
request.httpMethod = "GET"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.setValue("SolianWatch/1.0", forHTTPHeaderField: "User-Agent")
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: request)
|
||||||
|
|
||||||
|
if let httpResponse = response as? HTTPURLResponse {
|
||||||
|
_ = String(data: data, encoding: .utf8) ?? "Unable to decode response body"
|
||||||
|
|
||||||
|
if httpResponse.statusCode != 200 {
|
||||||
|
print("[watchOS] fetchChatMessages failed with status \(httpResponse.statusCode)")
|
||||||
|
throw URLError(URLError.Code(rawValue: httpResponse.statusCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if data is empty
|
||||||
|
if data.isEmpty {
|
||||||
|
print("[watchOS] fetchChatMessages received empty response data")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.dateDecodingStrategy = .iso8601
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
do {
|
||||||
|
let messages = try decoder.decode([SnChatMessage].self, from: data)
|
||||||
|
print("[watchOS] fetchChatMessages successfully decoded \(messages.count) messages")
|
||||||
|
return messages
|
||||||
|
} catch {
|
||||||
|
print("error: ", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - WebSocket
|
||||||
|
|
||||||
|
private var webSocketTask: URLSessionWebSocketTask?
|
||||||
|
private var heartbeatTimer: Timer?
|
||||||
|
private var reconnectTimer: Timer?
|
||||||
|
private var isDisconnectingManually = false
|
||||||
|
|
||||||
|
private var lastToken: String?
|
||||||
|
private var lastServerUrl: String?
|
||||||
|
|
||||||
|
private var heartbeatAt: Date?
|
||||||
|
var heartbeatDelay: TimeInterval?
|
||||||
|
|
||||||
|
private let connectLock = NSLock()
|
||||||
|
|
||||||
|
private let packetSubject = PassthroughSubject<WebSocketPacket, Error>()
|
||||||
|
private let stateSubject = CurrentValueSubject<WebSocketState, Never>(.disconnected) // Changed to CurrentValueSubject
|
||||||
|
|
||||||
|
private var currentConnectionState: WebSocketState = .disconnected { // New property
|
||||||
|
didSet {
|
||||||
|
// Only send updates if the state has actually changed
|
||||||
|
if oldValue != currentConnectionState {
|
||||||
|
stateSubject.send(currentConnectionState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var packetStream: AnyPublisher<WebSocketPacket, Error> {
|
||||||
|
packetSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
var stateStream: AnyPublisher<WebSocketState, Never> {
|
||||||
|
stateSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectWebSocket(token: String, serverUrl: String) {
|
||||||
|
webSocketQueue.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.connectLock.lock()
|
||||||
|
defer { self.connectLock.unlock() }
|
||||||
|
|
||||||
|
// Prevent redundant connection attempts
|
||||||
|
if self.currentConnectionState == .connecting || self.currentConnectionState == .connected {
|
||||||
|
print("[WebSocket] Already connecting or connected, ignoring new connect request.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentConnectionState = .connecting
|
||||||
|
|
||||||
|
// Ensure any existing task is cancelled before starting a new one
|
||||||
|
self.webSocketTask?.cancel(with: .goingAway, reason: nil)
|
||||||
|
self.webSocketTask = nil
|
||||||
|
|
||||||
|
self.isDisconnectingManually = false // Reset this flag for a new connection attempt
|
||||||
|
|
||||||
|
self.lastToken = token
|
||||||
|
self.lastServerUrl = serverUrl
|
||||||
|
|
||||||
|
guard var urlComponents = URLComponents(string: serverUrl) else {
|
||||||
|
self.currentConnectionState = .error("Invalid server URL")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
urlComponents.scheme = urlComponents.scheme?.replacingOccurrences(of: "http", with: "ws")
|
||||||
|
urlComponents.path = "/ws"
|
||||||
|
urlComponents.queryItems = [URLQueryItem(name: "deviceAlt", value: "watch")]
|
||||||
|
|
||||||
|
guard let url = urlComponents.url else {
|
||||||
|
self.currentConnectionState = .error("Invalid WebSocket URL")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
print("[WebSocket] Trying connecting to \(url)")
|
||||||
|
|
||||||
|
self.webSocketTask = self.session.webSocketTask(with: request)
|
||||||
|
self.webSocketTask?.resume()
|
||||||
|
|
||||||
|
self.listenForWebSocketMessages()
|
||||||
|
self.scheduleHeartbeat()
|
||||||
|
self.currentConnectionState = .connected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listenForWebSocketMessages() {
|
||||||
|
// Ensure webSocketTask is still valid before attempting to receive
|
||||||
|
guard let task = webSocketTask else {
|
||||||
|
print("[WebSocket] listenForWebSocketMessages: webSocketTask is nil, stopping listen.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task.receive { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .failure(let error):
|
||||||
|
print("[WebSocket] Error in receiving message: \(error)")
|
||||||
|
// Only attempt to reconnect if not manually disconnecting
|
||||||
|
if !self.isDisconnectingManually {
|
||||||
|
self.currentConnectionState = .error(error.localizedDescription)
|
||||||
|
self.scheduleReconnect()
|
||||||
|
} else {
|
||||||
|
// If manually disconnecting, just ensure state is disconnected
|
||||||
|
self.currentConnectionState = .disconnected
|
||||||
|
}
|
||||||
|
case .success(let message):
|
||||||
|
switch message {
|
||||||
|
case .string(let text):
|
||||||
|
self.handleWebSocketMessage(text: text)
|
||||||
|
case .data(let data):
|
||||||
|
if let text = String(data: data, encoding: .utf8) {
|
||||||
|
self.handleWebSocketMessage(text: text)
|
||||||
|
}
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Continue listening for next message only if task is still valid
|
||||||
|
if self.webSocketTask === task { // Check if it's the same task
|
||||||
|
self.listenForWebSocketMessages()
|
||||||
|
} else {
|
||||||
|
print("[WebSocket] listenForWebSocketMessages: Task changed, stopping listen for old task.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleWebSocketMessage(text: String) {
|
||||||
|
guard let data = text.data(using: .utf8) else {
|
||||||
|
print("[WebSocket] Could not convert message to data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||||
|
let type = json["type"] as? String
|
||||||
|
{
|
||||||
|
let packet = WebSocketPacket(
|
||||||
|
type: type,
|
||||||
|
data: json["data"] as? [String: Any],
|
||||||
|
endpoint: json["endpoint"] as? String,
|
||||||
|
errorMessage: json["errorMessage"] as? String
|
||||||
|
)
|
||||||
|
|
||||||
|
print("[WebSocket] Received packet: \(packet.type) \(packet.errorMessage ?? "")")
|
||||||
|
|
||||||
|
if packet.type == "error.dupe" {
|
||||||
|
self.currentConnectionState = .duplicateDevice
|
||||||
|
self.disconnectWebSocket()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if packet.type == "pong" {
|
||||||
|
if let beatAt = self.heartbeatAt {
|
||||||
|
let now = Date()
|
||||||
|
self.heartbeatDelay = now.timeIntervalSince(beatAt)
|
||||||
|
print("[WebSocket] Server respond last heartbeat for \((self.heartbeatDelay ?? 0) * 1000) ms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.packetSubject.send(packet)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("[WebSocket] Could not parse message json: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleReconnect() {
|
||||||
|
reconnectTimer?.invalidate()
|
||||||
|
reconnectTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||||
|
guard let self = self, let token = self.lastToken, let serverUrl = self.lastServerUrl else { return }
|
||||||
|
print("[WebSocket] Attempting to reconnect...")
|
||||||
|
|
||||||
|
// No need to call disconnectWebSocket here, connectWebSocket will handle cancelling old task
|
||||||
|
self.isDisconnectingManually = false // Reset for the new connection attempt
|
||||||
|
|
||||||
|
self.connectWebSocket(token: token, serverUrl: serverUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleHeartbeat() {
|
||||||
|
heartbeatTimer?.invalidate()
|
||||||
|
heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in
|
||||||
|
self?.beatTheHeart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func beatTheHeart() {
|
||||||
|
heartbeatAt = Date()
|
||||||
|
print("[WebSocket] We\'re beating the heart! \(String(describing: self.heartbeatAt))")
|
||||||
|
sendWebSocketMessage(message: "{\"type\":\"ping\"}")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendWebSocketMessage(message: String) {
|
||||||
|
webSocketTask?.send(.string(message)) { error in
|
||||||
|
if let error = error {
|
||||||
|
print("[WebSocket] Error sending message: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnectWebSocket() {
|
||||||
|
isDisconnectingManually = true
|
||||||
|
reconnectTimer?.invalidate()
|
||||||
|
heartbeatTimer?.invalidate()
|
||||||
|
|
||||||
|
// Cancel the task and then nil it out
|
||||||
|
webSocketTask?.cancel(with: .goingAway, reason: nil)
|
||||||
|
webSocketTask = nil // Set to nil immediately after cancelling
|
||||||
|
|
||||||
|
self.currentConnectionState = .disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
58
ios/Solian Watch App/State/AppState.swift
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// AppState.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
// MARK: - App State
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class AppState: ObservableObject {
|
||||||
|
@Published var token: String? = nil
|
||||||
|
@Published var serverUrl: String? = nil
|
||||||
|
@Published var isReady = false
|
||||||
|
@Published var errorMessage: String? = nil
|
||||||
|
|
||||||
|
let networkService = NetworkService()
|
||||||
|
private var wcService = WatchConnectivityService()
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private var hasAttemptedConnection = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
wcService.$token.combineLatest(wcService.$serverUrl, wcService.$isFetched, wcService.$errorMessage)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] (token: String?, serverUrl: String?, isFetched: Bool?, errorMessage: String?) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.token = token
|
||||||
|
self.serverUrl = serverUrl
|
||||||
|
self.errorMessage = errorMessage
|
||||||
|
|
||||||
|
if let token = token, let serverUrl = serverUrl, !token.isEmpty, !serverUrl.isEmpty {
|
||||||
|
self.isReady = true
|
||||||
|
// Only connect once when we have valid credentials and tried fetch from phone
|
||||||
|
if !self.hasAttemptedConnection && isFetched == true {
|
||||||
|
self.hasAttemptedConnection = true
|
||||||
|
print("[AppState] Connecting WebSocket to server: \(serverUrl)")
|
||||||
|
self.networkService.connectWebSocket(token: token, serverUrl: serverUrl)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.isReady = false
|
||||||
|
if self.hasAttemptedConnection {
|
||||||
|
self.hasAttemptedConnection = false
|
||||||
|
// Disconnect WebSocket if token or serverUrl become invalid
|
||||||
|
self.networkService.disconnectWebSocket()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestData() {
|
||||||
|
wcService.requestDataFromPhone()
|
||||||
|
}
|
||||||
|
}
|
||||||
113
ios/Solian Watch App/State/WatchConnectivityService.swift
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import WatchConnectivity
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class WatchConnectivityService: NSObject, WCSessionDelegate, ObservableObject {
|
||||||
|
@Published var token: String?
|
||||||
|
@Published var serverUrl: String?
|
||||||
|
@Published var isFetched: Bool?
|
||||||
|
@Published var errorMessage: String?
|
||||||
|
|
||||||
|
private let session: WCSession
|
||||||
|
private let userDefaults = UserDefaults.standard
|
||||||
|
private let tokenKey = "token"
|
||||||
|
private let serverUrlKey = "serverUrl"
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
self.session = .default
|
||||||
|
super.init()
|
||||||
|
print("[watchOS] Activating WCSession")
|
||||||
|
self.session.delegate = self
|
||||||
|
self.session.activate()
|
||||||
|
|
||||||
|
// Load cached data
|
||||||
|
self.token = userDefaults.string(forKey: tokenKey)
|
||||||
|
self.serverUrl = userDefaults.string(forKey: serverUrlKey)
|
||||||
|
self.isFetched = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||||
|
if let error = error {
|
||||||
|
print("[watchOS] WCSession activation failed with error: \(error.localizedDescription)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.errorMessage = "WCSession activation failed: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("[watchOS] WCSession activated with state: \(activationState.rawValue)")
|
||||||
|
if activationState == .activated {
|
||||||
|
requestDataFromPhone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
|
||||||
|
print("[watchOS] Received application context: \(applicationContext)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let token = applicationContext["token"] as? String {
|
||||||
|
self.token = token
|
||||||
|
self.userDefaults.set(token, forKey: self.tokenKey)
|
||||||
|
}
|
||||||
|
if let serverUrl = applicationContext["serverUrl"] as? String {
|
||||||
|
self.serverUrl = serverUrl
|
||||||
|
self.userDefaults.set(serverUrl, forKey: self.serverUrlKey)
|
||||||
|
}
|
||||||
|
self.isFetched = true
|
||||||
|
self.errorMessage = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
|
||||||
|
print("[watchOS] Received message: \(message)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let token = message["token"] as? String {
|
||||||
|
self.token = token
|
||||||
|
self.userDefaults.set(token, forKey: self.tokenKey)
|
||||||
|
}
|
||||||
|
if let serverUrl = message["serverUrl"] as? String {
|
||||||
|
self.serverUrl = serverUrl
|
||||||
|
self.userDefaults.set(serverUrl, forKey: self.serverUrlKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestDataFromPhone() {
|
||||||
|
// Check if we already have valid data to avoid unnecessary requests
|
||||||
|
if let token = self.token, let serverUrl = self.serverUrl, !token.isEmpty, !serverUrl.isEmpty {
|
||||||
|
print("[watchOS] Skipped fetch - already have valid data")
|
||||||
|
self.isFetched = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard session.activationState == .activated else {
|
||||||
|
print("[watchOS] Session not activated yet, state: \(session.activationState.rawValue)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.errorMessage = "Session not ready yet"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[watchOS] Requesting data from phone")
|
||||||
|
session.sendMessage(["request": "data"]) { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
print("[watchOS] Received reply: \(response)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isFetched = true
|
||||||
|
if let token = response["token"] as? String {
|
||||||
|
self.token = token
|
||||||
|
self.userDefaults.set(token, forKey: self.tokenKey)
|
||||||
|
}
|
||||||
|
if let serverUrl = response["serverUrl"] as? String {
|
||||||
|
self.serverUrl = serverUrl
|
||||||
|
self.userDefaults.set(serverUrl, forKey: self.serverUrlKey)
|
||||||
|
}
|
||||||
|
self.errorMessage = nil // Clear any previous errors
|
||||||
|
}
|
||||||
|
} errorHandler: { error in
|
||||||
|
print("[watchOS] sendMessage failed with error: \(error.localizedDescription)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.errorMessage = "Failed to get data from phone: \(error.localizedDescription)"
|
||||||
|
// Don't set isFetched = true on error - allow retry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ios/Solian Watch App/Utils/AttachmentUtils.swift
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// AttachmentUtils.swift
|
||||||
|
// WatchRunner Watch App
|
||||||
|
//
|
||||||
|
// Created by LittleSheep on 2025/10/29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Helper Functions
|
||||||
|
|
||||||
|
func getAttachmentUrl(for fileId: String, serverUrl: String) -> URL? {
|
||||||
|
let urlString: String
|
||||||
|
if fileId.starts(with: "http") {
|
||||||
|
urlString = fileId
|
||||||
|
} else {
|
||||||
|
urlString = "\(serverUrl)/drive/files/\(fileId)"
|
||||||
|
}
|
||||||
|
return URL(string: urlString)
|
||||||
|
}
|
||||||