diff --git a/Cargo.lock b/Cargo.lock index 843e97e..c77583d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,18 @@ version = 4 [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "ar_archive_writer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +dependencies = [ + "object", +] [[package]] name = "arrayvec" @@ -22,35 +31,39 @@ dependencies = [ "futures", "hex", "ic-cdk", + "ic-management-canister-types", "ic-stable-structures", "minicbor 0.26.5", "minicbor-derive 0.16.2", "serde", "sha2", "shared", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] name = "atlas_space" version = "0.1.0" dependencies = [ + "base64", "candid", "ethnum", "hex", "ic-cdk", "ic-cdk-timers", "ic-ledger-types", + "ic-management-canister-types", "ic-stable-structures", "icrc-ledger-types", "minicbor 0.26.5", "minicbor-derive 0.16.2", "num-bigint", "serde", + "serde_json", "sha2", "shared", "slotmap", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -65,6 +78,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "binread" version = "2.2.0" @@ -105,9 +124,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "candid" -version = "0.10.14" +version = "0.10.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d90f5a1426d0489283a0bd5da9ed406fb3e69597e0d823dcb88a1965bb58d2" +checksum = "8037a01ec09d6c06883a38bad4f47b8d06158ad360b841e0ae5707c9884dfaf6" dependencies = [ "anyhow", "binread", @@ -128,30 +147,31 @@ dependencies = [ [[package]] name = "candid_derive" -version = "0.6.6" +version = "0.10.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de398570c386726e7a59d9887b68763c481477f9a043fb998a2e09d428df1a9" +checksum = "fb45f4d5eff3805598ee633dd80f8afb306c023249d34b5b7dfdc2080ea1df2e" dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.108", ] [[package]] name = "cc" -version = "1.2.30" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cpufeatures" @@ -202,7 +222,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.104", + "syn 2.0.108", ] [[package]] @@ -213,7 +233,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.104", + "syn 2.0.108", ] [[package]] @@ -224,9 +244,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -253,6 +273,12 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "fnv" version = "1.0.7" @@ -315,7 +341,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.108", ] [[package]] @@ -350,9 +376,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -375,9 +401,9 @@ dependencies = [ [[package]] name = "ic-cdk" -version = "0.18.5" +version = "0.18.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9cc3e0e86ee12504c749fa33793014f1f4d6956a8a70e4db595169c5f6ac26" +checksum = "4efb278f5d3ef033b3eed7f01f1096eaf67701896aa5ef69f5eddf5a84833dc0" dependencies = [ "candid", "ic-cdk-executor", @@ -388,29 +414,30 @@ dependencies = [ "serde", "serde_bytes", "slotmap", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] name = "ic-cdk-executor" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15948808e3e7b50749fe50838df77fccaf048c8af2c26884ff5c8f787c29787a" +checksum = "99f4ee8930fd2e491177e2eb7fff53ee1c407c13b9582bdc7d6920cf83109a2d" dependencies = [ + "ic0", "slotmap", ] [[package]] name = "ic-cdk-macros" -version = "0.18.5" +version = "0.18.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b190cace2b141a5801252115bdc27397d47f086c928af3e917ce1da81b17e3cd" +checksum = "7eb14c5d691cc9d72bb95459b4761e3a4b3444b85a63d17555d5ddd782969a1e" dependencies = [ "candid", "darling", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.108", ] [[package]] @@ -456,9 +483,9 @@ dependencies = [ [[package]] name = "ic-management-canister-types" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95f3af3543f6d0cbdecd2dcdfd4737ada2bd42d935cc787eec22090c96492c76" +checksum = "ea7e5b8a0f7c3b320d9450ac950547db4f24a31601b5d398f9680b64427455d2" dependencies = [ "candid", "serde", @@ -476,9 +503,9 @@ dependencies = [ [[package]] name = "ic0" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8877193e1921b5fd16accb0305eb46016868cd1935b05c05eca0ec007b943272" +checksum = "1499d08fd5be8f790d477e1865d63bab6a8d748300e141270c4296e6d5fdd6bc" [[package]] name = "ic_principal" @@ -507,9 +534,9 @@ dependencies = [ [[package]] name = "icrc-ledger-types" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c31beeee0e5ab964861a3d5ea2b5ed7b688b2b22400367a832b1fcf0db1fa4" +checksum = "aafb78e620b2cc2b000cd745c0504dfb23a828acc3dd6f1baef208cd6c471e32" dependencies = [ "base32", "candid", @@ -564,9 +591,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libm" @@ -576,9 +603,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "minicbor" @@ -617,7 +644,7 @@ checksum = "a9882ef5c56df184b8ffc107fc6c61e33ee3a654b021961d790a78571bb9d67a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.108", ] [[package]] @@ -656,6 +683,15 @@ dependencies = [ "libm", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "paste" version = "1.0.15" @@ -682,9 +718,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "pretty" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac98773b7109bc75f475ab5a134c9b64b87e59d776d31098d8f346922396a477" +checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" dependencies = [ "arrayvec", "typed-arena", @@ -693,64 +729,95 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "psm" -version = "0.1.26" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" dependencies = [ + "ar_archive_writer", "cc", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.17" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.108", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", ] [[package]] @@ -786,9 +853,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slotmap" @@ -801,9 +868,9 @@ dependencies = [ [[package]] name = "stacker" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" dependencies = [ "cc", "cfg-if", @@ -837,7 +904,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.104", + "syn 2.0.108", ] [[package]] @@ -853,9 +920,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", @@ -873,11 +940,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -888,25 +955,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.108", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.108", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -919,15 +986,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -941,21 +1008,21 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index 9fcf14b..e3a89ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,11 @@ members = ["src/atlas_main", "src/atlas_space", "src/shared"] resolver = "2" [workspace.dependencies] -candid = "0.10" -ic-cdk = "0.18.5" +candid = "0.10.19" +ic-cdk = "0.18.7" +ic-management-canister-types = "0.3.1" serde = "1.0.219" +serde_json = "1.0.140" hex = "0.4.3" ic-stable-structures = "0.6.8" minicbor = { version = "0.26.4", features = ["alloc", "derive"] } @@ -16,4 +18,4 @@ ic-ledger-types = "0.15.0" ethnum = "1.5.1" num-bigint = "0.4.6" num-traits = "0.2.19" -sha2 = "0.10.9" \ No newline at end of file +sha2 = "0.10.9" diff --git a/package-lock.json b/package-lock.json index 6b8a634..dc9b12d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16617,7 +16617,6 @@ "version": "6.3.5", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/src/atlas_frontend/src/canisters/atlasSpace/api.ts b/src/atlas_frontend/src/canisters/atlasSpace/api.ts index a18d9c8..6229ba4 100644 --- a/src/atlas_frontend/src/canisters/atlasSpace/api.ts +++ b/src/atlas_frontend/src/canisters/atlasSpace/api.ts @@ -1,7 +1,6 @@ import type { ActorSubclass } from "@dfinity/agent"; import type { _SERVICE, - AnswerFormat, ClosedTask, EditTaskArgs, State, @@ -9,6 +8,7 @@ import type { SubmissionData, Task, TaskContent, + TwitterTaskType, } from "../../../../declarations/atlas_space/atlas_space.did.js"; import { unwrapCall } from "../delegatedCall.js"; import { setSpace, setTasks } from "../../store/slices/spacesSlice.js"; @@ -16,17 +16,16 @@ import type { Dispatch } from "react"; import type { UnknownAction } from "@reduxjs/toolkit"; import type { Principal } from "@dfinity/principal"; import type { ExternalLinks } from "./types.js"; +import type { DiscordTaskContent, GenericTaskContent, TwitterTaskContent } from "../../utils/taskMapper.js"; +import type { DiscordGuild, DiscordInviteApiResponse } from "../../components/Integrations/discord/types.js"; + export interface ExpiredTask extends Task { expired: true; } +import { validateDiscordInvite as validateInvite } from "../../components/Integrations/discord/inviteLink.js"; +import { getUserGuilds } from "../../components/Integrations/discord/userGuilds.js"; -interface CreateSubtaskArg { - task_type: string; - title: string; - description: string; - allow_resubmit: boolean; - answer_format: AnswerFormat -} +type CreateSubtaskArg = GenericTaskContent | DiscordTaskContent | TwitterTaskContent; interface GetAtlasSpaceArgs { unAuthAtlasSpace: ActorSubclass<_SERVICE>; @@ -92,14 +91,38 @@ export const createNewTask = async ({ startTime, endTime, }: CreateNewSpaceTaskArgs) => { - const transformedTasks: TaskContent[] = tasks.map((arg) => ({ - TitleAndDescription: { - task_title: arg.title, - task_description: arg.description, - allow_resubmit: arg.allow_resubmit, - answer_format: arg.answer_format - }, - })); + const transformedTasks: TaskContent[] = tasks.map((arg) => { + if (arg.task_type === "discord") { + return { + DiscordTask: { + task_title: arg.title, + task_description: arg.description, + guild_id: arg.guild_id, + invite_link: arg.invite_link, + allow_resubmit: arg.allow_resubmit, + }, + }; + } else if (arg.task_type === "twitter") { + return { + TwitterTask: { + task_title: arg.title, + task_description: arg.description, + x_post_link: arg.x_post_link, + allow_resubmit: arg.allow_resubmit, + x_answer_format: arg.x_answer_format, + }, + }; + } else { + return { + TitleAndDescription: { + task_title: arg.title, + task_description: arg.description, + allow_resubmit: arg.allow_resubmit, + answer_format: arg.answer_format, + }, + }; + } + }); const call = authAtlasSpaceActor.create_task({ task_title: taskTitle, @@ -436,3 +459,90 @@ export const deleteClosedTask = async ({ errMsg: "Failed to delete closed task", }); }; + +export const getDiscordGuilds = async ( + accessToken: string +): Promise => { + return await getUserGuilds(accessToken); +}; + +export const validateDiscordInvite = async ( + inviteCode: string, + expectedGuildId: string +): Promise => { + return await validateInvite(inviteCode, expectedGuildId); +}; + +interface ExchangeCodeForTokenArgs { + authAtlasSpace: ActorSubclass<_SERVICE>; + code: string; + codeVerifier: string; +} + +export const exchange_code_for_token = async ({ + authAtlasSpace, + code, + codeVerifier +}: ExchangeCodeForTokenArgs) => { + const call = authAtlasSpace.exchange_code_for_token( + code, + codeVerifier + ); + + return unwrapCall({ + call, + errMsg: "Failed to exchange Twitter code for token", + }); +}; + +interface FetchXUserInfoArgs { + authAtlasSpace: ActorSubclass<_SERVICE>; + accessToken: string; +} + +export const fetch_x_user_info = async ({ + authAtlasSpace, + accessToken +}: FetchXUserInfoArgs) => { + const call = authAtlasSpace.fetch_x_user_info( + accessToken + ); + + return unwrapCall({ + call, + errMsg: "Failed to fetch X user info", + }); +} + +interface FetchXPostLikesArgs { + authAtlasSpace: ActorSubclass<_SERVICE>; + accessToken: string; + postId: string; +} + +export async function fetch_x_tweet_activity( + args: FetchXPostLikesArgs, + taskType: TwitterTaskType +) { + const { authAtlasSpace, accessToken, postId } = args; + + const call = authAtlasSpace.fetch_x_tweet_activity( + accessToken, + postId, + taskType + ); + + return unwrapCall({ + call, + errMsg: `Failed to fetch X post ${Object.keys(taskType)[0]}`, + }); +} + +export const fetch_x_post_likes = async (args: FetchXPostLikesArgs) => { + return fetch_x_tweet_activity(args, { 'Like' : null }); +}; + +export const fetch_x_post_retweets = async (args: FetchXPostLikesArgs) => { + return fetch_x_tweet_activity(args, { 'Retweet' : null }); +}; + diff --git a/src/atlas_frontend/src/canisters/atlasSpace/tasks.ts b/src/atlas_frontend/src/canisters/atlasSpace/tasks.ts index ae0e7f5..c63c3b5 100644 --- a/src/atlas_frontend/src/canisters/atlasSpace/tasks.ts +++ b/src/atlas_frontend/src/canisters/atlasSpace/tasks.ts @@ -1,32 +1,30 @@ +import type { Principal } from "@dfinity/principal"; import type { + SubmissionData, SubmissionState, TaskType, } from "../../../../declarations/atlas_space/atlas_space.did"; import type { UserSubmissionsData } from "./types"; -export const getUsersSubmissions = (tasks: TaskType[]) => { - const data = tasks.reduce((acc, task, index) => { - if ("GenericTask" in task) { - const genericTask = task.GenericTask; - genericTask.submission.forEach(([principal, submissionData]) => { - const principalText = principal.toText(); - if (!acc[principalText]) { - acc[principalText] = {}; - } - if (!acc[principalText][`${index}`]) { - acc[principalText][`${index}`] = { - submissionData, - taskType: "GenericTask", - }; - } - }); - return acc; - } +export const getUsersSubmissions = (tasks: { [key: string]: TaskType }) => { + const data = Object.entries(tasks).reduce((acc, [subtaskIdStr, task]) => { + const foundType = Object.keys(task)[0] as keyof TaskType; + const taskData = task[foundType] as { submission: [Principal, SubmissionData][] }; + + taskData.submission.forEach(([principal, submissionData]) => { + const principalText = principal.toText(); + if (!acc[principalText]) { + acc[principalText] = {}; + } + acc[principalText][subtaskIdStr] = { + submissionData, + taskType: foundType, + }; + }); return acc; }, {} as UserSubmissionsData); - return new UserSubmissions(data); -}; +} export class UserSubmissions { constructor(public userSubmissionsData: UserSubmissionsData) {} diff --git a/src/atlas_frontend/src/components/DiscordButton.tsx b/src/atlas_frontend/src/components/DiscordButton.tsx deleted file mode 100644 index d6a6946..0000000 --- a/src/atlas_frontend/src/components/DiscordButton.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react" -import Button from "./Shared/Button.tsx" - - -const DiscordButton = () => { - -} - -export default DiscordButton \ No newline at end of file diff --git a/src/atlas_frontend/src/components/Integrations/DiscordCallback.tsx b/src/atlas_frontend/src/components/Integrations/DiscordCallback.tsx index e37ea36..eb745d7 100644 --- a/src/atlas_frontend/src/components/Integrations/DiscordCallback.tsx +++ b/src/atlas_frontend/src/components/Integrations/DiscordCallback.tsx @@ -1,21 +1,13 @@ import React, { useEffect } from "react"; -import { useAuth } from "@nfid/identitykit/react"; const DiscordCallback = () => { - const { user } = useAuth(); useEffect(() => { const query = new URLSearchParams(window.location.hash.substring(1)); - const tokenType = query.get("token_type"); const accessToken = query.get("access_token"); - const state = query.get("state"); - const expiresIn = query.get("expires_in"); if ( - !tokenType || !accessToken || - !expiresIn || - state === user?.principal.toString() || !window.opener ) { return @@ -23,7 +15,7 @@ const DiscordCallback = () => { try { window.opener.postMessage( - { tokenType, accessToken, state, expiresIn }, + { accessToken }, window.location.origin ); } catch (err) { diff --git a/src/atlas_frontend/src/components/Integrations/TwitterCallback.tsx b/src/atlas_frontend/src/components/Integrations/TwitterCallback.tsx new file mode 100644 index 0000000..5e0e042 --- /dev/null +++ b/src/atlas_frontend/src/components/Integrations/TwitterCallback.tsx @@ -0,0 +1,28 @@ +import { useEffect } from "react"; + +const TwitterCallback = () => { + useEffect(() => { + const qs = new URLSearchParams(window.location.search); + const payload = { + type: "x-oauth2-callback", + code: qs.get("code") || undefined, + state: qs.get("state") || undefined, + error: qs.get("error") || undefined, + error_description: qs.get("error_description") || undefined, + }; + + try { + if (window.opener && window.opener !== window) { + window.opener.postMessage(payload, window.location.origin); + setTimeout(() => window.close(), 50); + } else { + sessionStorage.setItem("x_oauth_payload", JSON.stringify(payload)); + window.location.replace("/"); + } + } catch { + window.location.replace("/"); + } + }, []); + return null; +}; +export default TwitterCallback; \ No newline at end of file diff --git a/src/atlas_frontend/src/integrations/discord.ts b/src/atlas_frontend/src/components/Integrations/discord/discord.ts similarity index 95% rename from src/atlas_frontend/src/integrations/discord.ts rename to src/atlas_frontend/src/components/Integrations/discord/discord.ts index 9e2ab32..9b544d4 100644 --- a/src/atlas_frontend/src/integrations/discord.ts +++ b/src/atlas_frontend/src/components/Integrations/discord/discord.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import { DISCORD_CALLBACK_PATH } from "../router/paths"; +import { DISCORD_CALLBACK_PATH } from "../../../router/paths"; export interface UserData { accent_color: null | string; @@ -21,12 +21,10 @@ export interface UserData { username: string; verified: boolean; } - export const getOAuth2URL = (stateData?: string) => { const discordBase = "https://discord.com"; const path = "/oauth2/authorize"; const url = new URL(path, discordBase); - url.searchParams.set("client_id", import.meta.env.PUBLIC_DISCORD_CLIENT_ID); url.searchParams.set( "redirect_uri", @@ -35,10 +33,8 @@ export const getOAuth2URL = (stateData?: string) => { url.searchParams.set("response_type", "token"); url.searchParams.set("scope", "identify"); if (stateData) url.searchParams.set("state", stateData); - return url.toString(); }; - export const getUserData = async (token: string) => { const { data } = await axios.get( "https://discord.com/api/users/@me", @@ -48,6 +44,5 @@ export const getUserData = async (token: string) => { }, } ); - return data; -}; +}; \ No newline at end of file diff --git a/src/atlas_frontend/src/components/Integrations/discord/inviteLink.ts b/src/atlas_frontend/src/components/Integrations/discord/inviteLink.ts new file mode 100644 index 0000000..4799ac9 --- /dev/null +++ b/src/atlas_frontend/src/components/Integrations/discord/inviteLink.ts @@ -0,0 +1,37 @@ +import type { DiscordInviteApiResponse } from "./types"; +import axios, { AxiosError } from "axios"; + +export const validateDiscordInvite = async ( + inviteCode: string, + expectedGuildId: string +): Promise => { + if (!inviteCode || inviteCode.includes("/")) { + throw new Error("Invalid Discord invite code format."); + } + + const url = `https://discord.com/api/v10/invites/${inviteCode}?with_counts=false`; + + let response; + try { + response = await axios.get(url); + } catch (err: unknown) { + throw new Error( + `The invite link is invalid.` + ); + + } + + const inviteData: DiscordInviteApiResponse = response.data; + + const guild = inviteData.guild; + + if (guild && guild.id === expectedGuildId) { + return inviteData; + } else if (guild) { + throw new Error( + `Invite is for a different server` + ); + } else { + throw new Error("Invite is not for a valid server."); + } +}; \ No newline at end of file diff --git a/src/atlas_frontend/src/components/Integrations/discord/types.ts b/src/atlas_frontend/src/components/Integrations/discord/types.ts new file mode 100644 index 0000000..8201193 --- /dev/null +++ b/src/atlas_frontend/src/components/Integrations/discord/types.ts @@ -0,0 +1,17 @@ +export interface DiscordGuild { + id: string; + name: string; + icon: string | null; + owner: boolean; + permissions: string; +} + +export interface DiscordGuildInfo { + id: string; + name: string; +} + +export interface DiscordInviteApiResponse { + guild: DiscordGuildInfo | null; + expires_at: string | null; +} \ No newline at end of file diff --git a/src/atlas_frontend/src/components/Integrations/discord/userGuilds.ts b/src/atlas_frontend/src/components/Integrations/discord/userGuilds.ts new file mode 100644 index 0000000..e7968ed --- /dev/null +++ b/src/atlas_frontend/src/components/Integrations/discord/userGuilds.ts @@ -0,0 +1,19 @@ +import type { DiscordGuild } from "./types"; + +export const getUserGuilds = async (token: string): Promise => { + const url = "https://discord.com/api/users/@me/guilds"; + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${token.trim()}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch user guilds: ${response.statusText}`); + } + + const guilds: DiscordGuild[] = await response.json(); + return guilds; +}; \ No newline at end of file diff --git a/src/atlas_frontend/src/components/Integrations/twitter/twitter.ts b/src/atlas_frontend/src/components/Integrations/twitter/twitter.ts new file mode 100644 index 0000000..dcdda04 --- /dev/null +++ b/src/atlas_frontend/src/components/Integrations/twitter/twitter.ts @@ -0,0 +1,104 @@ + +type PopupOptions = { + clientId: string; + redirectUri: string; + scope: string; +}; + +type AuthResult = { + code: string; + state: string; + codeVerifier: string; +}; + +const AUTHORIZE_URL = "https://x.com/i/oauth2/authorize"; + + +function randomState(bytes = 24) { + const arr = new Uint8Array(bytes); + crypto.getRandomValues(arr); + return Array.from(arr, (b) => ("0" + b.toString(16)).slice(-2)).join(""); +} + + +async function buildAuthorizeUrl(opts: PopupOptions, state: string) { +const codeVerifier = Array.from(crypto.getRandomValues(new Uint8Array(64)), b => ("0"+b.toString(16)).slice(-2)).join(""); +const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier)); +const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""); + const url = new URL(AUTHORIZE_URL); + url.searchParams.set("response_type", "code"); + url.searchParams.set("client_id", opts.clientId); + url.searchParams.set("redirect_uri", opts.redirectUri); + url.searchParams.set("scope", opts.scope); + url.searchParams.set("state", state); + url.searchParams.set("code_challenge", codeChallenge); + url.searchParams.set("code_challenge_method", "S256"); + return { url: url.toString(), codeVerifier }; +} + +function openCenteredPopup(url: string, title: string, width = 520, height = 720): Window | null { + const dualScreenLeft = window.screenLeft ?? window.screenX ?? 0; + const dualScreenTop = window.screenTop ?? window.screenY ?? 0; + const w = window.innerWidth ?? document.documentElement.clientWidth ?? screen.width; + const h = window.innerHeight ?? document.documentElement.clientHeight ?? screen.height; + const left = Math.max(0, w / 2 - width / 2) + dualScreenLeft; + const top = Math.max(0, h / 2 - height / 2) + dualScreenTop; + const features = [ + "scrollbars=yes", + "resizable=yes", + `width=${width}`, + `height=${height}`, + `top=${top}`, + `left=${left}`, + ].join(","); + return window.open(url, title, features); +} + +export async function openTwitterLoginPopup(opts: PopupOptions): Promise { + if (!opts.clientId || !opts.redirectUri || !opts.scope) { + throw new Error("Required parameters are missing: clientId, redirectUri, scope."); + } + + const state = randomState(); + + const {url: authUrl, codeVerifier } = await buildAuthorizeUrl(opts, state); + const popup = openCenteredPopup(await authUrl, "Sign in with X"); + if (!popup) throw new Error("Failed to open login window."); + + const expectedOrigin = new URL(opts.redirectUri).origin; + + return new Promise((resolve, reject) => { + const onMessage = (ev: MessageEvent) => { + if (ev.origin !== expectedOrigin) return; + const data = ev.data as { type?: string; code?: string; state?: string; error?: string; error_description?: string }; + if (data?.type !== "x-oauth2-callback") return; + + cleanup(); + + if (data.error) { + reject(new Error(data.error_description || data.error)); + return; + } + if (!data.code || !data.state) { + reject(new Error("Missing code/state in callback answer.")); + return; + } + resolve({ code: data.code, state: data.state, codeVerifier }); + }; + + const checkClosed = setInterval(() => { + if (popup.closed) { + cleanup(); + reject(new Error("The login window has been closed.")); + } + }, 400); + + function cleanup() { + clearInterval(checkClosed); + window.removeEventListener("message", onMessage); + try { if (popup) popup.close(); } catch {} + } + + window.addEventListener("message", onMessage); + }); +} \ No newline at end of file diff --git a/src/atlas_frontend/src/components/Space/TaskCard/index.tsx b/src/atlas_frontend/src/components/Space/TaskCard/index.tsx index 57417e2..8d1ed08 100644 --- a/src/atlas_frontend/src/components/Space/TaskCard/index.tsx +++ b/src/atlas_frontend/src/components/Space/TaskCard/index.tsx @@ -16,7 +16,7 @@ interface TaskCardProps { time: number } -const TaskCard = ({ startingIn, task, id, type, spaceId}: TaskCardProps) => { +const TaskCard = ({ startingIn, task, id, type, spaceId }: TaskCardProps) => { const navigate = useNavigate(); const reward = formatUnits(task.token_reward.CkUsdc.amount, DECIMALS) @@ -61,3 +61,4 @@ const TaskCard = ({ startingIn, task, id, type, spaceId}: TaskCardProps) => { }; export default TaskCard; + diff --git a/src/atlas_frontend/src/components/Submissions/ReviewSubmission.tsx b/src/atlas_frontend/src/components/Submissions/ReviewSubmission.tsx index c790ee9..d428fbc 100644 --- a/src/atlas_frontend/src/components/Submissions/ReviewSubmission.tsx +++ b/src/atlas_frontend/src/components/Submissions/ReviewSubmission.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import type { _SERVICE } from "../../../../declarations/atlas_space/atlas_space.did"; +import type { _SERVICE, TaskType } from "../../../../declarations/atlas_space/atlas_space.did"; import type { TaskData } from "../../canisters/atlasSpace/types"; import { useForm, type SubmitHandler } from "react-hook-form"; import { useDispatch, useSelector } from "react-redux"; @@ -14,8 +14,14 @@ import type { ActorSubclass } from "@dfinity/agent"; import toast from "react-hot-toast"; import { runWithLoading } from "../../utils/loading"; import Button from "../Shared/Button"; +import { useTwitterAuth, type TweetActivityType, type UsersResponse } from "../../hooks/useTwitterAuth"; + +type GenericTaskType = Extract['GenericTask']; +type DiscordTaskType = Extract['DiscordTask']; +type TwitterTaskType = Extract['TwitterTask']; interface ReviewSubmissionProps { + task: GenericTaskType | DiscordTaskType | TwitterTaskType; submission: TaskData; authAtlasSpace: ActorSubclass<_SERVICE>; taskId: string; @@ -34,7 +40,27 @@ interface SubtaskSubmission { reason: string | null; } +const extractPostIdFromUrl = (url: string): string | null => { + if (!url) return null; + try { + const urlObj = new URL(url); + const pathParts = urlObj.pathname.split('/'); + + const statusIndex = pathParts.indexOf('status'); + + if (statusIndex !== -1 && pathParts.length > statusIndex + 1) { + const postId = pathParts[statusIndex + 1]; + return postId.split('?')[0]; + } + return null; + } catch (error) { + console.error("Invalid URL for parsing post ID:", error); + return null; + } +}; + const ReviewSubmission = ({ + task, submission, authAtlasSpace, taskId, @@ -49,6 +75,9 @@ const ReviewSubmission = ({ const isLoading = useSelector((state: RootState) => state.app.isLoading); const { register, handleSubmit } = useForm(); + const [loggingIn, setIsLoggingIn] = useState(false); + const [loggedIn, setIsLoggedIn] = useState(false); + const { signIn, accessToken, getTweetActivity } = useTwitterAuth(); const onSubmit: SubmitHandler = async (data) => { const rawReason = data.reason?.trim(); @@ -109,6 +138,88 @@ const ReviewSubmission = ({ }; const singleSubmissionState = Object.keys(submission.submissionData.state)[0]; + + const handleXSignIn = async () => { + await runWithLoading(async () => { + setIsLoggingIn(true); + setIsLoggedIn(false); + await signIn(); + }, dispatch, () => + setIsLoggingIn(false)); + setIsLoggedIn(true); + }; + + const handleCheckTwitterTask = async () => { + if (!accessToken) { + toast.error("You must be logged in to X to check post."); + return; + } + + if (!("TwitterTask" in task.task_content)) { + return; + } + + const xTaskType = task.task_content.TwitterTask.x_answer_format; + + let activityType: TweetActivityType; + let activityVerb: string; + + if ('Like' in xTaskType) { + activityType = "Like"; + activityVerb = "liked"; + } else if ('Repost' in xTaskType) { + activityType = "Retweet"; + activityVerb = "reposted"; + } else { + toast.error("Unknown Twitter task type defined in task."); + return; + } + + const postUrl = task.task_content.TwitterTask.x_post_link; + const postId = extractPostIdFromUrl(postUrl); + if (!postId) { + toast.error("Invalid X post link."); + return; + } + + await runWithLoading( + async () => { + const response = await getTweetActivity( + authAtlasSpace, + accessToken, + postId, + activityType + ); + + if (response === null) { + toast.error("Failed to fetch post likes."); + return; + } + + try { + const responseString = String(response); + const parsedResponse: UsersResponse = JSON.parse(responseString); + + if ("Twitter" in submission.submissionData.submission) { + const { x_user_id, x_name } = submission.submissionData.submission.Twitter; + const userFound = parsedResponse.data.some( + (User) => User.id === x_user_id.toString() + ); + + if (userFound) { + toast.success(`Verification Success: @${x_name} ${activityVerb} the post!`); + } else { + toast.error(`Verification Failed: @${x_name} did NOT ${activityVerb} the post.`); + } + } + } catch (error) { + console.error("Error parsing x post response:", error); + toast.error("Failed to parse post activity. Please try again."); + } + }, + dispatch + ); + }; if (singleSubmissionState !== "WaitingForReview") { return ( @@ -148,6 +259,36 @@ const ReviewSubmission = ({ ))} )} + {"Twitter" in submission.submissionData.submission && (() => { + const { x_username, x_name, created_at} = submission.submissionData.submission.Twitter; + const creationDate = new Date(created_at); + return ( +
+

Username: @{x_username}

+

Name: {x_name}

+

Account Creation Date: {creationDate.toLocaleDateString()}

+ {!loggedIn && + + } + {loggedIn && + + } +
+ ); + })()} +
+
+ )} + +
+ {canSubmit && !accessToken && ( + + )} + {canSubmit && accessToken && !isJoined && inviteLink && ( + + )} +
+ + {canSubmit && accessToken && isJoined && ( + + )} + + {showRejectionReason && ( +
+

Reject Reason:

+

{reasonText}

+
+ )} + + {isUserAdmin && + allSubmissions.length > 0 && + authAtlasSpace && + unAuthAtlasSpace && ( +
+ + {openReview && ( +
+ {allSubmissions.map(([principal, submissionData]) => { + const rowState = Object.keys(submissionData.state ?? {})[0]; + return ( +
+
+ copyPrincipal(principal.toString())} + > + User: {shortPrincipal(principal.toString())}{" "} + + + + {rowState} + +
+ { + getSpaceTasks({ + spaceId: spacePrincipal.toString(), + unAuthAtlasSpace, + dispatch, + }); + }} task={discordTask} + /> +
+ ); + })} +
+ )} +
+ )} +
+ + ); +}; + +export default DiscordTask; diff --git a/src/atlas_frontend/src/components/Task/tasks/GenericTask.tsx b/src/atlas_frontend/src/components/Task/tasks/GenericTask.tsx index 012bb0c..157695c 100644 --- a/src/atlas_frontend/src/components/Task/tasks/GenericTask.tsx +++ b/src/atlas_frontend/src/components/Task/tasks/GenericTask.tsx @@ -30,8 +30,13 @@ import { FaCaretRight } from "react-icons/fa6"; import { shortPrincipal } from "../../../utils/icp"; import { FiCopy } from "react-icons/fi"; +type GenericTaskType = Extract< + TaskType, + { GenericTask: unknown } +>["GenericTask"]; + interface GenericTaskProps { - genericTask: TaskType["GenericTask"]; + genericTask: GenericTaskType; spacePrincipal: Principal; taskId: string; subtaskId: number; @@ -64,13 +69,13 @@ const GenericTask = ({ const dispatch = useDispatch(); const { user, connect } = useAuth(); const [openSubmission, setSubmission] = useState(false); - const answerFormatKey = "TitleAndDescription" in genericTask.task_content - ? (Object.keys(genericTask.task_content.TitleAndDescription.answer_format)[0] as - | "Small" - | "Paragraph" - | "Long" - | "List") - : null; + const [openReview, SetReview] = useState(false); + const answerFormatKey = + "TitleAndDescription" in genericTask.task_content + ? (Object.keys( + genericTask.task_content.TitleAndDescription.answer_format + )[0] as "Small" | "Paragraph" | "Long" | "List") + : null; const maxTextLength = useMemo(() => { switch (answerFormatKey) { @@ -97,71 +102,77 @@ const GenericTask = ({ const textForm = useForm({ resolver: yupResolver(textSchema), defaultValues: { - taskSubmission: "" + taskSubmission: "", }, }); - const [openReview, SetReview] = useState(false); - + const userBlockchainData = deserialize( useSelector(selectUserBlockchainData) ); const userInfo = userBlockchainData ? new BlockchainUser(userBlockchainData) : null; - const isUserAdmin = isAdmin || (userInfo?.canAdministrate(spacePrincipal) ?? false); + const isUserAdmin = + isAdmin || (userInfo?.canAdministrate(spacePrincipal) ?? false); const copyPrincipal = (user: string) => { navigator.clipboard.writeText(user); toast.success("Copied full principal"); }; - const onSubmitText: SubmitHandler = async ({ taskSubmission }) => { + const onSubmitText: SubmitHandler = async ({ + taskSubmission, + }) => { if (!authAtlasSpace || !unAuthAtlasSpace) return; - await runWithLoading(async () => { - const call = submitSubtaskSubmission({ - authAtlasSpace, - taskId: BigInt(taskId), - subtaskId: BigInt(subtaskId), - submission: { Text: { content: taskSubmission } }, - }); - await toast.promise(call, { - loading: "Submitting response...", - success: "Submitted response.", - error: getErrorWithInfoToast("Failed to submit response."), - }); - - setSubmission(false); - await getSpaceTasks({ - spaceId: spacePrincipal.toString(), - unAuthAtlasSpace, - dispatch, - }); - }, dispatch, () => setSubmission(false)); + await runWithLoading( + async () => { + const call = submitSubtaskSubmission({ + authAtlasSpace, + taskId: BigInt(taskId), + subtaskId: BigInt(subtaskId), + submission: { Text: { content: taskSubmission } }, + }); + await toast.promise(call, { + loading: "Submitting response...", + success: "Submitted response.", + error: getErrorWithInfoToast("Failed to submit response."), + }); + + setSubmission(false); + await getSpaceTasks({ + spaceId: spacePrincipal.toString(), + unAuthAtlasSpace, + dispatch, + }); + }, + dispatch, + () => setSubmission(false) + ); }; const listSchema = yup.object({ - items: yup - .array() - .of( - yup.object({ - value: yup - .string() - .trim() - .max(254, "Item too long") - .required("Item cannot be empty"), - }) - ) - .max(25, "Max 25 items allowed") - .required(), -}); - -const listForm = useForm({ - resolver: yupResolver(listSchema), - defaultValues: { - items: [{ value: "" }] - }, -}); + items: yup + .array() + .of( + yup.object({ + value: yup + .string() + .trim() + .max(254, "Item too long") + .required("Item cannot be empty"), + }) + ) + .max(25, "Max 25 items allowed") + .required(), + }); + + const listForm = useForm({ + resolver: yupResolver(listSchema), + defaultValues: { + items: [{ value: "" }], + }, + }); const { fields, append, remove } = useFieldArray({ control: listForm.control, @@ -171,48 +182,56 @@ const listForm = useForm({ const onSubmitList = async () => { if (!authAtlasSpace || !unAuthAtlasSpace) return; - const items = listForm.getValues().items.map(item => item.value.trim()); - await runWithLoading(async () => { - const call = submitSubtaskSubmission({ - authAtlasSpace, - taskId: BigInt(taskId), - subtaskId: BigInt(subtaskId), - submission: { List: { items } }, - }); - await toast.promise(call, { - loading: "Submitting list...", - success: "Submitted response.", - error: getErrorWithInfoToast("Failed to submit response."), - }); - - setSubmission(false); - await getSpaceTasks({ - spaceId: spacePrincipal.toString(), - unAuthAtlasSpace, - dispatch, - }); - }, dispatch, () => setSubmission(false)); + const items = listForm.getValues().items.map((item) => item.value.trim()); + await runWithLoading( + async () => { + const call = submitSubtaskSubmission({ + authAtlasSpace, + taskId: BigInt(taskId), + subtaskId: BigInt(subtaskId), + submission: { List: { items } }, + }); + await toast.promise(call, { + loading: "Submitting list...", + success: "Submitted response.", + error: getErrorWithInfoToast("Failed to submit response."), + }); + + setSubmission(false); + await getSpaceTasks({ + spaceId: spacePrincipal.toString(), + unAuthAtlasSpace, + dispatch, + }); + }, + dispatch, + () => setSubmission(false) + ); }; const [, submissionData] = user?.principal - ? (genericTask.submission.find( + ? genericTask.submission.find( ([principal]) => principal.toString() === user.principal.toString() - ) ?? []) + ) ?? [] : []; const allSubmissions = genericTask.submission; const currentSubmissionState = submissionData?.state - ? Object.keys(submissionData?.state)[0] - : null; + ? Object.keys(submissionData?.state)[0] + : null; - const canSubmit = user && isUserInHub && ( - currentSubmissionState === null || - (currentSubmissionState === "Rejected" && genericTask.task_content.TitleAndDescription.allow_resubmit) - ); + const canSubmit = + user && + isUserInHub && + (currentSubmissionState === null || + (currentSubmissionState === "Rejected" && + ("TitleAndDescription" in genericTask.task_content + ? genericTask.task_content.TitleAndDescription.allow_resubmit + : "N/A"))); const rawState = Object.keys(submissionData?.state || {})[0] ?? null; const validStates = ["Rejected", "WaitingForReview", "Accepted"] as const; - type SubmissionState = typeof validStates[number]; + type SubmissionState = (typeof validStates)[number]; const STATUS_LABELS: Record = { WaitingForReview: "Waiting for review", @@ -225,13 +244,15 @@ const listForm = useForm({ const submissionState = validStates.includes(rawState as SubmissionState) ? (rawState as SubmissionState) : null; - + const badgeCls = (s?: string) => - ({ - Accepted: "bg-green-500/20 text-green-300 border border-green-500/30", - Rejected: "bg-red-500/20 text-red-300 border border-red-500/30", - WaitingForReview: "bg-primary/20 border border-white/10", - } as Record)[s ?? ""]; + (( + { + Accepted: "bg-green-500/20 text-green-300 border border-green-500/30", + Rejected: "bg-red-500/20 text-red-300 border border-red-500/30", + WaitingForReview: "bg-primary/20 border border-white/10", + } as Record + )[s ?? ""]); const userState = Object.keys(submissionData?.state ?? {})[0]; @@ -240,15 +261,21 @@ const listForm = useForm({ el.style.height = `${el.scrollHeight}px`; }; - return ( + return (
{submissionState === "WaitingForReview" && ( - + )} {submissionState === "Accepted" && ( - + )}
@@ -256,12 +283,16 @@ const listForm = useForm({

- {"TitleAndDescription" in genericTask.task_content + {"TitleAndDescription" in genericTask.task_content ? genericTask.task_content.TitleAndDescription.task_title : "N/A"}

{user && !isUserAdmin && submissionData && ( - + {prettyStatus} )} @@ -269,19 +300,23 @@ const listForm = useForm({
{user && !isUserAdmin && submissionData && (
- + {prettyStatus}
)}

- {"TitleAndDescription" in genericTask.task_content + {"TitleAndDescription" in genericTask.task_content ? genericTask.task_content.TitleAndDescription.task_title : "N/A"}

- {"TitleAndDescription" in genericTask.task_content + {"TitleAndDescription" in genericTask.task_content ? genericTask.task_content.TitleAndDescription.task_description : "N/A"}

@@ -290,9 +325,11 @@ const listForm = useForm({ {canSubmit && openSubmission && !disabled && ( <> {answerFormatKey !== "List" ? ( -
-
-

Submit response:

+ +
+

+ Submit response: +