diff --git a/.env.example b/.env.example
index ded2d82be..068c88f7a 100644
--- a/.env.example
+++ b/.env.example
@@ -45,4 +45,7 @@ AWS_ENDPOINT=http://localhost:4566
MONGO_MIGRATION_URL=
-OPENAI_API_KEY=
\ No newline at end of file
+OPENAI_API_KEY=
+
+ZENVIA_API_URL=
+ZENVIA_API_TOKEN=
\ No newline at end of file
diff --git a/.github/workflows/aws.yml b/.github/workflows/aws.yml
index 7315eb6b3..04b73c44a 100644
--- a/.github/workflows/aws.yml
+++ b/.github/workflows/aws.yml
@@ -38,6 +38,8 @@ env:
NEXT_PUBLIC_RECAPTCHA_SITEKEY: ${{ secrets.RECAPTCHA_SITEKEY }}
AGENTS_API_URL: ${{ secrets.DEVELOPMENT_AGENTS_API_URL }}
OPENAI_API_KEY: ${{ secrets.DEVELOPMENT_OPENAI_API_KEY }}
+ ZENVIA_API_URL: ${{ secrets.DEVELOPMENT_ZENVIA_API_URL }}
+ ZENVIA_API_TOKEN: ${{ secrets.DEVELOPMENT_ZENVIA_API_URL }}
jobs:
setup-build-publish:
@@ -71,6 +73,8 @@ jobs:
echo "NEXT_PUBLIC_ORY_SDK_URL=${{ secrets.ORY_SDK_URL }}" >> $GITHUB_ENV
echo "AGENTS_API_URL=${{ secrets.PRODUCTION_AGENTS_API_URL }}" >> $GITHUB_ENV
echo "OPENAI_API_KEY=${{ secrets.PRODUCTION_OPENAI_API_KEY }}" >> $GITHUB_ENV
+ echo "ZENVIA_API_URL=${{ secrets.PRODUCTION_ZENVIA_API_URL }}" >> $GITHUB_ENV
+ echo "ZENVIA_API_TOKEN=${{ secrets.PRODUCTION_ZENVIA_API_TOKEN }}" >> $GITHUB_ENV
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
@@ -86,48 +90,50 @@ jobs:
# Setting up config.yaml based on environment
- name: Set config.yaml
env:
- RECAPTCHA_SECRET: ${{ secrets.RECAPTCHA_SECRETKEY }}
+ RECAPTCHA_SECRET: ${{ secrets.RECAPTCHA_SECRETKEY }}
run: |
- sed -i "s/ENV/$ENVIRONMENT/g" config.$ENVIRONMENT.yaml
- sed -i "s%RECAPTCHA_SECRET%$RECAPTCHA_SECRET%g" config.$ENVIRONMENT.yaml
- sed -i "s%MONGODB_URI%$MONGODB_URI%g" config.$ENVIRONMENT.yaml
- sed -i "s%ORY_SDK_URL%$ORY_SDK_URL%g" config.$ENVIRONMENT.yaml
- sed -i "s%GITLAB_FEATURE_FLAG_URL%$GITLAB_FEATURE_FLAG_URL%g" config.$ENVIRONMENT.yaml
- sed -i "s%GITLAB_FEATURE_FLAG_INSTANCE_ID%$GITLAB_FEATURE_FLAG_INSTANCE_ID%g" config.$ENVIRONMENT.yaml
- sed -i "s/ORY_ACCESS_TOKEN/$ORY_ACCESS_TOKEN/g" config.$ENVIRONMENT.yaml
- sed -i "s/ALETHEIA_SCHEMA_ID/$ALETHEIA_SCHEMA_ID/g" config.$ENVIRONMENT.yaml
- sed -i "s%AWS_SDK_BUCKET%$AWS_SDK_BUCKET%g" config.$ENVIRONMENT.yaml
- sed -i "s%AWS_ACCESS_KEY_ID%$AWS_ACCESS_KEY_ID%g" config.$ENVIRONMENT.yaml
- sed -i "s%AWS_SECRET_ACCESS_KEY%$AWS_SECRET_ACCESS_KEY%g" config.$ENVIRONMENT.yaml
- sed -i "s%NOVU_API_KEY%$NOVU_API_KEY%g" config.$ENVIRONMENT.yaml
- sed -i "s%NOVU_APPLICATION_IDENTIFIER%$NOVU_APPLICATION_IDENTIFIER%g" config.$ENVIRONMENT.yaml
- sed -i "s%AGENTS_API_URL%$AGENTS_API_URL%g" config.$ENVIRONMENT.yaml
- sed -i "s%OPENAI_API_KEY%$OPENAI_API_KEY%g" config.$ENVIRONMENT.yaml
+ sed -i "s/ENV/$ENVIRONMENT/g" config.$ENVIRONMENT.yaml
+ sed -i "s%RECAPTCHA_SECRET%$RECAPTCHA_SECRET%g" config.$ENVIRONMENT.yaml
+ sed -i "s%MONGODB_URI%$MONGODB_URI%g" config.$ENVIRONMENT.yaml
+ sed -i "s%ORY_SDK_URL%$ORY_SDK_URL%g" config.$ENVIRONMENT.yaml
+ sed -i "s%GITLAB_FEATURE_FLAG_URL%$GITLAB_FEATURE_FLAG_URL%g" config.$ENVIRONMENT.yaml
+ sed -i "s%GITLAB_FEATURE_FLAG_INSTANCE_ID%$GITLAB_FEATURE_FLAG_INSTANCE_ID%g" config.$ENVIRONMENT.yaml
+ sed -i "s/ORY_ACCESS_TOKEN/$ORY_ACCESS_TOKEN/g" config.$ENVIRONMENT.yaml
+ sed -i "s/ALETHEIA_SCHEMA_ID/$ALETHEIA_SCHEMA_ID/g" config.$ENVIRONMENT.yaml
+ sed -i "s%AWS_SDK_BUCKET%$AWS_SDK_BUCKET%g" config.$ENVIRONMENT.yaml
+ sed -i "s%AWS_ACCESS_KEY_ID%$AWS_ACCESS_KEY_ID%g" config.$ENVIRONMENT.yaml
+ sed -i "s%AWS_SECRET_ACCESS_KEY%$AWS_SECRET_ACCESS_KEY%g" config.$ENVIRONMENT.yaml
+ sed -i "s%NOVU_API_KEY%$NOVU_API_KEY%g" config.$ENVIRONMENT.yaml
+ sed -i "s%NOVU_APPLICATION_IDENTIFIER%$NOVU_APPLICATION_IDENTIFIER%g" config.$ENVIRONMENT.yaml
+ sed -i "s%AGENTS_API_URL%$AGENTS_API_URL%g" config.$ENVIRONMENT.yaml
+ sed -i "s%OPENAI_API_KEY%$OPENAI_API_KEY%g" config.$ENVIRONMENT.yaml
+ sed -i "s%ZENVIA_API_URL%$ZENVIA_API_URL%g" config.$ENVIRONMENT.yaml
+ sed -i "s%ZENVIA_API_TOKEN%$ZENVIA_API_TOKEN%g" config.$ENVIRONMENT.yaml
- name: Set migrate-mongo-config.ts
run: |
- sed -i "s%MONGODB_URI%$MONGODB_URI%g" migrate-mongo-config-example.ts
- sed -i "s%MONGODB_NAME%$MONGODB_NAME%g" migrate-mongo-config-example.ts
+ sed -i "s%MONGODB_URI%$MONGODB_URI%g" migrate-mongo-config-example.ts
+ sed -i "s%MONGODB_NAME%$MONGODB_NAME%g" migrate-mongo-config-example.ts
# Setting user seed config
- name: Set config.seed.example.yaml
env:
- SMTP_HOST: ${{ secrets.SMTP_HOST }}
- SMTP_PORT: ${{ secrets.SMTP_PORT }}
- SMTP_EMAIL_USER: ${{ secrets.SMTP_EMAIL_USER }}
- SMTP_EMAIL_PASS: ${{ secrets.SMTP_EMAIL_PASS }}
- TEST_USER_PASS: ${{ secrets.TEST_USER_PASS }}
+ SMTP_HOST: ${{ secrets.SMTP_HOST }}
+ SMTP_PORT: ${{ secrets.SMTP_PORT }}
+ SMTP_EMAIL_USER: ${{ secrets.SMTP_EMAIL_USER }}
+ SMTP_EMAIL_PASS: ${{ secrets.SMTP_EMAIL_PASS }}
+ TEST_USER_PASS: ${{ secrets.TEST_USER_PASS }}
run: |
- sed -i "s%SMTP_HOST%$SMTP_HOST%g" config.seed.example.yaml
- sed -i "s%SMTP_PORT%$SMTP_PORT%g" config.seed.example.yaml
- sed -i "s%SMTP_EMAIL_USER%$SMTP_EMAIL_USER%g" config.seed.example.yaml
- sed -i "s%SMTP_EMAIL_PASS%$SMTP_EMAIL_PASS%g" config.seed.example.yaml
- sed -i "s/TEST_USER_PASS/$TEST_USER_PASS/g" config.seed.example.yaml
- sed -i "s%MONGODB_URI%$MONGODB_URI%g" config.seed.example.yaml
- sed -i "s%ORY_SDK_URL%$ORY_SDK_URL%g" config.seed.example.yaml
- sed -i "s/ORY_ACCESS_TOKEN/$ORY_ACCESS_TOKEN/g" config.seed.example.yaml
- sed -i "s/ALETHEIA_SCHEMA_ID/$ALETHEIA_SCHEMA_ID/g" config.seed.example.yaml
+ sed -i "s%SMTP_HOST%$SMTP_HOST%g" config.seed.example.yaml
+ sed -i "s%SMTP_PORT%$SMTP_PORT%g" config.seed.example.yaml
+ sed -i "s%SMTP_EMAIL_USER%$SMTP_EMAIL_USER%g" config.seed.example.yaml
+ sed -i "s%SMTP_EMAIL_PASS%$SMTP_EMAIL_PASS%g" config.seed.example.yaml
+ sed -i "s/TEST_USER_PASS/$TEST_USER_PASS/g" config.seed.example.yaml
+ sed -i "s%MONGODB_URI%$MONGODB_URI%g" config.seed.example.yaml
+ sed -i "s%ORY_SDK_URL%$ORY_SDK_URL%g" config.seed.example.yaml
+ sed -i "s/ORY_ACCESS_TOKEN/$ORY_ACCESS_TOKEN/g" config.seed.example.yaml
+ sed -i "s/ALETHEIA_SCHEMA_ID/$ALETHEIA_SCHEMA_ID/g" config.seed.example.yaml
# Build the Docker image
- name: Build
@@ -174,6 +180,8 @@ jobs:
echo "NOVU_API_KEY=${{ secrets.PRODUCTION_NOVU_API_KEY }}" >> $GITHUB_ENV
echo "NOVU_APPLICATION_IDENTIFIER=${{ secrets.PRODUCTION_NOVU_APPLICATION_IDENTIFIER }}" >> $GITHUB_ENV
echo "OPENAI_API_KEY=${{ secrets.PRODUCTION_OPENAI_API_KEY }}" >> $GITHUB_ENV
+ echo "ZENVIA_API_URL=${{ secrets.PRODUCTION_ZENVIA_API_URL }}" >> $GITHUB_ENV
+ echo "ZENVIA_API_TOKEN=${{ secrets.PRODUCTION_ZENVIA_API_TOKEN }}" >> $GITHUB_ENV
- name: Set environment
run: |
@@ -199,8 +207,8 @@ jobs:
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- aws-region: 'us-east-1'
- cluster-name: 'production'
+ aws-region: "us-east-1"
+ cluster-name: "production"
command: kubectl apply -f ./deployment/
- name: Validation
@@ -208,8 +216,8 @@ jobs:
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- aws-region: 'us-east-1'
- cluster-name: 'production'
+ aws-region: "us-east-1"
+ cluster-name: "production"
command: kubectl rollout status deployments/aletheia -n ${{ env.ENVIRONMENT }} --timeout=360s
if: success()
@@ -218,7 +226,7 @@ jobs:
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- aws-region: 'us-east-1'
- cluster-name: 'production'
+ aws-region: "us-east-1"
+ cluster-name: "production"
command: kubectl rollout undo deployments/aletheia -n ${{ env.ENVIRONMENT }}
if: failure()
diff --git a/.gitignore b/.gitignore
index 8c762e922..ff1c1df9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@
# dependencies
/node_modules
+.yarn/install-state.gz
+
# testing
/coverage
/cypress.env.json
@@ -65,4 +67,4 @@ data
# Auto-generated documentation
docs/compodoc/**/*
-docs/storybook/**/*
+docs/storybook/**/*
\ No newline at end of file
diff --git a/.yarn/cache/@grpc-grpc-js-npm-1.10.9-fc7c3de4a3-88d91c2271.zip b/.yarn/cache/@grpc-grpc-js-npm-1.10.9-fc7c3de4a3-88d91c2271.zip
new file mode 100644
index 000000000..27bfc9b14
Binary files /dev/null and b/.yarn/cache/@grpc-grpc-js-npm-1.10.9-fc7c3de4a3-88d91c2271.zip differ
diff --git a/.yarn/cache/@grpc-grpc-js-npm-1.9.9-5641bba424-71183a483b.zip b/.yarn/cache/@grpc-grpc-js-npm-1.9.9-5641bba424-71183a483b.zip
deleted file mode 100644
index 9fea62e96..000000000
Binary files a/.yarn/cache/@grpc-grpc-js-npm-1.9.9-5641bba424-71183a483b.zip and /dev/null differ
diff --git a/.yarn/cache/@grpc-proto-loader-npm-0.7.13-be5b6af1c1-399c1b8a46.zip b/.yarn/cache/@grpc-proto-loader-npm-0.7.13-be5b6af1c1-399c1b8a46.zip
new file mode 100644
index 000000000..db353380c
Binary files /dev/null and b/.yarn/cache/@grpc-proto-loader-npm-0.7.13-be5b6af1c1-399c1b8a46.zip differ
diff --git a/.yarn/cache/@js-sdsl-ordered-map-npm-4.4.2-158f6c6b74-a927ae4ff8.zip b/.yarn/cache/@js-sdsl-ordered-map-npm-4.4.2-158f6c6b74-a927ae4ff8.zip
new file mode 100644
index 000000000..19bdb23ad
Binary files /dev/null and b/.yarn/cache/@js-sdsl-ordered-map-npm-4.4.2-158f6c6b74-a927ae4ff8.zip differ
diff --git a/.yarn/cache/async-limiter-npm-1.0.1-7e6819bcdb-2b849695b4.zip b/.yarn/cache/async-limiter-npm-1.0.1-7e6819bcdb-2b849695b4.zip
deleted file mode 100644
index 38dfc9d4b..000000000
Binary files a/.yarn/cache/async-limiter-npm-1.0.1-7e6819bcdb-2b849695b4.zip and /dev/null differ
diff --git a/.yarn/cache/axios-npm-1.7.2-c89264f6f7-e457e2b0ab.zip b/.yarn/cache/axios-npm-1.7.2-c89264f6f7-e457e2b0ab.zip
new file mode 100644
index 000000000..5d20f72e9
Binary files /dev/null and b/.yarn/cache/axios-npm-1.7.2-c89264f6f7-e457e2b0ab.zip differ
diff --git a/.yarn/cache/braces-npm-3.0.2-782240b28a-e2a8e769a8.zip b/.yarn/cache/braces-npm-3.0.2-782240b28a-e2a8e769a8.zip
deleted file mode 100644
index 92998e3cc..000000000
Binary files a/.yarn/cache/braces-npm-3.0.2-782240b28a-e2a8e769a8.zip and /dev/null differ
diff --git a/.yarn/cache/braces-npm-3.0.3-582c14023c-b95aa0b3bd.zip b/.yarn/cache/braces-npm-3.0.3-582c14023c-b95aa0b3bd.zip
new file mode 100644
index 000000000..b4e6ac897
Binary files /dev/null and b/.yarn/cache/braces-npm-3.0.3-582c14023c-b95aa0b3bd.zip differ
diff --git a/.yarn/cache/decompress-response-npm-6.0.0-359de2878c-d377cf47e0.zip b/.yarn/cache/decompress-response-npm-6.0.0-359de2878c-d377cf47e0.zip
new file mode 100644
index 000000000..bbc1db518
Binary files /dev/null and b/.yarn/cache/decompress-response-npm-6.0.0-359de2878c-d377cf47e0.zip differ
diff --git a/.yarn/cache/deep-extend-npm-0.6.0-e182924219-7be7e5a8d4.zip b/.yarn/cache/deep-extend-npm-0.6.0-e182924219-7be7e5a8d4.zip
new file mode 100644
index 000000000..87f0270ec
Binary files /dev/null and b/.yarn/cache/deep-extend-npm-0.6.0-e182924219-7be7e5a8d4.zip differ
diff --git a/.yarn/cache/ejs-npm-3.1.10-4e8cf4bdc1-ce90637e9c.zip b/.yarn/cache/ejs-npm-3.1.10-4e8cf4bdc1-ce90637e9c.zip
new file mode 100644
index 000000000..2771e3267
Binary files /dev/null and b/.yarn/cache/ejs-npm-3.1.10-4e8cf4bdc1-ce90637e9c.zip differ
diff --git a/.yarn/cache/ejs-npm-3.1.9-e201b2088c-af6f10eb81.zip b/.yarn/cache/ejs-npm-3.1.9-e201b2088c-af6f10eb81.zip
deleted file mode 100644
index 3f5e3828e..000000000
Binary files a/.yarn/cache/ejs-npm-3.1.9-e201b2088c-af6f10eb81.zip and /dev/null differ
diff --git a/.yarn/cache/fill-range-npm-7.0.1-b8b1817caa-cc283f4e65.zip b/.yarn/cache/fill-range-npm-7.0.1-b8b1817caa-cc283f4e65.zip
deleted file mode 100644
index 1da4a361d..000000000
Binary files a/.yarn/cache/fill-range-npm-7.0.1-b8b1817caa-cc283f4e65.zip and /dev/null differ
diff --git a/.yarn/cache/fill-range-npm-7.1.1-bf491486db-b4abfbca38.zip b/.yarn/cache/fill-range-npm-7.1.1-bf491486db-b4abfbca38.zip
new file mode 100644
index 000000000..399180922
Binary files /dev/null and b/.yarn/cache/fill-range-npm-7.1.1-bf491486db-b4abfbca38.zip differ
diff --git a/.yarn/cache/ini-npm-1.3.8-fb5040b4c0-dfd98b0ca3.zip b/.yarn/cache/ini-npm-1.3.8-fb5040b4c0-dfd98b0ca3.zip
new file mode 100644
index 000000000..ee9245b9c
Binary files /dev/null and b/.yarn/cache/ini-npm-1.3.8-fb5040b4c0-dfd98b0ca3.zip differ
diff --git a/.yarn/cache/mimic-response-npm-3.1.0-a4a24b4e96-25739fee32.zip b/.yarn/cache/mimic-response-npm-3.1.0-a4a24b4e96-25739fee32.zip
new file mode 100644
index 000000000..a47a9a623
Binary files /dev/null and b/.yarn/cache/mimic-response-npm-3.1.0-a4a24b4e96-25739fee32.zip differ
diff --git a/.yarn/cache/packument-npm-2.0.0-1a3f6fd340-ac6882ce3b.zip b/.yarn/cache/packument-npm-2.0.0-1a3f6fd340-ac6882ce3b.zip
new file mode 100644
index 000000000..0b775f4e4
Binary files /dev/null and b/.yarn/cache/packument-npm-2.0.0-1a3f6fd340-ac6882ce3b.zip differ
diff --git a/.yarn/cache/protobufjs-npm-7.3.1-22f14d5229-81f5543a59.zip b/.yarn/cache/protobufjs-npm-7.3.1-22f14d5229-81f5543a59.zip
new file mode 100644
index 000000000..f613a1936
Binary files /dev/null and b/.yarn/cache/protobufjs-npm-7.3.1-22f14d5229-81f5543a59.zip differ
diff --git a/.yarn/cache/rc-npm-1.2.8-d6768ac936-2e26e052f8.zip b/.yarn/cache/rc-npm-1.2.8-d6768ac936-2e26e052f8.zip
new file mode 100644
index 000000000..f7372f98e
Binary files /dev/null and b/.yarn/cache/rc-npm-1.2.8-d6768ac936-2e26e052f8.zip differ
diff --git a/.yarn/cache/registry-auth-token-npm-4.2.2-ffd70a9849-c503019854.zip b/.yarn/cache/registry-auth-token-npm-4.2.2-ffd70a9849-c503019854.zip
new file mode 100644
index 000000000..aac4909a0
Binary files /dev/null and b/.yarn/cache/registry-auth-token-npm-4.2.2-ffd70a9849-c503019854.zip differ
diff --git a/.yarn/cache/registry-url-npm-5.1.0-f58d0ca7ff-bcea86c84a.zip b/.yarn/cache/registry-url-npm-5.1.0-f58d0ca7ff-bcea86c84a.zip
new file mode 100644
index 000000000..de1542129
Binary files /dev/null and b/.yarn/cache/registry-url-npm-5.1.0-f58d0ca7ff-bcea86c84a.zip differ
diff --git a/.yarn/cache/simple-concat-npm-1.0.1-48df70de29-4d211042cc.zip b/.yarn/cache/simple-concat-npm-1.0.1-48df70de29-4d211042cc.zip
new file mode 100644
index 000000000..6b694bed9
Binary files /dev/null and b/.yarn/cache/simple-concat-npm-1.0.1-48df70de29-4d211042cc.zip differ
diff --git a/.yarn/cache/simple-get-npm-4.0.1-fa2a97645d-e4132fd27c.zip b/.yarn/cache/simple-get-npm-4.0.1-fa2a97645d-e4132fd27c.zip
new file mode 100644
index 000000000..95cce5fb2
Binary files /dev/null and b/.yarn/cache/simple-get-npm-4.0.1-fa2a97645d-e4132fd27c.zip differ
diff --git a/.yarn/cache/strip-json-comments-npm-2.0.1-e7883b2d04-1074ccb632.zip b/.yarn/cache/strip-json-comments-npm-2.0.1-e7883b2d04-1074ccb632.zip
new file mode 100644
index 000000000..9c537fe05
Binary files /dev/null and b/.yarn/cache/strip-json-comments-npm-2.0.1-e7883b2d04-1074ccb632.zip differ
diff --git a/.yarn/cache/ws-npm-6.2.2-ca62a10fa0-aec3154ec5.zip b/.yarn/cache/ws-npm-6.2.2-ca62a10fa0-aec3154ec5.zip
deleted file mode 100644
index 3ce33965a..000000000
Binary files a/.yarn/cache/ws-npm-6.2.2-ca62a10fa0-aec3154ec5.zip and /dev/null differ
diff --git a/.yarn/cache/ws-npm-7.5.9-26f12a5ed6-c3c100a181.zip b/.yarn/cache/ws-npm-7.5.9-26f12a5ed6-c3c100a181.zip
deleted file mode 100644
index 5e9490b85..000000000
Binary files a/.yarn/cache/ws-npm-7.5.9-26f12a5ed6-c3c100a181.zip and /dev/null differ
diff --git a/.yarn/cache/ws-npm-8.11.0-ab72116a01-316b33aba3.zip b/.yarn/cache/ws-npm-8.11.0-ab72116a01-316b33aba3.zip
deleted file mode 100644
index dbd70f152..000000000
Binary files a/.yarn/cache/ws-npm-8.11.0-ab72116a01-316b33aba3.zip and /dev/null differ
diff --git a/.yarn/cache/ws-npm-8.13.0-26ffa3016a-53e991bbf9.zip b/.yarn/cache/ws-npm-8.13.0-26ffa3016a-53e991bbf9.zip
deleted file mode 100644
index 74e59aab9..000000000
Binary files a/.yarn/cache/ws-npm-8.13.0-26ffa3016a-53e991bbf9.zip and /dev/null differ
diff --git a/.yarn/cache/ws-npm-8.14.2-b339ac47a2-3ca0dad26e.zip b/.yarn/cache/ws-npm-8.14.2-b339ac47a2-3ca0dad26e.zip
deleted file mode 100644
index 3b175343d..000000000
Binary files a/.yarn/cache/ws-npm-8.14.2-b339ac47a2-3ca0dad26e.zip and /dev/null differ
diff --git a/.yarn/cache/ws-npm-8.17.1-f57fb24a2c-442badcce1.zip b/.yarn/cache/ws-npm-8.17.1-f57fb24a2c-442badcce1.zip
new file mode 100644
index 000000000..a0ebe465f
Binary files /dev/null and b/.yarn/cache/ws-npm-8.17.1-f57fb24a2c-442badcce1.zip differ
diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz
deleted file mode 100644
index 5bde176e4..000000000
Binary files a/.yarn/install-state.gz and /dev/null differ
diff --git a/config.development.yaml b/config.development.yaml
index 966a6d173..459b1d0cd 100644
--- a/config.development.yaml
+++ b/config.development.yaml
@@ -41,4 +41,7 @@ services:
application_identifier: NOVU_APPLICATION_IDENTIFIER
openai:
api_key: OPENAI_API_KEY
+ zenvia:
+ api_url: ZENVIA_API_URL
+ api_token: ZENVIA_API_TOKEN
diff --git a/config.example.yaml b/config.example.yaml
index 79de44edc..0c2cc0d0c 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -43,4 +43,7 @@ services:
api_key: NOVU_API_KEY
openai:
api_key: OPENAI_API_KEY
+ zenvia:
+ api_url: ZENVIA_API_URL
+ api_token: ZENVIA_API_TOKEN
diff --git a/config.production.yaml b/config.production.yaml
index d6a5445b7..d592b4f54 100644
--- a/config.production.yaml
+++ b/config.production.yaml
@@ -39,3 +39,6 @@ services:
application_identifier: NOVU_APPLICATION_IDENTIFIER
openai:
api_key: OPENAI_API_KEY
+ zenvia:
+ api_url: ZENVIA_API_URL
+ api_token: ZENVIA_API_TOKEN
diff --git a/config.seed.example.yaml b/config.seed.example.yaml
index 1c13d4861..816c964df 100644
--- a/config.seed.example.yaml
+++ b/config.seed.example.yaml
@@ -37,3 +37,6 @@ services:
api_key: NOVU_API_KEY
openai:
api_key: OPENAI_API_KEY
+ zenvia:
+ api_url: ZENVIA_API_URL
+ api_token: ZENVIA_API_TOKEN
diff --git a/config.seed.test.ci.yaml b/config.seed.test.ci.yaml
index 7785ca01a..c4e6c8935 100644
--- a/config.seed.test.ci.yaml
+++ b/config.seed.test.ci.yaml
@@ -36,3 +36,6 @@ services:
instanceId: {env(GITLAB_FEATURE_FLAG_INSTANCE_ID)}
openai:
api_key: {env(OPENAI_API_KEY)}
+ zenvia:
+ api_url: {env(ZENVIA_API_URL)}
+ api_token: {env(ZENVIA_API_TOKEN)}
diff --git a/config.test.ci.yaml b/config.test.ci.yaml
index 429d667f6..51b17baa5 100644
--- a/config.test.ci.yaml
+++ b/config.test.ci.yaml
@@ -35,3 +35,6 @@ services:
secretAccessKey: {env(AWS_SECRET_ACCESS_KEY)}
openai:
api_key: {env(OPENAI_API_KEY)}
+ zenvia:
+ api_url: {env(ZENVIA_API_URL)}
+ api_token: {env(ZENVIA_API_TOKEN)}
diff --git a/cypress/e2e/tests/header.cy.ts b/cypress/e2e/tests/header.cy.ts
index 0265ae351..b7351b02f 100644
--- a/cypress/e2e/tests/header.cy.ts
+++ b/cypress/e2e/tests/header.cy.ts
@@ -23,10 +23,16 @@ describe("Test the header menus", () => {
cy.url().should("contains", "claim");
});
- it("Open side bar and click sources", () => {
+ it("Open side bar and click source", () => {
cy.get(locators.menu.SIDE_MENU).click();
- cy.get("[data-cy=testSourcestItem]").click();
- cy.url().should("contains", "sources");
+ cy.get("[data-cy=testSourcetItem]").click();
+ cy.url().should("contains", "source");
+ });
+
+ it("Open side bar and click source", () => {
+ cy.get(locators.menu.SIDE_MENU).click();
+ cy.get("[data-cy=testVerificationRequestItem]").click();
+ cy.url().should("contains", "verification-request");
});
it("Open side bar and click about", () => {
diff --git a/cypress/e2e/tests/image.cy.ts b/cypress/e2e/tests/image.cy.ts
index a1a35503f..539ce3a89 100644
--- a/cypress/e2e/tests/image.cy.ts
+++ b/cypress/e2e/tests/image.cy.ts
@@ -23,9 +23,9 @@ describe("Create image claim", () => {
cy.get(locators.claim.INPUT_SOURCE)
.should("be.visible")
- .type(claim.source);
+ .type(claim.imageSource);
cy.get(locators.claim.BTN_UPLOAD_IMAGE).should("be.visible");
- cy.get('input[type="file"]').selectFile(claim.imageSource, {
+ cy.get('input[type="file"]').selectFile(claim.imageSourceFile, {
force: true,
});
cy.get("[data-cy=testCheckboxAcceptTerms]").click();
diff --git a/cypress/e2e/tests/review.cy.ts b/cypress/e2e/tests/review.cy.ts
index d22939c77..661cc1e5e 100644
--- a/cypress/e2e/tests/review.cy.ts
+++ b/cypress/e2e/tests/review.cy.ts
@@ -23,6 +23,31 @@ const goToClaimReviewPage = () => {
cy.get(locators.claim.BTN_SEE_FULL_REVIEW).should("exist");
};
+const assignUser = () => {
+ cy.get(locators.claimReview.BTN_START_CLAIM_REVIEW).should("exist").click();
+ cy.get(locators.claimReview.INPUT_USER)
+ .should("exist")
+ .type(review.username, { delay: 200 });
+ cy.get(".ant-select-item-option-active").click();
+ cy.get('[title="reCAPTCHA"]').should("exist");
+ cy.get(locators.claimReview.BTN_ASSIGN_USER).should("be.disabled");
+ cy.checkRecaptcha();
+ cy.get(locators.claimReview.BTN_ASSIGN_USER).should("be.enabled").click();
+ cy.get(locators.claimReview.INPUT_CLASSIFICATION).should("exist");
+};
+
+const blockAssignedUserReview = () => {
+ cy.checkRecaptcha();
+ cy.get(locators.claimReview.BTN_SELECTED_REVIEW).should("exist").click();
+ cy.get(locators.claimReview.INPUT_REVIEWER)
+ .should("exist")
+ .type(review.username, { delay: 200 });
+ cy.get(".ant-select-item-option-active").click();
+ cy.checkRecaptcha();
+ cy.get(locators.claimReview.BTN_SUBMIT).should("be.enabled").click();
+ cy.get(locators.claimReview.TEXT_REVIEWER_ERROR).should("exist");
+};
+
describe("Test claim review", () => {
it("should not show start review when not logged in", () => {
cy.visit(
@@ -36,25 +61,12 @@ describe("Test claim review", () => {
it("should be able to assign a user", () => {
cy.login();
goToClaimReviewPage();
- cy.get(locators.claimReview.BTN_START_CLAIM_REVIEW)
- .should("exist")
- .click();
- cy.get(locators.claimReview.INPUT_USER)
- .should("exist")
- .type(review.username, { delay: 200 });
- cy.get(".ant-select-item-option-active").click();
- cy.get('[title="reCAPTCHA"]').should("exist");
- cy.get(locators.claimReview.BTN_ASSIGN_USER).should("be.disabled");
- cy.checkRecaptcha();
- cy.get(locators.claimReview.BTN_ASSIGN_USER)
- .should("be.enabled")
- .click();
- cy.get(locators.claimReview.INPUT_CLASSIFICATION).should("exist");
+ assignUser();
});
it("should be able to submit full review fields", () => {
cy.login();
- cy.intercept("GET", "/api/claimreviewtask/editor-content/*", (req) => {
+ cy.intercept("GET", "/api/reviewtask/editor-content/*", (req) => {
req.reply({
statusCode: 200,
body: review.editorContent,
@@ -102,16 +114,6 @@ describe("Test claim review", () => {
it("should not be able submit after choosing assigned user as reviewer", () => {
cy.login();
goToClaimReviewPage();
- cy.checkRecaptcha();
- cy.get(locators.claimReview.BTN_SELECTED_REVIEW)
- .should("exist")
- .click();
- cy.get(locators.claimReview.INPUT_REVIEWER)
- .should("exist")
- .type(review.username, { delay: 200 });
- cy.get(".ant-select-item-option-active").click();
- cy.checkRecaptcha();
- cy.get(locators.claimReview.BTN_SUBMIT).should("be.enabled").click();
- cy.get(locators.claimReview.TEXT_REVIEWER_ERROR).should("exist");
+ blockAssignedUserReview();
});
});
diff --git a/cypress/e2e/tests/source.cy.ts b/cypress/e2e/tests/source.cy.ts
new file mode 100644
index 000000000..e211d72c4
--- /dev/null
+++ b/cypress/e2e/tests/source.cy.ts
@@ -0,0 +1,82 @@
+/* eslint-disable no-undef */
+///
+
+import locators from "../../support/locators";
+import source from "../../fixtures/source";
+import review from "../../fixtures/review";
+
+const goToSourceReviewPage = () => {
+ cy.visit(`http://localhost:3000/source/${source.data_hash}`);
+};
+
+describe("Create source and source review", () => {
+ beforeEach("login", () => cy.login());
+
+ it("Should create a new Source", () => {
+ cy.get(locators.floatButton.FLOAT_BUTTON).should("be.visible").click();
+ cy.get(locators.floatButton.ADD_SOURCE).should("be.visible").click();
+
+ cy.url().should("contains", "source");
+ cy.get(locators.source.INPUT_SOURCE).type(source.href);
+ cy.checkRecaptcha();
+ cy.get(`${locators.source.BTN_SUBMIT_SOURCE}`).click();
+ });
+
+ it("should not show start source review when not logged in", () => {
+ goToSourceReviewPage();
+ cy.get(locators.claimReview.BTN_START_CLAIM_REVIEW).should("not.exist");
+ });
+
+ it("should be able to assign a user", () => {
+ goToSourceReviewPage();
+ cy.get(locators.claimReview.BTN_START_CLAIM_REVIEW)
+ .should("exist")
+ .click();
+ cy.get(locators.claimReview.INPUT_USER)
+ .should("exist")
+ .type(review.username, { delay: 200 });
+ cy.get(".ant-select-item-option-active").click();
+ cy.get('[title="reCAPTCHA"]').should("exist");
+ cy.get(locators.claimReview.BTN_ASSIGN_USER).should("be.disabled");
+ cy.checkRecaptcha();
+ cy.get(locators.claimReview.BTN_ASSIGN_USER)
+ .should("be.enabled")
+ .click();
+ cy.get(locators.claimReview.INPUT_CLASSIFICATION).should("exist");
+ });
+
+ it("should be able to submit source review fields", () => {
+ goToSourceReviewPage();
+ cy.get(locators.claimReview.INPUT_CLASSIFICATION)
+ .should("exist")
+ .click();
+ cy.get(`[data-cy=${review.classification}]`)
+ .should("be.visible")
+ .click();
+
+ cy.get(locators.claimReview.INPUT_SUMMARY)
+ .should("exist")
+ .type(review.summary);
+
+ cy.checkRecaptcha();
+ cy.get(locators.claimReview.BTN_FINISH_REPORT)
+ .should("be.enabled")
+ .click();
+ cy.get(locators.claimReview.BTN_SELECTED_REVIEW).should("exist");
+ });
+
+ it("should not be able submit after choosing assigned user as reviewer", () => {
+ goToSourceReviewPage();
+ cy.checkRecaptcha();
+ cy.get(locators.claimReview.BTN_SELECTED_REVIEW)
+ .should("exist")
+ .click();
+ cy.get(locators.claimReview.INPUT_REVIEWER)
+ .should("exist")
+ .type(review.username, { delay: 200 });
+ cy.get(".ant-select-item-option-active").click();
+ cy.checkRecaptcha();
+ cy.get(locators.claimReview.BTN_SUBMIT).should("be.enabled").click();
+ cy.get(locators.claimReview.TEXT_REVIEWER_ERROR).should("exist");
+ });
+});
diff --git a/cypress/fixtures/claim.ts b/cypress/fixtures/claim.ts
index 1ac7c0987..fc04e20b5 100644
--- a/cypress/fixtures/claim.ts
+++ b/cypress/fixtures/claim.ts
@@ -1,10 +1,11 @@
const claim = {
title: "Speech Claim Title",
content: "Speech Claim Content Lorem Ipsum Dolor Sit Amet...",
- source: "http://wikipedia.org",
+ source: "https://wikimedia.org",
+ imageSource: "https://aletheiafact.org",
slug: "speech-claim-title",
imageTitle: "Image Claim Title",
- imageSource: "cypress/fixtures/imageTest1.png",
+ imageSourceFile: "cypress/fixtures/imageTest1.png",
imagePersonalitySource: "cypress/fixtures/imageTest2.png",
imageSlug: "image-claim-title",
};
diff --git a/cypress/fixtures/review.ts b/cypress/fixtures/review.ts
index eb3ebaf06..66b3bb2bb 100644
--- a/cypress/fixtures/review.ts
+++ b/cypress/fixtures/review.ts
@@ -5,7 +5,7 @@ const review = {
question2: "Question 2 content",
report: "Verification Report content",
process: "Verification process content",
- source1: "https://wikipedia.org",
+ source1: "https://wikidata.org",
source2: "https://google.com",
classification: "arguable",
editorContent: {
@@ -32,7 +32,7 @@ const review = {
{
type: "link",
attrs: {
- href: "https://wikipedia.org",
+ href: "https://wikidata.org",
target: null,
auto: false,
id: "lovjnfw8",
diff --git a/cypress/fixtures/source.ts b/cypress/fixtures/source.ts
new file mode 100644
index 000000000..d7cfd9529
--- /dev/null
+++ b/cypress/fixtures/source.ts
@@ -0,0 +1,6 @@
+const source = {
+ href: "https://wikipedia.org",
+ data_hash: "bb2c466041b713fdd03e28917286baa2",
+};
+
+export default source;
diff --git a/cypress/support/locators.ts b/cypress/support/locators.ts
index 95ae60425..f6265c57a 100644
--- a/cypress/support/locators.ts
+++ b/cypress/support/locators.ts
@@ -30,10 +30,16 @@ const locators = {
INPUT_SOURCE: "[data-cy=testSource1]",
},
+ source: {
+ INPUT_SOURCE: "[data-cy=testClaimReviewsource]",
+ BTN_SUBMIT_SOURCE: "[data-cy=testSaveButton]",
+ },
+
floatButton: {
FLOAT_BUTTON: "[data-cy=testFloatButton]",
ADD_CLAIM: "[data-cy=testFloatButtonAddClaim]",
ADD_PERSONALITY: "[data-cy=testFloatButtonAddPersonality]",
+ ADD_SOURCE: "[data-cy=testFloatButtonAddSources]",
},
claimReview: {
diff --git a/lib/editor-parser.ts b/lib/editor-parser.ts
index d4ef429e4..99650ae9d 100644
--- a/lib/editor-parser.ts
+++ b/lib/editor-parser.ts
@@ -1,19 +1,39 @@
import { ObjectMark, RemirrorJSON } from "remirror";
-import { ReviewTaskMachineContextReviewData } from "../server/claim-review-task/dto/create-claim-review-task.dto";
-import { ReportModelEnum } from "../server/types/enums";
+import { ReviewTaskMachineContextReviewData } from "../server/review-task/dto/create-review-task.dto";
+import { ReportModelEnum, ReviewTaskTypeEnum } from "../server/types/enums";
-type SchemaType = {
- summary: string;
+type ClaimReviewSchemaType = {
+ summary?: string;
verification?: string;
report?: string;
- sources: any[];
- questions?: any[];
+ sources?: any[];
+ questions?: string[];
};
-const getEditorSchemaArray = (reportModel = ReportModelEnum.FactChecking) =>
- reportModel === ReportModelEnum.FactChecking
- ? ["summary", "report", "verification", "questions", "paragraph"]
- : ["summary", "paragraph"];
+type ReviewSchemaType = ClaimReviewSchemaType;
+
+const getEditorSchemaArray = (reportModel = ReportModelEnum.FactChecking) => {
+ if (!reportModel) {
+ return [];
+ }
+
+ if (!Object.values(ReportModelEnum).includes(reportModel)) {
+ return [];
+ }
+
+ const editorFields = {
+ [ReportModelEnum.FactChecking]: [
+ "summary",
+ "report",
+ "verification",
+ "questions",
+ "paragraph",
+ ],
+ [ReportModelEnum.InformativeNews]: ["summary", "paragraph"],
+ };
+
+ return editorFields[reportModel];
+};
const MarkupCleanerRegex = /{{[^|]+\|([^}]+)}}/;
@@ -24,7 +44,7 @@ const createParagraphBlock = (
content: [{ type: "paragraph" }],
});
-const getDefaultDoc = (reportModel: string): RemirrorJSON => {
+const getDefaultDoc = (reviewTaskType, reportModel: string): RemirrorJSON => {
const baseContent = [
createParagraphBlock("summary"),
createParagraphBlock("questions"),
@@ -32,7 +52,10 @@ const getDefaultDoc = (reportModel: string): RemirrorJSON => {
createParagraphBlock("verification"),
];
- if (reportModel === ReportModelEnum.InformativeNews) {
+ if (
+ reportModel === ReportModelEnum.InformativeNews ||
+ reviewTaskType === ReviewTaskTypeEnum.Source
+ ) {
return {
type: "doc",
content: [createParagraphBlock("summary")],
@@ -47,7 +70,7 @@ const getDefaultDoc = (reportModel: string): RemirrorJSON => {
export class EditorParser {
hasSources(sources): boolean {
- return sources.length > 0;
+ return sources?.length > 0;
}
getSourceByProperty(sources, property) {
@@ -164,13 +187,25 @@ export class EditorParser {
return newSchema;
}
- editor2schema(data: RemirrorJSON): ReviewTaskMachineContextReviewData {
- const schema: SchemaType = {
- summary: "",
- sources: [],
- };
+ editor2schema({
+ content,
+ attrs = { reviewTaskType: ReviewTaskTypeEnum.Claim },
+ }: RemirrorJSON): ReviewTaskMachineContextReviewData & {
+ summary?: string;
+ source?: string;
+ } {
+ let schema: Partial;
+ switch (attrs.reviewTaskType) {
+ case ReviewTaskTypeEnum.Claim:
+ schema = { summary: "", sources: [] };
+ break;
+ default:
+ schema = {};
+ break;
+ }
+
const questions = [];
- for (const cardContent of data?.content) {
+ for (const cardContent of content) {
if (getEditorSchemaArray().includes(cardContent?.type)) {
if (cardContent?.type === "questions") {
for (const { content } of cardContent.content) {
@@ -203,10 +238,13 @@ export class EditorParser {
* Needed to do this conditional because the form validation when the reportModel
* is equal to Informative news requires the questions field.
*/
- if (schema.report || schema.verification) {
+ if ("report" in schema || "verification" in schema) {
schema.questions = questions;
}
- schema.sources = this.replaceSourceContentToTextRange(schema);
+
+ if ("sources" in schema) {
+ schema.sources = this.replaceSourceContentToTextRange(schema);
+ }
return schema;
}
@@ -293,10 +331,11 @@ export class EditorParser {
async schema2editor(
schema: ReviewTaskMachineContextReviewData,
- reportModel = ReportModelEnum.FactChecking
+ reportModel = ReportModelEnum.FactChecking,
+ reviewTaskType: string = ReviewTaskTypeEnum.Claim
): Promise {
if (!schema) {
- return getDefaultDoc(reportModel);
+ return getDefaultDoc(reviewTaskType, reportModel);
}
const doc: RemirrorJSON = {
diff --git a/migrations/20240610193252-add-claimreviewid-on-sentences.ts b/migrations/20240610193252-add-claimreviewid-on-sentences.ts
new file mode 100644
index 000000000..0d00fdc1a
--- /dev/null
+++ b/migrations/20240610193252-add-claimreviewid-on-sentences.ts
@@ -0,0 +1,66 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("sentences");
+
+ const pipeline = [
+ {
+ $lookup: {
+ from: "paragraphs",
+ localField: "_id",
+ foreignField: "content",
+ as: "paragraphs",
+ },
+ },
+ {
+ $unwind: "$paragraphs",
+ },
+ {
+ $lookup: {
+ from: "speeches",
+ localField: "paragraphs._id",
+ foreignField: "content",
+ as: "speeches",
+ },
+ },
+ {
+ $unwind: "$speeches",
+ },
+ {
+ $lookup: {
+ from: "claimrevisions",
+ localField: "speeches._id",
+ foreignField: "contentId",
+ as: "claimRevisions",
+ },
+ },
+ {
+ $unwind: "$claimRevisions",
+ },
+ {
+ $addFields: {
+ claimRevisionId: "$claimRevisions._id",
+ },
+ },
+ ];
+
+ const sentencesWithClaimRevision = await collection
+ .aggregate(pipeline)
+ .toArray();
+
+ for (const sentence of sentencesWithClaimRevision) {
+ await collection.updateOne(
+ { _id: sentence._id },
+ { $set: { claimRevisionId: sentence.claimRevisionId } }
+ );
+ }
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("sentences");
+
+ await collection.updateMany(
+ { claimRevisionId: { $exists: true } },
+ { $unset: { claimRevisionId: "" } }
+ );
+}
diff --git a/migrations/20240610195130-add-claimreviewid-on-debates-sentences.ts b/migrations/20240610195130-add-claimreviewid-on-debates-sentences.ts
new file mode 100644
index 000000000..ab2ff2ed7
--- /dev/null
+++ b/migrations/20240610195130-add-claimreviewid-on-debates-sentences.ts
@@ -0,0 +1,74 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("sentences");
+
+ const pipeline = [
+ {
+ $lookup: {
+ from: "paragraphs",
+ localField: "_id",
+ foreignField: "content",
+ as: "paragraphs",
+ },
+ },
+ {
+ $unwind: "$paragraphs",
+ },
+ {
+ $lookup: {
+ from: "speeches",
+ localField: "paragraphs._id",
+ foreignField: "content",
+ as: "speeches",
+ },
+ },
+ {
+ $unwind: "$speeches",
+ },
+ {
+ $lookup: {
+ from: "debates",
+ localField: "speeches._id",
+ foreignField: "content",
+ as: "debate",
+ },
+ },
+ {
+ $lookup: {
+ from: "claimrevisions",
+ localField: "debate._id",
+ foreignField: "contentId",
+ as: "claimRevisions",
+ },
+ },
+ {
+ $unwind: "$claimRevisions",
+ },
+ {
+ $addFields: {
+ claimRevisionId: "$claimRevisions._id",
+ },
+ },
+ ];
+
+ const sentencesWithClaimRevision = await collection
+ .aggregate(pipeline)
+ .toArray();
+
+ for (const sentence of sentencesWithClaimRevision) {
+ await collection.updateOne(
+ { _id: sentence._id },
+ { $set: { claimRevisionId: sentence.claimRevisionId } }
+ );
+ }
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("sentences");
+
+ await collection.updateMany(
+ { claimRevisionId: { $exists: true } },
+ { $unset: { claimRevisionId: "" } }
+ );
+}
diff --git a/migrations/20240610195623-add-claimreviewid-on-unattributed-sentences.ts b/migrations/20240610195623-add-claimreviewid-on-unattributed-sentences.ts
new file mode 100644
index 000000000..8fb41186a
--- /dev/null
+++ b/migrations/20240610195623-add-claimreviewid-on-unattributed-sentences.ts
@@ -0,0 +1,63 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("sentences");
+
+ const pipeline = [
+ {
+ $lookup: {
+ from: "paragraphs",
+ localField: "_id",
+ foreignField: "content",
+ as: "paragraphs",
+ },
+ },
+ {
+ $unwind: "$paragraphs",
+ },
+ {
+ $lookup: {
+ from: "unattributeds",
+ localField: "paragraphs._id",
+ foreignField: "content",
+ as: "unattributed",
+ },
+ },
+ {
+ $lookup: {
+ from: "claimrevisions",
+ localField: "unattributed._id",
+ foreignField: "contentId",
+ as: "claimRevisions",
+ },
+ },
+ {
+ $unwind: "$claimRevisions",
+ },
+ {
+ $addFields: {
+ claimRevisionId: "$claimRevisions._id",
+ },
+ },
+ ];
+
+ const sentencesWithClaimRevision = await collection
+ .aggregate(pipeline)
+ .toArray();
+
+ for (const sentence of sentencesWithClaimRevision) {
+ await collection.updateOne(
+ { _id: sentence._id },
+ { $set: { claimRevisionId: sentence.claimRevisionId } }
+ );
+ }
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("sentences");
+
+ await collection.updateMany(
+ { claimRevisionId: { $exists: true } },
+ { $unset: { claimRevisionId: "" } }
+ );
+}
diff --git a/migrations/20240610202706-add-claimreviewid-on-debates.ts b/migrations/20240610202706-add-claimreviewid-on-debates.ts
new file mode 100644
index 000000000..506b67394
--- /dev/null
+++ b/migrations/20240610202706-add-claimreviewid-on-debates.ts
@@ -0,0 +1,44 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("debates");
+
+ const pipeline = [
+ {
+ $lookup: {
+ from: "claimrevisions",
+ localField: "_id",
+ foreignField: "contentId",
+ as: "claimRevisions",
+ },
+ },
+ {
+ $unwind: "$claimRevisions",
+ },
+ {
+ $addFields: {
+ claimRevisionId: "$claimRevisions._id",
+ },
+ },
+ ];
+
+ const debatesWithClaimRevision = await collection
+ .aggregate(pipeline)
+ .toArray();
+
+ for (const debates of debatesWithClaimRevision) {
+ await collection.updateOne(
+ { _id: debates._id },
+ { $set: { claimRevisionId: debates.claimRevisionId } }
+ );
+ }
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("debates");
+
+ await collection.updateMany(
+ { claimRevisionId: { $exists: true } },
+ { $unset: { claimRevisionId: "" } }
+ );
+}
diff --git a/migrations/20240610204108-add-claimreviewid-on-images.ts b/migrations/20240610204108-add-claimreviewid-on-images.ts
new file mode 100644
index 000000000..3793d335e
--- /dev/null
+++ b/migrations/20240610204108-add-claimreviewid-on-images.ts
@@ -0,0 +1,44 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("images");
+
+ const pipeline = [
+ {
+ $lookup: {
+ from: "claimrevisions",
+ localField: "_id",
+ foreignField: "contentId",
+ as: "claimRevisions",
+ },
+ },
+ {
+ $unwind: "$claimRevisions",
+ },
+ {
+ $addFields: {
+ claimRevisionId: "$claimRevisions._id",
+ },
+ },
+ ];
+
+ const imagesWithClaimRevision = await collection
+ .aggregate(pipeline)
+ .toArray();
+
+ for (const images of imagesWithClaimRevision) {
+ await collection.updateOne(
+ { _id: images._id },
+ { $set: { claimRevisionId: images.claimRevisionId } }
+ );
+ }
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("images");
+
+ await collection.updateMany(
+ { claimRevisionId: { $exists: true } },
+ { $unset: { claimRevisionId: "" } }
+ );
+}
diff --git a/migrations/20240611185358-add-claimreviewid-on-paragraphs.ts b/migrations/20240611185358-add-claimreviewid-on-paragraphs.ts
new file mode 100644
index 000000000..65cca447a
--- /dev/null
+++ b/migrations/20240611185358-add-claimreviewid-on-paragraphs.ts
@@ -0,0 +1,55 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("paragraphs");
+
+ const pipeline = [
+ {
+ $lookup: {
+ from: "speeches",
+ localField: "_id",
+ foreignField: "content",
+ as: "speeches",
+ },
+ },
+ {
+ $unwind: "$speeches",
+ },
+ {
+ $lookup: {
+ from: "claimrevisions",
+ localField: "speeches._id",
+ foreignField: "contentId",
+ as: "claimRevisions",
+ },
+ },
+ {
+ $unwind: "$claimRevisions",
+ },
+ {
+ $addFields: {
+ claimRevisionId: "$claimRevisions._id",
+ },
+ },
+ ];
+
+ const paragraphsWithClaimRevision = await collection
+ .aggregate(pipeline)
+ .toArray();
+
+ for (const paragraph of paragraphsWithClaimRevision) {
+ await collection.updateOne(
+ { _id: paragraph._id },
+ { $set: { claimRevisionId: paragraph.claimRevisionId } }
+ );
+ }
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("paragraphs");
+
+ await collection.updateMany(
+ { claimRevisionId: { $exists: true } },
+ { $unset: { claimRevisionId: "" } }
+ );
+}
diff --git a/migrations/20240611185513-add-claimreviewid-on-unattributeds-paragraphs.ts b/migrations/20240611185513-add-claimreviewid-on-unattributeds-paragraphs.ts
new file mode 100644
index 000000000..9acd7adc4
--- /dev/null
+++ b/migrations/20240611185513-add-claimreviewid-on-unattributeds-paragraphs.ts
@@ -0,0 +1,52 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("paragraphs");
+
+ const pipeline = [
+ {
+ $lookup: {
+ from: "unattributeds",
+ localField: "_id",
+ foreignField: "content",
+ as: "unattributed",
+ },
+ },
+ {
+ $lookup: {
+ from: "claimrevisions",
+ localField: "unattributed._id",
+ foreignField: "contentId",
+ as: "claimRevisions",
+ },
+ },
+ {
+ $unwind: "$claimRevisions",
+ },
+ {
+ $addFields: {
+ claimRevisionId: "$claimRevisions._id",
+ },
+ },
+ ];
+
+ const paragraphsWithClaimRevision = await collection
+ .aggregate(pipeline)
+ .toArray();
+
+ for (const paragraph of paragraphsWithClaimRevision) {
+ await collection.updateOne(
+ { _id: paragraph._id },
+ { $set: { claimRevisionId: paragraph.claimRevisionId } }
+ );
+ }
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("paragraphs");
+
+ await collection.updateMany(
+ { claimRevisionId: { $exists: true } },
+ { $unset: { claimRevisionId: "" } }
+ );
+}
diff --git a/migrations/20240611185733-add-claimreviewid-on-debates-paragraphs.ts b/migrations/20240611185733-add-claimreviewid-on-debates-paragraphs.ts
new file mode 100644
index 000000000..17ed18ae8
--- /dev/null
+++ b/migrations/20240611185733-add-claimreviewid-on-debates-paragraphs.ts
@@ -0,0 +1,63 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("paragraphs");
+
+ const pipeline = [
+ {
+ $lookup: {
+ from: "speeches",
+ localField: "_id",
+ foreignField: "content",
+ as: "speeches",
+ },
+ },
+ {
+ $unwind: "$speeches",
+ },
+ {
+ $lookup: {
+ from: "debates",
+ localField: "speeches._id",
+ foreignField: "content",
+ as: "debate",
+ },
+ },
+ {
+ $lookup: {
+ from: "claimrevisions",
+ localField: "debate._id",
+ foreignField: "contentId",
+ as: "claimRevisions",
+ },
+ },
+ {
+ $unwind: "$claimRevisions",
+ },
+ {
+ $addFields: {
+ claimRevisionId: "$claimRevisions._id",
+ },
+ },
+ ];
+
+ const paragraphsWithClaimRevision = await collection
+ .aggregate(pipeline)
+ .toArray();
+
+ for (const paragraph of paragraphsWithClaimRevision) {
+ await collection.updateOne(
+ { _id: paragraph._id },
+ { $set: { claimRevisionId: paragraph.claimRevisionId } }
+ );
+ }
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("paragraphs");
+
+ await collection.updateMany(
+ { claimRevisionId: { $exists: true } },
+ { $unset: { claimRevisionId: "" } }
+ );
+}
diff --git a/migrations/20240611191417-add-claimreviewid-on-debates-speeches.ts b/migrations/20240611191417-add-claimreviewid-on-debates-speeches.ts
new file mode 100644
index 000000000..8cf598b11
--- /dev/null
+++ b/migrations/20240611191417-add-claimreviewid-on-debates-speeches.ts
@@ -0,0 +1,52 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("speeches");
+
+ const pipeline = [
+ {
+ $lookup: {
+ from: "debates",
+ localField: "_id",
+ foreignField: "content",
+ as: "debate",
+ },
+ },
+ {
+ $lookup: {
+ from: "claimrevisions",
+ localField: "debate._id",
+ foreignField: "contentId",
+ as: "claimRevisions",
+ },
+ },
+ {
+ $unwind: "$claimRevisions",
+ },
+ {
+ $addFields: {
+ claimRevisionId: "$claimRevisions._id",
+ },
+ },
+ ];
+
+ const speechesWithClaimRevision = await collection
+ .aggregate(pipeline)
+ .toArray();
+
+ for (const speeche of speechesWithClaimRevision) {
+ await collection.updateOne(
+ { _id: speeche._id },
+ { $set: { claimRevisionId: speeche.claimRevisionId } }
+ );
+ }
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("speeches");
+
+ await collection.updateMany(
+ { claimRevisionId: { $exists: true } },
+ { $unset: { claimRevisionId: "" } }
+ );
+}
diff --git a/migrations/20240611191431-add-claimreviewid-on-speeches.ts b/migrations/20240611191431-add-claimreviewid-on-speeches.ts
new file mode 100644
index 000000000..9aaaa9612
--- /dev/null
+++ b/migrations/20240611191431-add-claimreviewid-on-speeches.ts
@@ -0,0 +1,44 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("speeches");
+
+ const pipeline = [
+ {
+ $lookup: {
+ from: "claimrevisions",
+ localField: "_id",
+ foreignField: "contentId",
+ as: "claimRevisions",
+ },
+ },
+ {
+ $unwind: "$claimRevisions",
+ },
+ {
+ $addFields: {
+ claimRevisionId: "$claimRevisions._id",
+ },
+ },
+ ];
+
+ const speechesWithClaimRevision = await collection
+ .aggregate(pipeline)
+ .toArray();
+
+ for (const speeche of speechesWithClaimRevision) {
+ await collection.updateOne(
+ { _id: speeche._id },
+ { $set: { claimRevisionId: speeche.claimRevisionId } }
+ );
+ }
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("speeches");
+
+ await collection.updateMany(
+ { claimRevisionId: { $exists: true } },
+ { $unset: { claimRevisionId: "" } }
+ );
+}
diff --git a/migrations/20240618124843-change-review-task-history-target-model-field.ts b/migrations/20240618124843-change-review-task-history-target-model-field.ts
new file mode 100644
index 000000000..409cf0636
--- /dev/null
+++ b/migrations/20240618124843-change-review-task-history-target-model-field.ts
@@ -0,0 +1,19 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("histories");
+
+ await collection.updateMany(
+ { targetModel: "ClaimReviewTask" },
+ { $set: { targetModel: "ReviewTask" } }
+ );
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("histories");
+
+ await collection.updateMany(
+ { targetModel: "ReviewTask" },
+ { $set: { targetModel: "ClaimReviewTask" } }
+ );
+}
diff --git a/migrations/20240627115131-rename-review-task-collection.ts b/migrations/20240627115131-rename-review-task-collection.ts
new file mode 100644
index 000000000..d1ca0ef9c
--- /dev/null
+++ b/migrations/20240627115131-rename-review-task-collection.ts
@@ -0,0 +1,13 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const collection = db.collection("claimreviewtasks");
+
+ await collection.rename("reviewtasks");
+}
+
+export async function down(db: Db) {
+ const collection = db.collection("reviewtasks");
+
+ await collection.rename("claimreviewtasks");
+}
diff --git a/migrations/20240701184655-add-namespace-in-review-task-schemae.ts b/migrations/20240701184655-add-namespace-in-review-task-schemae.ts
new file mode 100644
index 000000000..6af8d29aa
--- /dev/null
+++ b/migrations/20240701184655-add-namespace-in-review-task-schemae.ts
@@ -0,0 +1,51 @@
+import { Db } from "mongodb";
+const ObjectId = require("mongodb").ObjectID;
+
+export async function up(db: Db) {
+ const reviewTasksCursor = await db.collection("reviewtasks").find();
+
+ while (await reviewTasksCursor.hasNext()) {
+ const reviewTask = await reviewTasksCursor.next();
+ if (reviewTask.machine.context.claimReview.claim) {
+ const claim = await db.collection("claims").findOne({
+ _id: ObjectId(reviewTask.machine.context.claimReview.claim),
+ });
+
+ await db
+ .collection("reviewtasks")
+ .updateOne(
+ { _id: reviewTask._id },
+ {
+ $set: {
+ nameSpace: claim?.nameSpace,
+ reviewTaskType: "Claim",
+ },
+ }
+ );
+ }
+
+ if (reviewTask.machine.context.claimReview.source) {
+ const source = await db.collection("sources").findOne({
+ _id: ObjectId(reviewTask.machine.context.claimReview.source),
+ });
+
+ await db
+ .collection("reviewtasks")
+ .updateOne(
+ { _id: reviewTask._id },
+ {
+ $set: {
+ nameSpace: source?.nameSpace,
+ reviewTaskType: "Source",
+ },
+ }
+ );
+ }
+ }
+}
+
+export async function down(db: Db) {
+ await db
+ .collection("reviewtasks")
+ .updateMany({}, { $unset: { nameSpace: "", reviewTaskType: "" } });
+}
diff --git a/migrations/20240701191347-add-target-field-in-machine-context.ts b/migrations/20240701191347-add-target-field-in-machine-context.ts
new file mode 100644
index 000000000..a479081b5
--- /dev/null
+++ b/migrations/20240701191347-add-target-field-in-machine-context.ts
@@ -0,0 +1,36 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const reviewTasksCursor = await db.collection("reviewtasks").find();
+
+ while (await reviewTasksCursor.hasNext()) {
+ const reviewTask = await reviewTasksCursor.next();
+ if (reviewTask.machine.context.claimReview.claim) {
+ await db.collection("reviewtasks").updateOne(
+ { _id: reviewTask._id },
+ {
+ $set: {
+ target: reviewTask.machine.context.claimReview.claim,
+ },
+ $unset: {
+ "machine.context.claimReview.claim": undefined,
+ },
+ }
+ );
+ }
+
+ if (reviewTask.machine.context.claimReview.source) {
+ await db.collection("reviewtasks").updateOne(
+ { _id: reviewTask._id },
+ {
+ $set: {
+ target: reviewTask.machine.context.claimReview.source,
+ },
+ $unset: {
+ "machine.context.claimReview.source": undefined,
+ },
+ }
+ );
+ }
+ }
+}
diff --git a/migrations/20240701221810-rename-claim-review-field-in-review-task-schema.ts b/migrations/20240701221810-rename-claim-review-field-in-review-task-schema.ts
new file mode 100644
index 000000000..1d0fa8c77
--- /dev/null
+++ b/migrations/20240701221810-rename-claim-review-field-in-review-task-schema.ts
@@ -0,0 +1,39 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const reviewTasksCursor = await db.collection("reviewtasks").find();
+
+ while (await reviewTasksCursor.hasNext()) {
+ const reviewTask = await reviewTasksCursor.next();
+
+ await db
+ .collection("reviewtasks")
+ .updateOne(
+ { _id: reviewTask._id },
+ {
+ $rename: {
+ "machine.context.claimReview": "machine.context.review",
+ },
+ }
+ );
+ }
+}
+
+export async function down(db: Db) {
+ const reviewTasksCursor = await db.collection("reviewtasks").find();
+
+ while (await reviewTasksCursor.hasNext()) {
+ const reviewTask = await reviewTasksCursor.next();
+
+ await db
+ .collection("reviewtasks")
+ .updateOne(
+ { _id: reviewTask._id },
+ {
+ $rename: {
+ "machine.context.review": "machine.context.claimReview",
+ },
+ }
+ );
+ }
+}
diff --git a/migrations/20240717100604-add-target-and-target-model-fields-in-claim-review-schema.ts b/migrations/20240717100604-add-target-and-target-model-fields-in-claim-review-schema.ts
new file mode 100644
index 000000000..8962fb242
--- /dev/null
+++ b/migrations/20240717100604-add-target-and-target-model-fields-in-claim-review-schema.ts
@@ -0,0 +1,38 @@
+import { Db } from "mongodb";
+
+export async function up(db: Db) {
+ const claimReviewCursor = await db.collection("claimreviews").find();
+
+ while (await claimReviewCursor.hasNext()) {
+ const claimReview = await claimReviewCursor.next();
+ if (claimReview.claim) {
+ await db.collection("claimreviews").updateOne(
+ { _id: claimReview._id },
+ {
+ $set: {
+ target: claimReview.claim,
+ targetModel: "Claim",
+ },
+ $unset: {
+ claim: undefined,
+ },
+ }
+ );
+ }
+
+ if (claimReview.source) {
+ await db.collection("claimreviews").updateOne(
+ { _id: claimReview._id },
+ {
+ $set: {
+ target: claimReview.source,
+ targetModel: "Source",
+ },
+ $unset: {
+ source: undefined,
+ },
+ }
+ );
+ }
+ }
+}
diff --git a/package.json b/package.json
index d4960dd62..7568a751d 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,8 @@
"ory-kratos:cy": "docker compose up -d --build --force-recreate kratos kratos-migrate",
"test:e2e:cy": "env-cmd --silent concurrently -p \"[{name}]\" -n \"MongoDB,Ory,Server\" \"yarn test:e2e:mongo-server\" \"yarn ory-kratos:cy\" \"wait-on tcp:127.0.0.1:35025 tcp:127.0.0.1:4433 tcp:127.0.0.1:4434 && yarn test:e2e:app-server\"",
"test:e2e:mongo-server": "node dist/server/mongodb.server.js",
- "test:e2e:app-server": "yarn seed:ci && yarn start -c config.test.ci.yaml"
+ "test:e2e:app-server": "yarn seed:ci && yarn start -c config.test.ci.yaml",
+ "create-novu-subscribers": "env-cmd ts-node scripts/createNovuSubscribers.ts"
},
"nodemonConfig": {
"ignore": [
@@ -107,7 +108,7 @@
"ai": "^3.1.1",
"antd": "^4.18.5",
"aws-sdk": "^2.1154.0",
- "axios": "^1.5.0",
+ "axios": "^1.6.0",
"babel-jest": "^29.7.0",
"babel-plugin-styled-components": "^1.13.2",
"class-transformer": "^0.5.1",
@@ -259,7 +260,11 @@
"ip": "1.1.9",
"tar": "6.2.1",
"@types/react": "^17.0.38",
- "@types/react-dom": "17.0.2"
+ "@types/react-dom": "17.0.2",
+ "ejs": "3.1.10",
+ "@grpc/grpc-js": "1.10.9",
+ "braces": "3.0.3",
+ "ws": "8.17.1"
},
"packageManager": "yarn@3.6.3"
}
diff --git a/public/locales/en/claimReview.json b/public/locales/en/claimReview.json
index 61bec75c8..edd9167cc 100644
--- a/public/locales/en/claimReview.json
+++ b/public/locales/en/claimReview.json
@@ -1,7 +1,8 @@
{
"cardAuthor": "{{name}} reviewed this as:",
"anonymousUserName": "Anonymous",
- "claimReview": "Claim reviewed as ",
+ "titleClaimReview": "Claim reviewed as: ",
+ "titleSourceReview": "Source reviewed as: ",
"listTitle": "Reviews",
"summarySectionTitle": "Summary of conclusion",
"questionsSectionTitle": "What questions should the verification answer?",
diff --git a/public/locales/en/claimReviewForm.json b/public/locales/en/claimReviewForm.json
index 4ce954288..7c3f72aa2 100644
--- a/public/locales/en/claimReviewForm.json
+++ b/public/locales/en/claimReviewForm.json
@@ -2,8 +2,10 @@
"titleEmpty": "Choose a sentence to review",
"title": "Review the sentence",
"classificationLabel": "Review this claim",
- "addReviewButton": "Start a fact-check",
+ "addFactCheckingReviewButton": "Start a fact-check",
"addInformativeNewsButton": "Start an informative news",
+ "addSourceReviewButton": "Start a source review",
+ "addVerificationRequestButton": "Start a verification request review",
"cancelButton": "Cancel",
"classificationPlaceholder": "Choose a classification",
"not-fact": "Not fact",
@@ -29,12 +31,13 @@
"addQuestionLabel": "Add another question",
"reportLabel": "Verification report",
"reportPlaceholder": "Insert report content here",
+ "sourceEditorLabel": "Create source review",
"verificationLabel": "How it was verified",
"verificationPlaceholder": "Describe how the verification was done",
- "collaborativeEditorLabel": "Report",
- "collaborativeEditorPlaceholder": "Insert report fields",
- "sourcesLabel": "Provide at least one link to a reliable source",
- "sourcesPlaceholder": "Paste URL",
+ "visualEditorLabel": "Report",
+ "visualEditorPlaceholder": "Insert report fields",
+ "sourceLabel": "Provide one link to a reliable source",
+ "sourcePlaceholder": "Paste URL",
"addSourceLabel": "Add another source",
"reviewerLabel": "Select a reviewer for review",
"reviewerPlaceholder": "Select a user",
@@ -47,5 +50,9 @@
"rejectionCommentLabel": "Rejection comment",
"rejectionCommentPlaceholder": "Describe what needs to be changed",
"loginButton": "Login to continue",
- "notReviewed": "Not reviewed"
+ "notReviewed": "Not reviewed",
+ "groupLabel": "Group verification requests",
+ "groupPlaceholder": "Select verification requests",
+ "isSensitiveLabel": "Sensitve content",
+ "isSensitive": "This verification request contains sensitve content"
}
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 3dfd87af0..697e7df90 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -8,6 +8,7 @@
"questionsRequiredFieldError": "Questions field is required",
"verificationRequiredFieldError": "Verification field is required",
"sourcesRequiredFieldError": "Source is required",
+ "sourceRequiredFieldError": "Source is required",
"supportEmail": "support@aletheifact.org",
"contactEmail": "contact@aletheiafact.org",
"captchaError": "There was an error validating the captcha",
diff --git a/public/locales/en/kanban.json b/public/locales/en/kanban.json
index 55e12c968..95e41daed 100644
--- a/public/locales/en/kanban.json
+++ b/public/locales/en/kanban.json
@@ -1,7 +1,10 @@
{
"claimBy": "Claim by",
"assignedTo": "Fact-checkers",
- "myTasks": "My tasks only",
- "myCrossChecks": "My cross-checks only",
- "myReviews": "My reviews only"
+ "myAssigned": "My tasks only",
+ "myCrossChecked": "My cross-checks only",
+ "myReviewed": "My reviews only",
+ "tabClaimTitle": "Claims",
+ "tabSourceTitle": "Sources",
+ "tabVerificationRequestTitle": "Verification Requests"
}
diff --git a/public/locales/en/menu.json b/public/locales/en/menu.json
index 6ca03ac9b..b3c74c738 100644
--- a/public/locales/en/menu.json
+++ b/public/locales/en/menu.json
@@ -12,5 +12,6 @@
"kanbanItem": "Kanban",
"nameSpaceItem": "Namespace",
"supportiveMaterials": "Supportive Materials",
- "sourcesItem": "Sources"
+ "sourcesItem": "Sources",
+ "verificationRequestItem": "Verification Requests"
}
diff --git a/public/locales/en/claimReviewTask.json b/public/locales/en/reviewTask.json
similarity index 85%
rename from public/locales/en/claimReviewTask.json
rename to public/locales/en/reviewTask.json
index 8bb5670a0..67eb77a7c 100644
--- a/public/locales/en/claimReviewTask.json
+++ b/public/locales/en/reviewTask.json
@@ -1,6 +1,8 @@
{
"ASSIGN_USER_SUCCESS": "User assigned successfully",
"ASSIGN_USER_ERROR": "Error when assigning the user",
+ "ASSIGN_REQUEST_SUCCESS": "User assigned successfully",
+ "ASSIGN_REQUEST_ERROR": "Error when assigning the user",
"FINISH_REPORT_SUCCESS": "Report saved succesfully",
"FINISH_REPORT_ERROR": "Error when saving the report",
"ADD_COMMENT_SUCCESS": "Successfully advanced step",
@@ -25,9 +27,14 @@
"FULL_REVIEW_ERROR": "Error when saving the summary",
"GO_BACK_SUCCESS": "Successfully turned step",
"GO_BACK_ERROR": "It was not possible to return",
+ "REJECT_SUCCESS": "Successfully rejected verification request",
+ "REJECT_ERROR": "It was not possible to reject",
+ "RESET_SUCCESS": "Successfully reset workflow",
+ "RESET_ERROR": "It was not possible to reset",
"ADD_REJECTION_COMMENT": "Send comment",
"ASSIGN_USER": "Assign",
+ "ASSIGN_REQUEST": "Assign",
"FINISH_REPORT": "Finish Report",
"SELECTED_CROSS_CHECKING": "Select cross-checker",
"SELECTED_REVIEW": "Select reviewer",
@@ -42,6 +49,8 @@
"FULL_REVIEW": "Complete report",
"GO_BACK": "Go Back",
"SAVE_DRAFT": "Save Draft",
+ "REJECT_REQUEST": "Reject",
+ "RESET": "Reset",
"errorWhileFetching": "Error while fetching machine",
"assigned": "User assigned",
@@ -50,6 +59,8 @@
"submitted": "In review",
"published": "Published",
"draft": "Draft",
+ "assignedRequest": "User assigned",
+ "rejectedRequest": "Rejected",
"crossCheckingAlertTitle": "This report is beeing cross-checked",
"reviewingAlertTitle": "This report is beeing reviewed",
"rejectionAlertTitle": "This review was rejected with the following comment",
diff --git a/public/locales/en/seo.json b/public/locales/en/seo.json
index 17793fe66..319b16c3d 100644
--- a/public/locales/en/seo.json
+++ b/public/locales/en/seo.json
@@ -13,5 +13,7 @@
"claimListTitle": "Claims",
"claimListDescription": "See claims on AletheiaFact.org",
"createSourceTitle": "Create source",
- "createSourceDescription": "Create sources to AletheiaFact.org"
+ "createSourceDescription": "Create sources to AletheiaFact.org",
+ "verificationRequestTitle": "Verification Requests",
+ "verificationRequestDescription": "See verification requests on AletheiaFact.org"
}
diff --git a/public/locales/en/sourceForm.json b/public/locales/en/sourceForm.json
index 82df6ee45..06f85094e 100644
--- a/public/locales/en/sourceForm.json
+++ b/public/locales/en/sourceForm.json
@@ -14,5 +14,6 @@
"summaryLabel": "Summary",
"summaryPlaceholder": "Insert the source summary",
"classificationLabel": "Review this source",
- "classificationPlaceholder": "Choose a classification"
+ "classificationPlaceholder": "Choose a classification",
+ "summarizeSouce": "Summarize source automatic"
}
diff --git a/public/locales/en/verificationRequest.json b/public/locales/en/verificationRequest.json
new file mode 100644
index 000000000..5bce0292a
--- /dev/null
+++ b/public/locales/en/verificationRequest.json
@@ -0,0 +1,10 @@
+{
+ "verificationRequestListHeader": "Verification Requests",
+ "verificationRequestTitle": "Verification Request",
+ "manageVerificationRequests": "Manage selected verification requests",
+ "createClaimFromVerificationRequest": "No claim was created with this verification request",
+ "openVerificationRequestClaimLabel": "A claim created was related to this verification request",
+ "openVerificationRequestClaimButton": "Open claim",
+ "agroupVerificationRequest": "Related verification requests",
+ "openVerificationRequest": "Open"
+}
\ No newline at end of file
diff --git a/public/locales/pt/claimReview.json b/public/locales/pt/claimReview.json
index ca798771f..1870d3cbe 100644
--- a/public/locales/pt/claimReview.json
+++ b/public/locales/pt/claimReview.json
@@ -1,7 +1,8 @@
{
"cardAuthor": "{{name}} avaliou essa afirmação como:",
"anonymousUserName": "Anônimo",
- "claimReview": "Afirmação avaliada como ",
+ "titleClaimReview": "Afirmação avaliada como: ",
+ "titleSourceReview": "Fonte avaliada como: ",
"listTitle": "Avaliações",
"summarySectionTitle": "Resumo da conclusão",
"questionsSectionTitle": "Quais perguntas a verificação deverá responder?",
diff --git a/public/locales/pt/claimReviewForm.json b/public/locales/pt/claimReviewForm.json
index cb2e7101f..9f992f60d 100644
--- a/public/locales/pt/claimReviewForm.json
+++ b/public/locales/pt/claimReviewForm.json
@@ -2,8 +2,10 @@
"titleEmpty": "Escolha uma frase para revisar",
"title": "Classifique a frase",
"classificationLabel": "Revise essa frase",
- "addReviewButton": "Comece uma checagem de fatos",
+ "addFactCheckingReviewButton": "Comece uma checagem de fatos",
"addInformativeNewsButton": "Comece uma noticia informativa",
+ "addSourceReviewButton": "Comece uma checagem de fonte",
+ "addVerificationRequestButton": "Comece uma revisão de denúncia",
"cancelButton": "Cancelar",
"classificationPlaceholder": "Selecione uma classificação",
"not-fact": "Não é fato",
@@ -31,10 +33,11 @@
"reportPlaceholder": "Insira o relatório da verificação",
"verificationLabel": "Como verificamos?",
"verificationPlaceholder": "Insira como a verificação foi feita",
- "collaborativeEditorLabel": "Relatório",
- "collaborativeEditorPlaceholder": "Insira campos do relatório",
- "sourcesLabel": "Forneça ao menos um link de uma fonte confiável",
- "sourcesPlaceholder": "Cole uma URL",
+ "visualEditorLabel": "Relatório",
+ "visualEditorPlaceholder": "Insira campos do relatório",
+ "sourceLabel": "Forneça um link de uma fonte confiável",
+ "sourceEditorLabel": "Criar revisão da fonte",
+ "sourcePlaceholder": "Cole uma URL",
"addSourceLabel": "Incluir outra fonte",
"reviewerLabel": "Selecione um revisor para a revisão",
"reviewerPlaceholder": "Selecione um usuário",
@@ -47,5 +50,9 @@
"rejectionCommentLabel": "Comentário de rejeição",
"rejectionCommentPlaceholder": "Descreva o que precisa ser alterado",
"loginButton": "Faça login para continuar",
- "notReviewed": "Não revisado"
+ "notReviewed": "Não revisado",
+ "groupLabel": "Agrupar denúncias",
+ "groupPlaceholder": "Selecione denúncias",
+ "isSensitiveLabel": "Conteúdo confidencial",
+ "isSensitive": "Esta denúncia contém conteúdo confidencial"
}
diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json
index 7bc8f6b54..8cb1a8881 100644
--- a/public/locales/pt/common.json
+++ b/public/locales/pt/common.json
@@ -8,6 +8,7 @@
"questionsRequiredFieldError": "Campo de perguntas obrigatório",
"verificationRequiredFieldError": "Campo de verificação obrigatório",
"sourcesRequiredFieldError": "Fonte obrigatória",
+ "sourceRequiredFieldError": "Fonte obrigatória",
"supportEmail": "support@aletheifact.org",
"contactEmail": "contato@aletheiafact.org",
"captchaError": "Erro na validação do captcha",
diff --git a/public/locales/pt/kanban.json b/public/locales/pt/kanban.json
index cd32b90fd..e4cbe3585 100644
--- a/public/locales/pt/kanban.json
+++ b/public/locales/pt/kanban.json
@@ -1,7 +1,10 @@
{
"claimBy": "Afirmação de",
"assignedTo": "Checadores",
- "myTasks": "Apenas minhas tarefas",
- "myCrossChecks": "Apenas meus cross-checkings",
- "myReviews": "Minhas revisões"
+ "myAssigned": "Apenas minhas tarefas",
+ "myCrossChecked": "Apenas meus cross-checkings",
+ "myReviewed": "Minhas revisões",
+ "tabClaimTitle": "Afirmações",
+ "tabSourceTitle": "Fontes",
+ "tabVerificationRequestTitle": "Denúncias"
}
diff --git a/public/locales/pt/menu.json b/public/locales/pt/menu.json
index 9956f2a13..aa80d2e31 100644
--- a/public/locales/pt/menu.json
+++ b/public/locales/pt/menu.json
@@ -12,5 +12,6 @@
"kanbanItem": "Meu Trabalho",
"nameSpaceItem": "Namespace",
"supportiveMaterials": "Materiais de Apoio",
- "sourcesItem": "Fontes"
+ "sourcesItem": "Fontes",
+ "verificationRequestItem": "Denúncias"
}
diff --git a/public/locales/pt/claimReviewTask.json b/public/locales/pt/reviewTask.json
similarity index 85%
rename from public/locales/pt/claimReviewTask.json
rename to public/locales/pt/reviewTask.json
index 39e8535b8..948b03800 100644
--- a/public/locales/pt/claimReviewTask.json
+++ b/public/locales/pt/reviewTask.json
@@ -1,6 +1,8 @@
{
"ASSIGN_USER_SUCCESS": "Usuário atribuÃdo com sucesso",
"ASSIGN_USER_ERROR": "Erro ao atribuir usuário",
+ "ASSIGN_REQUEST_SUCCESS": "Usuário atribuÃdo com sucesso",
+ "ASSIGN_REQUEST_ERROR": "Erro ao atribuir usuário",
"FINISH_REPORT_SUCCESS": "Relatório salvo com sucesso",
"FINISH_REPORT_ERROR": "Erro ao salvar o relatório",
"ADD_COMMENT_SUCCESS": "Passo avançado com sucesso",
@@ -25,9 +27,14 @@
"FULL_REVIEW_ERROR": "Erro ao salvar resumo",
"GO_BACK_SUCCESS": "Passo voltado com sucesso",
"GO_BACK_ERROR": "Não foi possÃvel voltar",
+ "REJECT_SUCCESS": "Denúncia rejeitada com sucesso",
+ "REJECT_ERROR": "Não foi possÃvel rejeitar a denúncia",
+ "RESET_SUCCESS": "Fluxo reiniciado com sucesso",
+ "RESET_ERROR": "Não foi possÃvel reiniciar o fluxo",
"ADD_REJECTION_COMMENT": "Enviar comentário",
"ASSIGN_USER": "Atribuir",
+ "ASSIGN_REQUEST": "Atribuir",
"FINISH_REPORT": "Concluir Relatório",
"SELECTED_CROSS_CHECKING": "Selecione um cross-checker",
"SELECTED_REVIEW": "Selecione um revisor",
@@ -42,6 +49,8 @@
"FULL_REVIEW": "Completar relatório",
"GO_BACK": "Voltar",
"SAVE_DRAFT": "Salvar rascunho",
+ "REJECT_REQUEST": "Rejeitar",
+ "RESET": "Resetar",
"errorWhileFetching": "Erro ao buscar a máquina",
"assigned": "Usuário atribuÃdo",
@@ -50,6 +59,8 @@
"submitted": "Em revisão",
"published": "Publicado",
"draft": "Rascunho",
+ "assignedRequest": "Usuário atribuÃdo",
+ "rejectedRequest": "Rejeitado",
"crossCheckingAlertTitle": "Este relatório está em processo de cross-checking",
"reviewingAlertTitle": "Este relatório está em processo de revisão",
"rejectionAlertTitle": "A revisão foi rejeitada com o seguinte comentário",
diff --git a/public/locales/pt/seo.json b/public/locales/pt/seo.json
index c31d76a7c..c0331071d 100644
--- a/public/locales/pt/seo.json
+++ b/public/locales/pt/seo.json
@@ -13,5 +13,7 @@
"claimListTitle": "Declarações",
"claimListDescription": "Veja declarações na AletheiaFact.org",
"createSourceTitle": "Adicione uma fonte",
- "createSourceDescription": "Adiciona uma fonte na AletheiaFact.org"
+ "createSourceDescription": "Adiciona uma fonte na AletheiaFact.org",
+ "verificationRequestTitle": "Denúncias",
+ "verificationRequestDescription": "Veja as denúncias na AletheiaFact.org"
}
diff --git a/public/locales/pt/sourceForm.json b/public/locales/pt/sourceForm.json
index 02d352624..861af562c 100644
--- a/public/locales/pt/sourceForm.json
+++ b/public/locales/pt/sourceForm.json
@@ -14,5 +14,6 @@
"summaryLabel": "Resumo",
"summaryPlaceholder": "Insira o resumo da fonte",
"classificationLabel": "Revise essa fonte",
- "classificationPlaceholder": "Selecione uma classificação"
+ "classificationPlaceholder": "Selecione uma classificação",
+ "summarizeSouce": "Resumo automático"
}
diff --git a/public/locales/pt/verificationRequest.json b/public/locales/pt/verificationRequest.json
new file mode 100644
index 000000000..76d7c0f3a
--- /dev/null
+++ b/public/locales/pt/verificationRequest.json
@@ -0,0 +1,10 @@
+{
+ "verificationRequestListHeader": "Denúncias",
+ "verificationRequestTitle": "Denúncia",
+ "manageVerificationRequests": "Gerenciar denúncias selecionadas",
+ "createClaimFromVerificationRequest": "Nenhuma afirmação foi criada com esta denúncia",
+ "openVerificationRequestClaimLabel": "Uma afirmação criada foi relacionada com essa denúncia",
+ "openVerificationRequestClaimButton": "Abrir afirmação",
+ "agroupVerificationRequest": "Denúncias relacionadas",
+ "openVerificationRequest": "Abrir"
+}
\ No newline at end of file
diff --git a/scripts/createNovuSubscribers.ts b/scripts/createNovuSubscribers.ts
new file mode 100644
index 000000000..20d628e42
--- /dev/null
+++ b/scripts/createNovuSubscribers.ts
@@ -0,0 +1,76 @@
+/**
+ * Script to create Novu subscribers and add them to a topic.
+ *
+ * Usage:
+ * Run the following command to create subscribers and add them to a specified topic:
+ * yarn create-novu-subscribers ""
+ */
+import { Novu } from "@novu/node";
+import { v4 as uuidv4 } from "uuid";
+
+if (!process.env.NOVU_API_KEY) {
+ console.error("NOVU_API_KEY is not set in the environment variables.");
+ process.exit(1);
+}
+
+const novu = new Novu(`${process.env.NOVU_API_KEY}`);
+
+const args = process.argv.slice(2);
+
+if (args.length < 2) {
+ console.error(
+ "Usage: yarn create-novu-subscribers "
+ );
+ process.exit(1);
+}
+
+const topicKey = args[0];
+const emails = args[1].split(",");
+
+const subscriberIds: string[] = [];
+
+const createSubscribers = async (emails: string[]) => {
+ for (const email of emails) {
+ const [firstName, domain] = email.split("@");
+ const subscriberId = uuidv4();
+ subscriberIds.push(subscriberId);
+
+ try {
+ await novu.subscribers.identify(subscriberId, {
+ firstName,
+ email,
+ });
+ console.log(
+ `Subscriber created: ${firstName} (${email}) with ID: ${subscriberId}`
+ );
+ } catch (error) {
+ console.error(`Failed to create subscriber for ${email}:`, error);
+ }
+ }
+};
+
+const addSubscribersToTopic = async (
+ topicKey: string,
+ subscribers: string[]
+) => {
+ try {
+ const result = await novu.topics.addSubscribers(topicKey, {
+ subscribers,
+ });
+ console.log(`Subscribers added to topic ${topicKey}:`, result);
+ } catch (error) {
+ console.error(`Failed to add subscribers to topic ${topicKey}:`, error);
+ }
+};
+
+const main = async () => {
+ try {
+ await createSubscribers(emails);
+
+ await addSubscribersToTopic(topicKey, subscriberIds);
+ } catch (error) {
+ console.error("Error:", error);
+ }
+};
+
+main();
diff --git a/server/app.module.ts b/server/app.module.ts
index e278f989e..a3de297bd 100644
--- a/server/app.module.ts
+++ b/server/app.module.ts
@@ -23,7 +23,7 @@ import { ThrottlerModule, ThrottlerGuard } from "@nestjs/throttler";
import { SitemapModule } from "./sitemap/sitemap.module";
import { ClaimRevisionModule } from "./claim/claim-revision/claim-revision.module";
import { HistoryModule } from "./history/history.module";
-import { ClaimReviewTaskModule } from "./claim-review-task/claim-review-task.module";
+import { ReviewTaskModule } from "./review-task/review-task.module";
import { LoggerMiddleware } from "./middleware/logger.middleware";
import { ReportModule } from "./report/report.module";
import OryModule from "./auth/ory/ory.module";
@@ -48,13 +48,18 @@ import { EditorModule } from "./editor/editor.module";
import { BadgeModule } from "./badge/badge.module";
import { EditorParseModule } from "./editor-parse/editor-parse.module";
import { NotificationModule } from "./notifications/notifications.module";
-import { CommentModule } from "./claim-review-task/comment/comment.module";
+import { CommentModule } from "./review-task/comment/comment.module";
import { NameSpaceModule } from "./auth/name-space/name-space.module";
import { NameSpaceGuard } from "./auth/name-space/name-space.guard";
import { AutomatedFactCheckingModule } from "./automated-fact-checking/automated-fact-checking.module";
import { CopilotChatModule } from "./copilot/copilot-chat.module";
import { UnattributedModule } from "./claim/types/unattributed/unattributed.module";
import { DailyReportModule } from "./daily-report/daily-report.module";
+import { SummarizationCrawlerModule } from "./summarization/summarization-crawler.module";
+import { ChatbotModule } from "./chat-bot/chat-bot.module";
+import { VerificationRequestModule } from "./verification-request/verification-request.module";
+import { FeatureFlagModule } from "./feature-flag/feature-flag.module";
+import { GroupModule } from "./group/group.module";
@Module({})
export class AppModule implements NestModule {
@@ -84,7 +89,7 @@ export class AppModule implements NestModule {
PersonalityModule,
ClaimModule,
ClaimReviewModule,
- ClaimReviewTaskModule,
+ ReviewTaskModule,
ClaimRevisionModule,
HistoryModule,
StateEventModule,
@@ -115,6 +120,11 @@ export class AppModule implements NestModule {
CopilotChatModule,
UnattributedModule,
DailyReportModule,
+ SummarizationCrawlerModule,
+ ChatbotModule,
+ VerificationRequestModule,
+ FeatureFlagModule,
+ GroupModule,
];
if (options.feature_flag) {
imports.push(
diff --git a/server/auth/name-space/dto/create-namespace.dto.ts b/server/auth/name-space/dto/create-namespace.dto.ts
index 89b3ab393..f48f88712 100644
--- a/server/auth/name-space/dto/create-namespace.dto.ts
+++ b/server/auth/name-space/dto/create-namespace.dto.ts
@@ -12,4 +12,9 @@ export class CreateNameSpaceDTO {
@IsOptional()
@ApiProperty()
users: User[];
+
+ @IsString()
+ @IsOptional()
+ @ApiProperty()
+ slug: string;
}
diff --git a/server/auth/name-space/name-space.controller.ts b/server/auth/name-space/name-space.controller.ts
index 1e4e3edd5..5baae275d 100644
--- a/server/auth/name-space/name-space.controller.ts
+++ b/server/auth/name-space/name-space.controller.ts
@@ -22,13 +22,18 @@ import {
CheckAbilities,
} from "../../auth/ability/ability.decorator";
import { AbilitiesGuard } from "../../auth/ability/abilities.guard";
+import { Types } from "mongoose";
+import { Roles } from "../../auth/ability/ability.factory";
+import { NotificationService } from "../../notifications/notifications.service";
+import slugify from "slugify";
@Controller()
export class NameSpaceController {
constructor(
private nameSpaceService: NameSpaceService,
private usersService: UsersService,
- private viewService: ViewService
+ private viewService: ViewService,
+ private notificationService: NotificationService
) {}
@ApiTags("name-space")
@@ -36,6 +41,16 @@ export class NameSpaceController {
@UseGuards(AbilitiesGuard)
@CheckAbilities(new AdminUserAbility())
async create(@Body() namespace: CreateNameSpaceDTO) {
+ namespace.slug = slugify(namespace.name, {
+ lower: true,
+ strict: true,
+ });
+
+ namespace.users = await this.updateNameSpaceUsers(
+ namespace.users,
+ namespace.slug
+ );
+
return await this.nameSpaceService.create(namespace);
}
@@ -43,8 +58,31 @@ export class NameSpaceController {
@Put("api/name-space/:id")
@UseGuards(AbilitiesGuard)
@CheckAbilities(new AdminUserAbility())
- async update(@Param("id") id, @Body() namespace: UpdateNameSpaceDTO) {
- return await this.nameSpaceService.update(id, namespace);
+ async update(@Param("id") id, @Body() namespaceBody: UpdateNameSpaceDTO) {
+ const nameSpace = await this.nameSpaceService.getById(id);
+ const newNameSpace = {
+ ...nameSpace.toObject(),
+ ...namespaceBody,
+ };
+
+ newNameSpace.slug = slugify(nameSpace.name, {
+ lower: true,
+ strict: true,
+ });
+
+ newNameSpace.users = await this.updateNameSpaceUsers(
+ newNameSpace.users,
+ nameSpace.slug
+ );
+
+ await this.findNameSpaceUsersAndDelete(
+ id,
+ nameSpace.slug,
+ newNameSpace.users,
+ nameSpace.users
+ );
+
+ return await this.nameSpaceService.update(id, newNameSpace);
}
@ApiTags("name-space")
@@ -62,4 +100,58 @@ export class NameSpaceController {
Object.assign(parsedUrl.query, { nameSpaces, users })
);
}
+
+ private async updateNameSpaceUsers(users, key) {
+ const promises = users.map(async (user) => {
+ const userId = Types.ObjectId(user._id);
+ const existingUser = await this.usersService.getById(userId);
+
+ if (!existingUser.role[key]) {
+ await this.usersService.updateUser(existingUser._id, {
+ role: {
+ ...existingUser.role,
+ [key]: Roles.Regular,
+ },
+ });
+ }
+
+ return userId;
+ });
+
+ return await Promise.all(promises);
+ }
+
+ private async findNameSpaceUsersAndDelete(
+ id,
+ nameSpaceSlug,
+ users,
+ previousUsersId
+ ) {
+ const usersIdSet = new Set(users.map((user) => user.toString()));
+ const nameSpaceUsersTodelete = previousUsersId.filter(
+ (previousUserId) => !usersIdSet.has(previousUserId.toString())
+ );
+
+ if (nameSpaceUsersTodelete.length > 0) {
+ this.notificationService.removeTopicSubscriber(
+ id,
+ nameSpaceUsersTodelete
+ );
+ return await this.deleteUsersNameSpace(
+ nameSpaceUsersTodelete,
+ nameSpaceSlug
+ );
+ }
+ }
+
+ private async deleteUsersNameSpace(usersId, key) {
+ const updatePromises = usersId.map(async (userId) => {
+ const id = Types.ObjectId(userId);
+ const user = await this.usersService.getById(id);
+ delete user.role[key];
+ return this.usersService.updateUser(user._id, { role: user.role });
+ });
+
+ await Promise.all(updatePromises);
+ }
}
diff --git a/server/auth/name-space/name-space.service.ts b/server/auth/name-space/name-space.service.ts
index b6750771e..10bdb06af 100644
--- a/server/auth/name-space/name-space.service.ts
+++ b/server/auth/name-space/name-space.service.ts
@@ -1,10 +1,7 @@
import { Injectable } from "@nestjs/common";
-import { Model, Types } from "mongoose";
+import { Model } from "mongoose";
import { NameSpaceDocument, NameSpace } from "./schemas/name-space.schema";
import { InjectModel } from "@nestjs/mongoose";
-import slugify from "slugify";
-import { UsersService } from "../../users/users.service";
-import { Roles } from "../../auth/ability/ability.factory";
import { UpdateNameSpaceDTO } from "./dto/update-name-space.dto";
import { NotificationService } from "../../notifications/notifications.service";
@@ -13,8 +10,7 @@ export class NameSpaceService {
constructor(
@InjectModel(NameSpace.name)
private NameSpaceModel: Model,
- private notificationService: NotificationService,
- private usersService: UsersService
+ private notificationService: NotificationService
) {}
listAll() {
@@ -22,16 +18,6 @@ export class NameSpaceService {
}
async create(nameSpace) {
- nameSpace.slug = slugify(nameSpace.name, {
- lower: true,
- strict: true,
- });
-
- nameSpace.users = await this.updateNameSpaceUsers(
- nameSpace.users,
- nameSpace.slug
- );
-
const newNameSpace = await new this.NameSpaceModel(nameSpace).save();
await this.notificationService.createTopic(
@@ -47,27 +33,11 @@ export class NameSpaceService {
return newNameSpace;
}
- async update(id, nameSpaceBody: UpdateNameSpaceDTO) {
- const nameSpace = await this.getById(id);
- const newNameSpace = {
- ...nameSpace.toObject(),
- ...nameSpaceBody,
- };
-
- newNameSpace.slug = slugify(newNameSpace.name, {
- lower: true,
- strict: true,
- });
-
+ async update(id, newNameSpace: UpdateNameSpaceDTO) {
const isNameSpaceTopic = await this.notificationService.getTopic(
newNameSpace._id
);
- newNameSpace.users = await this.updateNameSpaceUsers(
- newNameSpace.users,
- nameSpace.slug
- );
-
await this.ensureTopicAndSubscribers(
id,
newNameSpace.name,
@@ -75,15 +45,8 @@ export class NameSpaceService {
isNameSpaceTopic
);
- await this.findNameSpaceUsersAndDelete(
- id,
- nameSpace.slug,
- newNameSpace.users,
- nameSpace.users
- );
-
return await this.NameSpaceModel.updateOne(
- { _id: nameSpace._id },
+ { _id: newNameSpace._id },
newNameSpace
);
}
@@ -103,59 +66,6 @@ export class NameSpaceService {
await this.notificationService.addTopicSubscriber(namespaceId, users);
}
- async updateNameSpaceUsers(users, key) {
- const promises = users.map(async (user) => {
- const userId = Types.ObjectId(user._id);
- const existingUser = await this.usersService.getById(userId);
-
- if (!user.role[key]) {
- await this.usersService.updateUser(existingUser._id, {
- role: {
- ...existingUser.role,
- [key]: Roles.Regular,
- },
- });
- }
-
- return userId;
- });
-
- return await Promise.all(promises);
- }
-
- async deleteUsersNameSpace(usersId, key) {
- const updatePromises = usersId.map(async (userId) => {
- const id = Types.ObjectId(userId);
- const user = await this.usersService.getById(id);
- delete user.role[key];
- return this.usersService.updateUser(user._id, { role: user.role });
- });
-
- await Promise.all(updatePromises);
- }
-
- async findNameSpaceUsersAndDelete(
- id,
- nameSpaceSlug,
- users,
- previousUsersId
- ) {
- const usersIdSet = new Set(users.map((user) => user.toString()));
- const nameSpaceUsersTodelete = previousUsersId.filter(
- (previousUserId) => !usersIdSet.has(previousUserId.toString())
- );
- if (nameSpaceUsersTodelete.length > 0) {
- this.notificationService.removeTopicSubscriber(
- id,
- nameSpaceUsersTodelete
- );
- return await this.deleteUsersNameSpace(
- nameSpaceUsersTodelete,
- nameSpaceSlug
- );
- }
- }
-
findOne(match) {
return this.NameSpaceModel.findOne(match);
}
diff --git a/server/automated-fact-checking/automated-fact-checking.module.ts b/server/automated-fact-checking/automated-fact-checking.module.ts
index f53a82e21..96678aca6 100644
--- a/server/automated-fact-checking/automated-fact-checking.module.ts
+++ b/server/automated-fact-checking/automated-fact-checking.module.ts
@@ -1,11 +1,11 @@
import { Module } from "@nestjs/common";
-import { ClaimReviewTaskModule } from "../claim-review-task/claim-review-task.module";
+import { ReviewTaskModule } from "../review-task/review-task.module";
import { AutomatedFactCheckingService } from "./automated-fact-checking.service";
import { AutomatedFactCheckingController } from "./automated-fact-checking.controller";
import { ConfigModule } from "@nestjs/config";
@Module({
- imports: [ClaimReviewTaskModule, ConfigModule],
+ imports: [ReviewTaskModule, ConfigModule],
providers: [AutomatedFactCheckingService],
exports: [AutomatedFactCheckingService],
controllers: [AutomatedFactCheckingController],
diff --git a/server/automated-fact-checking/automated-fact-checking.service.ts b/server/automated-fact-checking/automated-fact-checking.service.ts
index 5490152de..1ce95eaff 100644
--- a/server/automated-fact-checking/automated-fact-checking.service.ts
+++ b/server/automated-fact-checking/automated-fact-checking.service.ts
@@ -12,7 +12,6 @@ export class AutomatedFactCheckingService {
}
async getResponseFromAgents(data): Promise<{ stream: string; json: any }> {
- console.log(this.agenciaURL);
const params = {
input: {
claim: data.claim,
@@ -50,8 +49,13 @@ export class AutomatedFactCheckingService {
.map((line) => JSON.parse(line.substring(5)))
.reduce((acc, data) => ({ ...acc, ...data }), {});
- jsonEvents.__end__.messages = JSON.parse(jsonEvents.__end__.messages);
+ if (jsonEvents.start_fact_checking) {
+ const report = JSON.parse(jsonEvents.start_fact_checking.messages);
+ return { stream: streamResponse, json: { messages: report } };
+ }
+
+ const report = JSON.parse(jsonEvents.create_report.messages);
- return { stream: streamResponse, json: jsonEvents.__end__ };
+ return { stream: streamResponse, json: { messages: report } };
}
}
diff --git a/server/chat-bot/chat-bot-actions.ts b/server/chat-bot/chat-bot-actions.ts
new file mode 100644
index 000000000..53b5e5844
--- /dev/null
+++ b/server/chat-bot/chat-bot-actions.ts
@@ -0,0 +1,74 @@
+import { EventObject, assign } from "xstate";
+import { ChatBotContext } from "./chat-bot.machine";
+
+interface VerificationRequestEvent extends EventObject {
+ verificationRequest: string;
+}
+
+const MESSAGES = {
+ greeting:
+ "Olá! Sou o assistente virtual da AletheiaFact.org, estou aqui para ajudá-lo(a) a combater desinformações 🙂 Você gostaria de fazer uma denúncia agora?\n\nResponda SIM para continuar ou NÃO se não deseja denunciar.",
+ noMessage:
+ "Entendi. Nosso trabalho é verificar informações falsas.\n\nSe quiser saber mais sobre o que fazemos, visite: https://aletheiafact.org. Se mudar de ideia e desejar fazer uma denúncia, basta digitar DENÚNCIA a qualquer momento.",
+ notUnderstood:
+ "Desculpe, não entendi sua resposta. Para continuar, preciso que você digite SIM se deseja fazer uma denúncia, ou NÃO se não deseja.\n\nVocê gostaria de fazer uma denúncia agora?",
+ askForVerificationRequest:
+ "Por favor, me conte com detalhes o que você gostaria de denunciar.\n\nPor favor, inclua todas as informações que considerar relevantes para que possamos verificar a denúncia de forma eficiente 👀",
+ thanks: "Muito obrigada por sua contribuição!\n\nSua informação será analisada pela nossa equipe ✅Para saber mais, visite nosso site: https://aletheiafact.org.\n\nDeseja relatar outra denúncia? Responda SIM para continuar ou NÃO para encerrar.",
+ noTextMessageGreeting:
+ "Desculpe, só podemos processar mensagens de texto. Por favor, envie sua mensagem em formato de texto.\n\nOlá! Sou o assistente virtual da AletheiaFact.org, estou aqui para ajudá-lo(a) a combater desinformações 🙂 Você gostaria de fazer uma denúncia agora?\n\nResponda SIM para continuar ou NÃO se não deseja denunciar.",
+ noTextMessageAskForVerificationRequest:
+ "Desculpe, só podemos processar mensagens de texto. Por favor, envie sua mensagem em formato de texto para que possamos entender e verificar sua denúncia de forma eficiente.\n\nPor favor, me conte com detalhes o que você gostaria de denunciar.\n\nPor favor, inclua todas as informações que considerar relevantes para que possamos verificar a denúncia de forma eficiente 👀",
+ noTextMessageNoMessage:
+ "Desculpe, só podemos processar mensagens de texto. Por favor, envie sua mensagem em formato de texto.\n\nNosso trabalho é verificar informações falsas.\n\nSe quiser saber mais sobre o que fazemos, visite: https://aletheiafact.org. Se mudar de ideia e desejar fazer uma denúncia, basta digitar DENÚNCIA a qualquer momento.",
+ noTextMessageAskIfForVerificationRequest:
+ "Desculpe, só podemos processar mensagens de texto. Por favor, envie sua mensagem em formato de texto.\n\nVocê gostaria de fazer uma denúncia agora? Responda SIM para continuar ou NÃO se não deseja.",
+};
+
+export const sendGreeting = assign({
+ responseMessage: () => MESSAGES.greeting,
+});
+
+export const sendNoMessage = assign({
+ responseMessage: () => MESSAGES.noMessage,
+});
+
+export const sendNotUnderstoodMessage = assign({
+ responseMessage: () => MESSAGES.notUnderstood,
+});
+
+export const askForVerificationRequest = assign({
+ responseMessage: () => MESSAGES.askForVerificationRequest,
+});
+
+export const saveVerificationRequest = assign({
+ verificationRequest: (context, event) =>
+ (event as VerificationRequestEvent).verificationRequest,
+});
+
+export const sendThanks = assign({
+ responseMessage: () => MESSAGES.thanks,
+});
+
+export const sendNoTextMessageGreeting = assign({
+ responseMessage: () => MESSAGES.noTextMessageGreeting,
+});
+
+export const sendNoTextMessageAskForVerificationRequest =
+ assign({
+ responseMessage: () => MESSAGES.noTextMessageAskForVerificationRequest,
+ });
+
+export const sendNoTextMessageNoMessage = assign({
+ responseMessage: () => MESSAGES.noTextMessageNoMessage,
+});
+
+export const sendNoTextMessageAskIfForVerificationRequest =
+ assign({
+ responseMessage: () =>
+ MESSAGES.noTextMessageAskIfForVerificationRequest,
+ });
+
+export const setResponseMessage = assign({
+ responseMessage: (context) => context.responseMessage,
+});
diff --git a/server/chat-bot/chat-bot.controller.ts b/server/chat-bot/chat-bot.controller.ts
new file mode 100644
index 000000000..e4fc8e152
--- /dev/null
+++ b/server/chat-bot/chat-bot.controller.ts
@@ -0,0 +1,16 @@
+import { Controller, Post, Req, Res } from "@nestjs/common";
+import { ChatbotService } from "./chat-bot.service";
+import type { Request, Response } from "express";
+
+@Controller()
+export class ChatbotController {
+ constructor(private readonly chatBotService: ChatbotService) {}
+
+ @Post("api/chatbot/hook")
+ handleHook(@Req() req: Request, @Res() res: Response) {
+ const response = this.chatBotService.sendMessage(req.body.message);
+
+ res.status(200).json({ message: response });
+ return response;
+ }
+}
diff --git a/server/chat-bot/chat-bot.machine.ts b/server/chat-bot/chat-bot.machine.ts
new file mode 100644
index 000000000..d9e921597
--- /dev/null
+++ b/server/chat-bot/chat-bot.machine.ts
@@ -0,0 +1,128 @@
+import { createMachine, interpret } from "xstate";
+import * as actions from "./chat-bot-actions";
+import { VerificationRequestService } from "../verification-request/verification-request.service";
+
+export interface ChatBotContext {
+ verificationRequest: string;
+ responseMessage: string;
+}
+
+export const createChatBotMachine = (
+ verificationRequestService: VerificationRequestService
+) => {
+ const chatBotMachine = createMachine(
+ {
+ id: "chatBot",
+ initial: "greeting",
+ context: {
+ verificationRequest: "",
+ responseMessage: "",
+ },
+ states: {
+ greeting: {
+ on: {
+ ASK_IF_VERIFICATION_REQUEST: {
+ target: "askingIfVerificationRequest",
+ actions: ["sendGreeting", "setResponseMessage"],
+ },
+ NON_TEXT_MESSAGE: {
+ target: "askingIfVerificationRequest",
+ actions: [
+ "sendNoTextMessageGreeting",
+ "setResponseMessage",
+ ],
+ },
+ },
+ },
+ askingIfVerificationRequest: {
+ on: {
+ RECEIVE_YES: {
+ target: "askingForVerificationRequest",
+ actions: [
+ "askForVerificationRequest",
+ "setResponseMessage",
+ ],
+ },
+ RECEIVE_NO: {
+ target: "sendingNoMessage",
+ actions: ["sendNoMessage", "setResponseMessage"],
+ },
+ NOT_UNDERSTOOD: {
+ target: "askingIfVerificationRequest",
+ actions: [
+ "sendNotUnderstoodMessage",
+ "setResponseMessage",
+ ],
+ },
+ NON_TEXT_MESSAGE: {
+ target: "askingIfVerificationRequest",
+ actions: [
+ "sendNoTextMessageAskIfForVerificationRequest",
+ "setResponseMessage",
+ ],
+ },
+ },
+ },
+ askingForVerificationRequest: {
+ on: {
+ RECEIVE_REPORT: {
+ target: "askingIfVerificationRequest",
+ actions: [
+ "saveVerificationRequest",
+ "sendThanks",
+ "setResponseMessage",
+ "saveVerificationRequestToDB",
+ ],
+ },
+ NON_TEXT_MESSAGE: {
+ target: "askingForVerificationRequest",
+ actions: [
+ "sendNoTextMessageAskForVerificationRequest",
+ "setResponseMessage",
+ ],
+ },
+ },
+ },
+ sendingNoMessage: {
+ on: {
+ ASK_TO_REPORT: {
+ target: "askingForVerificationRequest",
+ actions: [
+ "askForVerificationRequest",
+ "setResponseMessage",
+ ],
+ },
+ RECEIVE_NO: {
+ target: "sendingNoMessage",
+ actions: ["sendNoMessage", "setResponseMessage"],
+ },
+ NON_TEXT_MESSAGE: {
+ target: "sendingNoMessage",
+ actions: [
+ "sendNoTextMessageNoMessage",
+ "setResponseMessage",
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ actions: {
+ ...actions,
+ saveVerificationRequestToDB: (context) => {
+ const verificationRequestBody = {
+ content: context.verificationRequest,
+ date: new Date(),
+ sources: [],
+ data_hash: "",
+ };
+
+ verificationRequestService.create(verificationRequestBody);
+ },
+ },
+ }
+ );
+
+ return interpret(chatBotMachine);
+};
diff --git a/server/chat-bot/chat-bot.module.ts b/server/chat-bot/chat-bot.module.ts
new file mode 100644
index 000000000..95487d163
--- /dev/null
+++ b/server/chat-bot/chat-bot.module.ts
@@ -0,0 +1,26 @@
+import {
+ MiddlewareConsumer,
+ Module,
+ NestModule,
+ RequestMethod,
+} from "@nestjs/common";
+import { ChatbotService } from "./chat-bot.service";
+import { ChatbotController } from "./chat-bot.controller";
+import { HttpModule } from "@nestjs/axios";
+import { VerificationRequestModule } from "../verification-request/verification-request.module";
+import { ConfigModule } from "@nestjs/config";
+import { AuthZenviaWebHookMiddleware } from "../middleware/auth-zenvia-webhook.middleware";
+
+@Module({
+ imports: [HttpModule, VerificationRequestModule, ConfigModule],
+ providers: [ChatbotService],
+ controllers: [ChatbotController],
+})
+export class ChatbotModule implements NestModule {
+ configure(consumer: MiddlewareConsumer) {
+ consumer.apply(AuthZenviaWebHookMiddleware).forRoutes({
+ path: "api/chatbot/hook",
+ method: RequestMethod.POST,
+ });
+ }
+}
diff --git a/server/chat-bot/chat-bot.service.ts b/server/chat-bot/chat-bot.service.ts
new file mode 100644
index 000000000..aaf8b9bed
--- /dev/null
+++ b/server/chat-bot/chat-bot.service.ts
@@ -0,0 +1,112 @@
+import { Injectable } from "@nestjs/common";
+import { HttpService } from "@nestjs/axios";
+import { AxiosResponse } from "axios";
+import { catchError, map } from "rxjs/operators";
+import { Observable, throwError } from "rxjs";
+import { createChatBotMachine } from "./chat-bot.machine";
+import { VerificationRequestService } from "../verification-request/verification-request.service";
+import { ConfigService } from "@nestjs/config";
+
+const diacriticsRegex = /[\u0300-\u036f]/g;
+const MESSAGE_MAP = {
+ sim: "RECEIVE_YES",
+ nao: "RECEIVE_NO",
+};
+
+@Injectable()
+export class ChatbotService {
+ private chatBotMachineService;
+
+ constructor(
+ private configService: ConfigService,
+ private readonly httpService: HttpService,
+ private verificationService: VerificationRequestService
+ ) {}
+
+ onModuleInit() {
+ this.initializeChatBotMachine();
+ }
+
+ private initializeChatBotMachine() {
+ this.chatBotMachineService = createChatBotMachine(
+ this.verificationService
+ );
+ this.chatBotMachineService.start();
+ }
+
+ //TODO: Find a better way to interpret the user's message.
+ private normalizeAndLowercase(message: string): string {
+ return message
+ .normalize("NFD")
+ .replace(diacriticsRegex, "")
+ .toLowerCase();
+ }
+
+ private handleMachineEventSend(parsedMessage: string): void {
+ this.chatBotMachineService.send(
+ MESSAGE_MAP[parsedMessage] || "NOT_UNDERSTOOD"
+ );
+ }
+
+ private handleSendingNoMessage(parsedMessage: string): void {
+ this.chatBotMachineService.send(
+ parsedMessage === "denuncia" ? "ASK_TO_REPORT" : "RECEIVE_NO"
+ );
+ }
+
+ private handleUserMessage(message): void {
+ const messageType = message.contents[0].type;
+ const userMessage = message.contents[0].text;
+
+ if (messageType !== "text") {
+ this.chatBotMachineService.send("NON_TEXT_MESSAGE");
+ return;
+ }
+
+ const parsedMessage = this.normalizeAndLowercase(userMessage);
+ const currentState = this.chatBotMachineService.getSnapshot().value;
+
+ switch (currentState) {
+ case "greeting":
+ this.chatBotMachineService.send("ASK_IF_VERIFICATION_REQUEST");
+ break;
+ case "askingIfVerificationRequest":
+ this.handleMachineEventSend(parsedMessage);
+ break;
+ case "askingForVerificationRequest":
+ this.chatBotMachineService.send({
+ type: "RECEIVE_REPORT",
+ verificationRequest: userMessage,
+ });
+ break;
+ case "sendingNoMessage":
+ this.handleSendingNoMessage(parsedMessage);
+ break;
+ default:
+ console.warn(`Unhandled state: ${currentState}`);
+ }
+ }
+
+ public sendMessage(message): Observable> {
+ const { api_url, api_token } = this.configService.get("zenvia");
+ this.handleUserMessage(message);
+
+ const snapshot = this.chatBotMachineService.getSnapshot();
+ const responseMessage = snapshot.context.responseMessage;
+
+ const body = {
+ from: message.to,
+ to: message.from,
+ contents: [{ type: "text", text: responseMessage }],
+ };
+
+ return this.httpService
+ .post(api_url, body, {
+ headers: { "X-API-TOKEN": api_token },
+ })
+ .pipe(
+ map((response) => response),
+ catchError((error) => throwError(() => new Error(error)))
+ );
+ }
+}
diff --git a/server/claim-review-task/claim-review-task.controller.ts b/server/claim-review-task/claim-review-task.controller.ts
deleted file mode 100644
index e28e3962d..000000000
--- a/server/claim-review-task/claim-review-task.controller.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-import {
- Body,
- Controller,
- Post,
- Param,
- Get,
- Put,
- Query,
- Req,
- Res,
- Header,
- Optional,
-} from "@nestjs/common";
-import { ClaimReviewTaskService } from "./claim-review-task.service";
-import { CreateClaimReviewTaskDTO } from "./dto/create-claim-review-task.dto";
-import { UpdateClaimReviewTaskDTO } from "./dto/update-claim-review-task.dto";
-import { CaptchaService } from "../captcha/captcha.service";
-import { parse } from "url";
-import type { Request, Response } from "express";
-import { ViewService } from "../view/view.service";
-import { GetTasksDTO } from "./dto/get-tasks.dto";
-import { getQueryMatchForMachineValue } from "./mongo-utils";
-import { ConfigService } from "@nestjs/config";
-import { UnleashService } from "nestjs-unleash";
-import { ApiTags } from "@nestjs/swagger";
-import { NameSpaceEnum } from "../auth/name-space/schemas/name-space.schema";
-
-@Controller(":namespace?")
-export class ClaimReviewController {
- constructor(
- private claimReviewTaskService: ClaimReviewTaskService,
- private captchaService: CaptchaService,
- private viewService: ViewService,
- private configService: ConfigService,
- @Optional() private readonly unleash: UnleashService
- ) {}
-
- @ApiTags("claim-review-task")
- @Get("api/claimreviewtask")
- @Header("Cache-Control", "no-cache")
- public async getByMachineValue(@Query() getTasksDTO: GetTasksDTO) {
- const {
- page = 0,
- pageSize = 10,
- order = 1,
- value,
- filterUser,
- nameSpace = NameSpaceEnum.Main,
- } = getTasksDTO;
- return Promise.all([
- this.claimReviewTaskService.listAll(
- page,
- pageSize,
- order,
- value,
- filterUser,
- nameSpace
- ),
- this.claimReviewTaskService.countReviewTasksNotDeleted(
- getQueryMatchForMachineValue(value),
- filterUser,
- nameSpace
- ),
- ]).then(([tasks, totalTasks]) => {
- const totalPages = Math.ceil(totalTasks / pageSize);
-
- return {
- tasks,
- totalTasks,
- totalPages,
- page,
- pageSize,
- };
- });
- }
-
- @ApiTags("claim-review-task")
- @Get("api/claimreviewtask/:id")
- @Header("Cache-Control", "no-cache")
- async getById(@Param("id") id: string) {
- return this.claimReviewTaskService.getById(id);
- }
-
- @ApiTags("claim-review-task")
- @Post("api/claimreviewtask")
- @Header("Cache-Control", "no-cache")
- async create(@Body() createClaimReviewTask: CreateClaimReviewTaskDTO) {
- const validateCaptcha = await this.captchaService.validate(
- createClaimReviewTask.recaptcha
- );
- if (!validateCaptcha) {
- throw new Error("Error validating captcha");
- }
- return this.claimReviewTaskService.create(createClaimReviewTask);
- }
-
- @ApiTags("claim-review-task")
- @Put("api/claimreviewtask/:data_hash")
- @Header("Cache-Control", "no-cache")
- async autoSaveDraft(
- @Param("data_hash") data_hash,
- @Body() claimReviewTaskBody: UpdateClaimReviewTaskDTO
- ) {
- const history = false;
- return this.claimReviewTaskService
- .getClaimReviewTaskByDataHash(data_hash)
- .then((review) => {
- if (review) {
- return this.claimReviewTaskService.update(
- data_hash,
- claimReviewTaskBody,
- claimReviewTaskBody.nameSpace,
- claimReviewTaskBody.reportModel,
- history
- );
- }
- });
- }
-
- // TODO: remove hash from the url
- @ApiTags("claim-review-task")
- @Get("api/claimreviewtask/hash/:data_hash")
- @Header("Cache-Control", "no-cache")
- async getByDataHash(@Param("data_hash") data_hash: string) {
- return this.claimReviewTaskService.getClaimReviewTaskByDataHash(
- data_hash
- );
- }
-
- @ApiTags("claim-review-task")
- @Get("api/claimreviewtask/editor-content/:data_hash")
- @Header("Cache-Control", "no-cache")
- async getEditorContentByDataHash(
- @Param("data_hash") data_hash: string,
- @Query() query: { reportModel: string }
- ) {
- const claimReviewTask =
- await this.claimReviewTaskService.getClaimReviewTaskByDataHash(
- data_hash
- );
-
- return this.claimReviewTaskService.getEditorContentObject(
- claimReviewTask?.machine?.context?.reviewData,
- query.reportModel
- );
- }
-
- @ApiTags("claim-review-task")
- @Put("api/claimreviewtask/add-comment/:data_hash")
- @Header("Cache-Control", "no-cache")
- async addComment(@Param("data_hash") data_hash: string, @Body() body) {
- return this.claimReviewTaskService.addComment(data_hash, body.comment);
- }
-
- @ApiTags("claim-review-task")
- @Put("api/claimreviewtask/delete-comment/:data_hash")
- @Header("Cache-Control", "no-cache")
- async deleteComment(@Param("data_hash") data_hash: string, @Body() body) {
- return this.claimReviewTaskService.deleteComment(
- data_hash,
- body.commentId
- );
- }
-
- @ApiTags("pages")
- @Get("kanban")
- @Header("Cache-Control", "no-cache")
- public async personalityList(@Req() req: Request, @Res() res: Response) {
- const parsedUrl = parse(req.url, true);
- const enableCollaborativeEditor = this.isEnableCollaborativeEditor();
- const enableCopilotChatBot = this.isEnableCopilotChatBot();
- const enableEditorAnnotations = this.isEnableEditorAnnotations();
- const enableAddEditorSourcesWithoutSelecting =
- this.isEnableAddEditorSourcesWithoutSelecting();
-
- await this.viewService.getNextServer().render(
- req,
- res,
- "/kanban-page",
- Object.assign(parsedUrl.query, {
- sitekey: this.configService.get("recaptcha_sitekey"),
- enableCollaborativeEditor,
- enableEditorAnnotations,
- enableCopilotChatBot,
- enableAddEditorSourcesWithoutSelecting,
- websocketUrl: this.configService.get("websocketUrl"),
- nameSpace: req.params.namespace,
- })
- );
- }
-
- private isEnableCollaborativeEditor() {
- const config = this.configService.get("feature_flag");
-
- return config
- ? this.unleash.isEnabled("enable_collaborative_editor")
- : false;
- }
-
- private isEnableCopilotChatBot() {
- const config = this.configService.get("feature_flag");
-
- return config ? this.unleash.isEnabled("copilot_chat_bot") : false;
- }
-
- private isEnableEditorAnnotations() {
- const config = this.configService.get("feature_flag");
-
- return config
- ? this.unleash.isEnabled("enable_editor_annotations")
- : false;
- }
-
- private isEnableAddEditorSourcesWithoutSelecting() {
- const config = this.configService.get("feature_flag");
-
- return config
- ? this.unleash.isEnabled(
- "enable_add_editor_sources_without_selecting"
- )
- : false;
- }
-}
diff --git a/server/claim-review-task/claim-review-task.service.ts b/server/claim-review-task/claim-review-task.service.ts
deleted file mode 100644
index 3ef77ffc2..000000000
--- a/server/claim-review-task/claim-review-task.service.ts
+++ /dev/null
@@ -1,635 +0,0 @@
-import { ForbiddenException, Inject, Injectable, Scope } from "@nestjs/common";
-import { Model, Types } from "mongoose";
-import {
- ClaimReviewTask,
- ClaimReviewTaskDocument,
-} from "./schemas/claim-review-task.schema";
-import { InjectModel } from "@nestjs/mongoose";
-import { CreateClaimReviewTaskDTO } from "./dto/create-claim-review-task.dto";
-import { UpdateClaimReviewTaskDTO } from "./dto/update-claim-review-task.dto";
-import { ClaimReviewService } from "../claim-review/claim-review.service";
-import { ReportService } from "../report/report.service";
-import { HistoryType, TargetModel } from "../history/schema/history.schema";
-import { HistoryService } from "../history/history.service";
-import { StateEventService } from "../state-event/state-event.service";
-import { TypeModel } from "../state-event/schema/state-event.schema";
-import { REQUEST } from "@nestjs/core";
-import type { BaseRequest } from "../types";
-import { SentenceService } from "../claim/types/sentence/sentence.service";
-import { getQueryMatchForMachineValue } from "./mongo-utils";
-import { Roles } from "../auth/ability/ability.factory";
-import { ImageService } from "../claim/types/image/image.service";
-import { ContentModelEnum } from "../types/enums";
-import lookupUsers from "../mongo-pipelines/lookupUsers";
-import lookUpPersonalityties from "../mongo-pipelines/lookUpPersonalityties";
-import lookupClaims from "../mongo-pipelines/lookupClaims";
-import lookupClaimReviews from "../mongo-pipelines/lookupClaimReviews";
-import lookupClaimRevisions from "../mongo-pipelines/lookupClaimRevisions";
-import { EditorParseService } from "../editor-parse/editor-parse.service";
-import { CommentService } from "./comment/comment.service";
-import { NameSpaceEnum } from "../auth/name-space/schemas/name-space.schema";
-import { CommentEnum } from "./comment/schema/comment.schema";
-
-@Injectable({ scope: Scope.REQUEST })
-export class ClaimReviewTaskService {
- constructor(
- @Inject(REQUEST) private req: BaseRequest,
- @InjectModel(ClaimReviewTask.name)
- private ClaimReviewTaskModel: Model,
- private claimReviewService: ClaimReviewService,
- private reportService: ReportService,
- private historyService: HistoryService,
- private stateEventService: StateEventService,
- private sentenceService: SentenceService,
- private imageService: ImageService,
- private editorParseService: EditorParseService,
- private commentService: CommentService
- ) {}
-
- _verifyMachineValueAndAddMatchPipeline(pipeline, value) {
- if (value === "published") {
- pipeline.push(
- lookupClaimReviews({
- as: "machine.context.claimReview.claimReview",
- }),
- { $unwind: "$machine.context.claimReview.claimReview" },
- {
- $match: {
- "machine.context.claimReview.claimReview.isDeleted":
- false,
- "machine.context.claimReview.claim.isDeleted": false,
- },
- }
- );
- } else {
- pipeline.push({
- $match: {
- "machine.context.claimReview.claim.isDeleted": false,
- },
- });
- }
- }
-
- _buildPipeline(value, filterUser, nameSpace) {
- const pipeline = [];
- const query = getQueryMatchForMachineValue(value);
- const fieldMap = {
- assigned: "machine.context.reviewData.usersId",
- crossChecked: "machine.context.reviewData.crossCheckerId",
- reviewed: "machine.context.reviewData.reviewerId",
- };
-
- Object.keys(filterUser).forEach((key) => {
- const value = filterUser[key];
- if (value === true || value === "true") {
- const queryPath = fieldMap[key];
- query[queryPath] = Types.ObjectId(this.req.user._id);
- }
- });
-
- pipeline.push(
- { $match: query },
- lookupUsers(),
- lookUpPersonalityties(TargetModel.ClaimReviewTask),
- lookupClaims(TargetModel.ClaimReviewTask, {
- pipeline: [
- { $match: { $expr: { $eq: ["$_id", "$$claimId"] } } },
- lookupClaimRevisions(TargetModel.ClaimReviewTask),
- { $unwind: "$latestRevision" },
- ],
- as: "machine.context.claimReview.claim",
- }),
- {
- $match: {
- "machine.context.claimReview.claim.nameSpace": nameSpace,
- },
- }
- );
-
- this._verifyMachineValueAndAddMatchPipeline(pipeline, value);
-
- return pipeline;
- }
-
- async listAll(page, pageSize, order, value, filterUser, nameSpace) {
- const pipeline = this._buildPipeline(value, filterUser, nameSpace);
- pipeline.push(
- { $sort: { _id: order === "asc" ? 1 : -1 } },
- { $skip: page * pageSize },
- { $limit: pageSize }
- );
-
- const reviewTasks = await this.ClaimReviewTaskModel.aggregate(
- pipeline
- ).exec();
-
- return Promise.all(
- reviewTasks?.map(async ({ data_hash, machine }) => {
- const {
- personality: [personality],
- claim: [claim],
- }: any = machine.context.claimReview;
- const { title, contentModel } = claim.latestRevision;
- const isContentImage = contentModel === ContentModelEnum.Image;
-
- const personalityPath = `/personality/${personality?.slug}`;
-
- const contentModelPathMap = {
- [ContentModelEnum.Debate]: `/claim/${claim?._id}/debate`,
- [ContentModelEnum.Image]: personality
- ? `${personalityPath}/claim/${claim?.slug}/${claim?._id}`
- : `/claim/${claim?._id}`,
- [ContentModelEnum.Speech]: `${personalityPath}/claim/${claim?.slug}/sentence/${data_hash}`,
- };
-
- let reviewHref =
- nameSpace !== NameSpaceEnum.Main
- ? `/${nameSpace}${contentModelPathMap[contentModel]}`
- : contentModelPathMap[contentModel];
-
- const usersName = machine.context.reviewData.users.map(
- (user) => {
- return user.name;
- }
- );
-
- const content = isContentImage
- ? await this.imageService.getByDataHash(data_hash)
- : await this.sentenceService.getByDataHash(data_hash);
-
- return {
- content,
- usersName,
- value: machine.value,
- personalityName: personality?.name,
- claimTitle: title,
- claimId: claim._id,
- personalityId: personality?._id,
- reviewHref,
- contentModel,
- };
- })
- );
- }
-
- getById(claimReviewTaskId: string) {
- return this.ClaimReviewTaskModel.findById(claimReviewTaskId);
- }
-
- _createReviewTaskHistory(
- newClaimReviewTask,
- previousClaimReviewTask = null
- ) {
- let historyType;
-
- if (typeof newClaimReviewTask.machine.value === "object") {
- historyType =
- newClaimReviewTask.machine.value?.[
- Object.keys(newClaimReviewTask.machine.value)[0]
- ] === "draft"
- ? HistoryType.Draft
- : Object.keys(newClaimReviewTask.machine.value)[0];
- }
-
- const user = this.req.user;
-
- const history = this.historyService.getHistoryParams(
- newClaimReviewTask._id,
- TargetModel.ClaimReviewTask,
- user,
- historyType || HistoryType.Published,
- {
- ...newClaimReviewTask.machine.context.reviewData,
- ...newClaimReviewTask.machine.context.claimReview.claim,
- value: newClaimReviewTask.machine.value,
- },
- previousClaimReviewTask && {
- ...previousClaimReviewTask.machine.context.reviewData,
- ...previousClaimReviewTask.machine.context.claimReview.claim,
- value: previousClaimReviewTask.machine.value,
- }
- );
-
- this.historyService.createHistory(history);
- }
-
- _createStateEvent(newClaimReviewTask) {
- let typeModel;
- let draft = false;
-
- if (typeof newClaimReviewTask.machine.value === "object") {
- draft =
- newClaimReviewTask.machine.value?.[
- Object.keys(newClaimReviewTask.machine.value)[0]
- ] === "draft"
- ? true
- : false;
-
- typeModel = Object.keys(newClaimReviewTask.machine.value)[0];
- }
-
- const stateEvent = this.stateEventService.getStateEventParams(
- Types.ObjectId(
- newClaimReviewTask.machine.context.claimReview.claim
- ),
- typeModel || TypeModel.Published,
- draft,
- newClaimReviewTask._id
- );
-
- this.stateEventService.createStateEvent(stateEvent);
- }
-
- async _createReportAndClaimReview(
- data_hash,
- machine,
- reportModel,
- nameSpace
- ) {
- const claimReviewData = machine.context.claimReview;
-
- const newReport = Object.assign(machine.context.reviewData, {
- data_hash,
- reportModel,
- });
-
- const report = await this.reportService.create(newReport);
-
- this.claimReviewService.create(
- {
- ...claimReviewData,
- report,
- nameSpace,
- },
- data_hash,
- reportModel
- );
- }
-
- _returnObjectId(data): any {
- if (Array.isArray(data)) {
- return data.map((item) =>
- item._id ? Types.ObjectId(item._id) || "" : Types.ObjectId(item)
- );
- }
- }
-
- _createCrossCheckingComment(comment, text, targetId) {
- const newCrossCheckingComment = {
- comment,
- text,
- type: CommentEnum.crossChecking,
- targetId,
- user: this.req.user._id,
- };
- return this.commentService.create(newCrossCheckingComment);
- }
-
- async create(claimReviewTaskBody: CreateClaimReviewTaskDTO) {
- const reviewDataBody = claimReviewTaskBody.machine.context.reviewData;
- const claimReviewTask = await this.getClaimReviewTaskByDataHash(
- claimReviewTaskBody.data_hash
- );
-
- const createCrossCheckingComment =
- claimReviewTask?.machine?.value === "addCommentCrossChecking" &&
- reviewDataBody.crossCheckingClassification &&
- reviewDataBody.crossCheckingComment;
-
- claimReviewTaskBody.machine.context.reviewData.usersId =
- this._returnObjectId(reviewDataBody.usersId);
-
- if (reviewDataBody.reviewerId) {
- claimReviewTaskBody.machine.context.reviewData.reviewerId =
- Types.ObjectId(reviewDataBody.reviewerId) || "";
- }
-
- if (reviewDataBody.crossCheckerId) {
- claimReviewTaskBody.machine.context.reviewData.crossCheckerId =
- Types.ObjectId(reviewDataBody.crossCheckerId) || "";
- }
-
- if (reviewDataBody.reviewComments) {
- this.commentService.updateManyComments(
- reviewDataBody.reviewComments
- );
- claimReviewTaskBody.machine.context.reviewData.reviewComments =
- this._returnObjectId(reviewDataBody.reviewComments);
- }
-
- if (reviewDataBody.crossCheckingComments) {
- claimReviewTaskBody.machine.context.reviewData.crossCheckingComments =
- this._returnObjectId(reviewDataBody.crossCheckingComments);
- }
-
- if (createCrossCheckingComment) {
- const crossCheckingComment = await this._createCrossCheckingComment(
- reviewDataBody.crossCheckingComment,
- reviewDataBody.crossCheckingClassification,
- claimReviewTask._id
- );
- claimReviewTaskBody.machine.context.reviewData.crossCheckingComments.push(
- crossCheckingComment._id
- );
- }
-
- if (claimReviewTask) {
- return this.update(
- claimReviewTaskBody.data_hash,
- claimReviewTaskBody,
- claimReviewTaskBody.nameSpace,
- claimReviewTask.reportModel
- );
- } else {
- const newClaimReviewTask = new this.ClaimReviewTaskModel(
- claimReviewTaskBody
- );
- newClaimReviewTask.save();
- this._createReviewTaskHistory(newClaimReviewTask);
- this._createStateEvent(newClaimReviewTask);
- return newClaimReviewTask;
- }
- }
-
- async update(
- data_hash: string,
- { machine }: UpdateClaimReviewTaskDTO,
- nameSpace: string,
- reportModel: string,
- history: boolean = true
- ) {
- const loggedInUser = this.req.user;
- // This line may cause a false positive in sonarCloud because if we remove the await, we cannot iterate through the results
- const claimReviewTask = await this.getClaimReviewTaskByDataHash(
- data_hash
- );
-
- const newClaimReviewTaskMachine = {
- ...claimReviewTask.machine,
- ...machine,
- };
-
- const newClaimReviewTask = {
- ...claimReviewTask.toObject(),
- machine: newClaimReviewTaskMachine,
- };
-
- if (newClaimReviewTaskMachine.value === "published") {
- if (
- loggedInUser.role[nameSpace] !== Roles.Admin &&
- loggedInUser.role[nameSpace] !== Roles.SuperAdmin &&
- loggedInUser._id !==
- machine.context.reviewData.reviewerId.toString()
- ) {
- throw new ForbiddenException(
- "This user does not have permission to publish the report"
- );
- }
- this._createReportAndClaimReview(
- data_hash,
- newClaimReviewTask.machine,
- reportModel,
- nameSpace
- );
- }
-
- if (history) {
- this._createReviewTaskHistory(newClaimReviewTask, claimReviewTask);
- this._createStateEvent(newClaimReviewTask);
- }
-
- return this.ClaimReviewTaskModel.updateOne(
- { _id: newClaimReviewTask._id },
- newClaimReviewTask
- );
- }
-
- getClaimReviewTaskByDataHash(data_hash: string) {
- const commentPopulation = [
- {
- path: "user",
- select: "name",
- },
- {
- path: "replies",
- populate: {
- path: "user",
- select: "name",
- },
- },
- ];
-
- return this.ClaimReviewTaskModel.findOne({ data_hash })
- .populate({
- path: "machine.context.reviewData.reviewComments",
- model: "Comment",
- populate: commentPopulation,
- })
- .populate({
- path: "machine.context.reviewData.crossCheckingComments",
- model: "Comment",
- populate: commentPopulation,
- });
- }
-
- async getReviewTasksByClaimId(claimId: string) {
- return await this.ClaimReviewTaskModel.aggregate([
- {
- $match: {
- "machine.context.claimReview.claim": claimId.toString(),
- "machine.value": { $ne: "published" },
- },
- },
- ]);
- }
-
- async getClaimReviewTaskByDataHashWithUsernames(data_hash: string) {
- // This may cause a false positive in sonarCloud
- const claimReviewTask = await this.getClaimReviewTaskByDataHash(
- data_hash
- )
- .populate({
- path: "machine.context.reviewData.usersId",
- model: "User",
- select: "name",
- })
- .populate({
- path: "machine.context.reviewData.crossCheckerId",
- model: "User",
- select: "name",
- })
- .populate({
- path: "machine.context.reviewData.reviewerId",
- model: "User",
- select: "name",
- });
-
- if (claimReviewTask) {
- const preloadedAsignees = [];
- const usersId = [];
- claimReviewTask.machine.context.reviewData.usersId.forEach(
- (assignee) => {
- preloadedAsignees.push({
- value: assignee._id,
- label: assignee.name,
- });
- usersId.push(assignee._id);
- }
- );
- claimReviewTask.machine.context.reviewData.usersId = usersId;
- claimReviewTask.machine.context.preloadedOptions = {
- usersId: preloadedAsignees,
- };
-
- if (claimReviewTask.machine.context.reviewData.crossCheckerId) {
- const crossCheckerUser =
- claimReviewTask.machine.context.reviewData.crossCheckerId;
- claimReviewTask.machine.context.preloadedOptions.crossCheckerId =
- [
- {
- value: crossCheckerUser._id,
- label: crossCheckerUser.name,
- },
- ];
- claimReviewTask.machine.context.reviewData.crossCheckerId =
- crossCheckerUser._id;
- }
-
- if (claimReviewTask.machine.context.reviewData.reviewerId) {
- const reviewerUser =
- claimReviewTask.machine.context.reviewData.reviewerId;
- claimReviewTask.machine.context.preloadedOptions.reviewerId = [
- {
- value: reviewerUser._id,
- label: reviewerUser.name,
- },
- ];
- claimReviewTask.machine.context.reviewData.reviewerId =
- reviewerUser._id;
- }
- }
-
- return claimReviewTask;
- }
-
- count(query: any = {}) {
- return this.ClaimReviewTaskModel.countDocuments().where(query);
- }
-
- async countReviewTasksNotDeleted(query, filterUser, nameSpace) {
- try {
- const fieldMap = {
- assigned: "machine.context.reviewData.usersId",
- crossChecked: "machine.context.reviewData.crossCheckerId",
- reviewed: "machine.context.reviewData.reviewerId",
- };
-
- Object.keys(filterUser).forEach((key) => {
- const value = filterUser[key];
- if (value === true || value === "true") {
- const queryPath = fieldMap[key];
- query[queryPath] = Types.ObjectId(this.req.user._id);
- }
- });
-
- const pipeline = [
- { $match: query },
- lookupClaimReviews({
- as: "machine.context.claimReview.claimReview",
- }),
- lookupClaims(TargetModel.ClaimReviewTask, {
- pipeline: [
- { $match: { $expr: { $eq: ["$_id", "$$claimId"] } } },
- { $project: { isDeleted: 1, nameSpace: 1 } },
- ],
- as: "machine.context.claimReview.claim",
- }),
- {
- $match: {
- "machine.context.claimReview.claim.nameSpace":
- nameSpace,
- "machine.context.claimReview.claimReview.isDeleted": {
- $ne: true,
- },
- "machine.context.claimReview.claim.isDeleted": {
- $ne: true,
- },
- },
- },
- { $count: "count" },
- ];
-
- const result = await this.ClaimReviewTaskModel.aggregate(
- pipeline
- ).exec();
-
- if (result.length > 0) {
- return result[0].count;
- }
-
- return 0;
- } catch (error) {
- console.error("Error in countReviewTasksNotDeleted:", error);
- throw error;
- }
- }
-
- getEditorContentObject(schema, reportModel) {
- return this.editorParseService.schema2editor(schema, reportModel);
- }
-
- async addComment(data_hash, comment) {
- const claimReviewTask = await this.getClaimReviewTaskByDataHash(
- data_hash
- );
- const reviewData = claimReviewTask.machine.context.reviewData;
- const newComment = await this.commentService.create({
- ...comment,
- targetId: claimReviewTask._id,
- });
-
- if (!reviewData.reviewComments) {
- reviewData.reviewComments = [];
- }
-
- reviewData.reviewComments.push(Types.ObjectId(newComment?._id));
-
- const { machine } = await this.ClaimReviewTaskModel.findOneAndUpdate(
- { _id: claimReviewTask._id },
- { "machine.context.reviewData": reviewData },
- { new: true }
- );
-
- return {
- reviewData: machine.context.reviewData,
- comment: newComment,
- };
- }
-
- async deleteComment(data_hash, commentId) {
- const commentIdObject = Types.ObjectId(commentId);
- const claimReviewTask = await this.getClaimReviewTaskByDataHash(
- data_hash
- );
- const reviewData = claimReviewTask.machine.context.reviewData;
- reviewData.reviewComments = reviewData.reviewComments.filter(
- (comment) => !comment._id.equals(commentIdObject)
- );
- reviewData.reviewComments = reviewData.crossCheckingComments.filter(
- (comment) => !comment._id.equals(commentIdObject)
- );
-
- return this.ClaimReviewTaskModel.findByIdAndUpdate(
- claimReviewTask._id,
- { "machine.context.reviewData": reviewData }
- );
- }
-
- async getHtmlFromSchema(schema) {
- const htmlContent = this.editorParseService.schema2html(schema);
- return {
- ...schema,
- ...htmlContent,
- };
- }
-}
diff --git a/server/claim-review-task/dto/update-claim-review-task.dto.ts b/server/claim-review-task/dto/update-claim-review-task.dto.ts
deleted file mode 100644
index 984b8b46f..000000000
--- a/server/claim-review-task/dto/update-claim-review-task.dto.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { PartialType } from "@nestjs/mapped-types";
-import { CreateClaimReviewTaskDTO } from "./create-claim-review-task.dto";
-
-export class UpdateClaimReviewTaskDTO extends PartialType(
- CreateClaimReviewTaskDTO
-) {}
diff --git a/server/claim-review-task/schemas/claim-review-task.schema.ts b/server/claim-review-task/schemas/claim-review-task.schema.ts
deleted file mode 100644
index 7d14c97ba..000000000
--- a/server/claim-review-task/schemas/claim-review-task.schema.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as mongoose from "mongoose";
-import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
-import type { Machine } from "../dto/create-claim-review-task.dto";
-import { ReportModelEnum } from "../../types/enums";
-
-export type ClaimReviewTaskDocument = ClaimReviewTask & mongoose.Document;
-
-@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } })
-export class ClaimReviewTask {
- @Prop({ type: Object, required: true })
- machine: Machine;
-
- @Prop({ unique: true, required: true })
- data_hash: string;
-
- @Prop({
- required: true,
- validate: {
- validator: (v) => {
- return Object.values(ReportModelEnum).includes(v);
- },
- },
- message: (tag) => `${tag} is not a valid report type.`,
- })
- reportModel: ReportModelEnum;
-}
-
-export const ClaimReviewTaskSchema =
- SchemaFactory.createForClass(ClaimReviewTask);
diff --git a/server/claim-review/claim-review.controller.ts b/server/claim-review/claim-review.controller.ts
index a7fd88e9b..3bd5a836d 100644
--- a/server/claim-review/claim-review.controller.ts
+++ b/server/claim-review/claim-review.controller.ts
@@ -46,13 +46,13 @@ export class ClaimReviewController {
} = getClaimReviewsDto;
return Promise.all([
- this.claimReviewService.listAll(
+ this.claimReviewService.listAll({
page,
pageSize,
order,
- { isHidden, isDeleted: false, nameSpace },
- latest
- ),
+ query: { isHidden, isDeleted: false, nameSpace },
+ latest,
+ }),
this.claimReviewService.count({ isHidden, isDeleted: false }),
]).then(([reviews, totalReviews]) => {
const totalPages = Math.ceil(totalReviews / pageSize);
diff --git a/server/claim-review/claim-review.module.ts b/server/claim-review/claim-review.module.ts
index 716767553..3df980cc4 100644
--- a/server/claim-review/claim-review.module.ts
+++ b/server/claim-review/claim-review.module.ts
@@ -1,10 +1,9 @@
-import { Module } from "@nestjs/common";
+import { Module, forwardRef } from "@nestjs/common";
import { ClaimReview, ClaimReviewSchema } from "./schemas/claim-review.schema";
import { ClaimReviewService } from "./claim-review.service";
import { MongooseModule } from "@nestjs/mongoose";
import { ClaimReviewController } from "./claim-review.controller";
import { UtilService } from "../util";
-import { SourceModule } from "../source/source.module";
import { ConfigModule } from "@nestjs/config";
import { HistoryModule } from "../history/history.module";
import { SentenceModule } from "../claim/types/sentence/sentence.module";
@@ -12,6 +11,7 @@ import { CaptchaModule } from "../captcha/captcha.module";
import { AbilityModule } from "../auth/ability/ability.module";
import { ImageModule } from "../claim/types/image/image.module";
import { EditorParseModule } from "../editor-parse/editor-parse.module";
+import { WikidataModule } from "../wikidata/wikidata.module";
export const ClaimReviewModel = MongooseModule.forFeature([
{
@@ -24,13 +24,13 @@ export const ClaimReviewModel = MongooseModule.forFeature([
imports: [
ClaimReviewModel,
HistoryModule,
- SourceModule,
ConfigModule,
- SentenceModule,
+ forwardRef(() => SentenceModule),
CaptchaModule,
AbilityModule,
ImageModule,
EditorParseModule,
+ WikidataModule,
],
providers: [UtilService, ClaimReviewService],
exports: [ClaimReviewService],
diff --git a/server/claim-review/claim-review.service.ts b/server/claim-review/claim-review.service.ts
index c8d54bb38..5eca3f3ef 100644
--- a/server/claim-review/claim-review.service.ts
+++ b/server/claim-review/claim-review.service.ts
@@ -14,11 +14,10 @@ import { SentenceService } from "../claim/types/sentence/sentence.service";
import { REQUEST } from "@nestjs/core";
import type { BaseRequest } from "../types";
import { ImageService } from "../claim/types/image/image.service";
-import { ContentModelEnum } from "../types/enums";
-import lookUpPersonalityties from "../mongo-pipelines/lookUpPersonalityties";
-import lookupClaims from "../mongo-pipelines/lookupClaims";
+import { ContentModelEnum, ReviewTaskTypeEnum } from "../types/enums";
import { NameSpaceEnum } from "../auth/name-space/schemas/name-space.schema";
import { EditorParseService } from "../editor-parse/editor-parse.service";
+import { WikidataService } from "../wikidata/wikidata.service";
@Injectable({ scope: Scope.REQUEST })
export class ClaimReviewService {
@@ -31,14 +30,19 @@ export class ClaimReviewService {
private util: UtilService,
private sentenceService: SentenceService,
private imageService: ImageService,
- private editorParseService: EditorParseService
+ private editorParseService: EditorParseService,
+ private wikidata: WikidataService
) {}
- async listAll(page, pageSize, order, query, latest = false) {
- const pipeline = this.ClaimReviewModel.find(query)
+ async listAll({ page, pageSize, order, query, latest = false }) {
+ // Currently only list claim reviews
+ const pipeline = this.ClaimReviewModel.find({
+ ...query,
+ targetModel: ReviewTaskTypeEnum.Claim,
+ })
.sort(latest ? { date: -1 } : { _id: order === "asc" ? 1 : -1 })
.populate({
- path: "claim",
+ path: "target",
model: "Claim",
populate: {
path: "latestRevision",
@@ -78,10 +82,13 @@ export class ClaimReviewService {
}
async listDailyClaimReviews(query) {
- const pipeline = this.ClaimReviewModel.find(query)
+ const pipeline = this.ClaimReviewModel.find({
+ ...query,
+ targetModel: ReviewTaskTypeEnum.Claim,
+ })
.sort({ _id: 1 })
.populate({
- path: "claim",
+ path: "target",
model: "Claim",
populate: {
path: "latestRevision",
@@ -127,7 +134,7 @@ export class ClaimReviewService {
async agreggateClassification(match: any) {
const claimReviews = await this.ClaimReviewModel.find(match).populate({
- path: "claim",
+ path: "target",
model: "Claim",
match: {
"claim.isHidden": false,
@@ -143,14 +150,28 @@ export class ClaimReviewService {
aggregation.push(
{ $match: query },
- lookUpPersonalityties(TargetModel.ClaimReview),
- lookupClaims(TargetModel.ClaimReview),
- { $unwind: "$claim" },
+ {
+ $lookup: {
+ from: "personalities",
+ localField: "personality",
+ foreignField: "_id",
+ as: "personality",
+ },
+ },
+ {
+ $lookup: {
+ from: "claims",
+ localField: "target",
+ foreignField: "_id",
+ as: "target",
+ },
+ },
+ { $unwind: "$target" },
{
$match: {
"personality.isDeleted": false,
- "claim.isHidden": query.isHidden,
- "claim.isDeleted": false,
+ "target.isHidden": query.isHidden,
+ "target.isDeleted": false,
},
}
);
@@ -174,7 +195,7 @@ export class ClaimReviewService {
async getReviewStatsByClaimId(claimId) {
const reviews = await this.ClaimReviewModel.find({
- claim: claimId,
+ target: claimId,
isDeleted: false,
isPublished: true,
isHidden: false,
@@ -192,7 +213,7 @@ export class ClaimReviewService {
async getReviewsByClaimId(claimId) {
const classificationCounts = {};
const claimReviews = await this.ClaimReviewModel.find({
- claim: claimId,
+ target: claimId,
isDeleted: false,
isPublished: true,
isHidden: false,
@@ -220,44 +241,41 @@ export class ClaimReviewService {
* This function creates a new claim review.
* Also creates a History Module that tracks creation of claim reviews.
* @param claimReview ClaimReviewBody received of the client.
+ * @param data_hash unique claim review task hash
+ * @param reportModel FactChecking or InformativeNews
* @returns Return a new claim review object.
*/
async create(claimReview, data_hash, reportModel) {
- // This line may cause a false positive in sonarCloud because if we remove the await, we cannot iterate through the results
- const review = await this.getReviewByDataHash(data_hash);
-
- if (review) {
- throw new Error("This Claim already has a review");
- //TODO: verify if already start a review and isn't published
- } else {
- // Cast ObjectId
- claimReview.personality = claimReview.personality
- ? Types.ObjectId(claimReview.personality)
- : null;
- claimReview.claim = Types.ObjectId(claimReview.claim);
- claimReview.usersId = claimReview.report.usersId.map((userId) => {
- return Types.ObjectId(userId);
- });
- claimReview.report = Types.ObjectId(claimReview.report._id);
- claimReview.data_hash = data_hash;
- claimReview.reportModel = reportModel;
- claimReview.date = new Date();
- const newClaimReview = new this.ClaimReviewModel(claimReview);
- newClaimReview.isPublished = true;
- newClaimReview.isPartialReview = claimReview.isPartialReview;
-
- const history = this.historyService.getHistoryParams(
- newClaimReview._id,
- TargetModel.ClaimReview,
- claimReview.usersId,
- HistoryType.Create,
- newClaimReview
- );
-
- this.historyService.createHistory(history);
+ if (claimReview.personality) {
+ claimReview.personality = Types.ObjectId(claimReview.personality);
+ }
- return newClaimReview.save();
+ if (claimReview.target) {
+ claimReview.target = Types.ObjectId(claimReview.target);
}
+
+ claimReview.usersId = claimReview.report.usersId.map((userId) => {
+ return Types.ObjectId(userId);
+ });
+ claimReview.report = Types.ObjectId(claimReview.report._id);
+ claimReview.data_hash = data_hash;
+ claimReview.reportModel = reportModel;
+ claimReview.date = new Date();
+ const newClaimReview = new this.ClaimReviewModel(claimReview);
+ newClaimReview.isPublished = true;
+ newClaimReview.isPartialReview = claimReview.isPartialReview;
+
+ const history = this.historyService.getHistoryParams(
+ newClaimReview._id,
+ TargetModel.ClaimReview,
+ claimReview.usersId,
+ HistoryType.Create,
+ newClaimReview
+ );
+
+ this.historyService.createHistory(history);
+
+ return newClaimReview.save();
}
getById(claimReviewId) {
@@ -340,14 +358,20 @@ export class ClaimReviewService {
}
private async postProcess(review) {
- const { personality, data_hash, report } = review;
+ const { data_hash, report } = review;
+ const personality = await this.personalityPostProcess(
+ review.personality
+ );
+
const nameSpace =
this.req.params.namespace ||
this.req.query.nameSpace ||
NameSpaceEnum.Main;
const claim = {
- contentModel: review.claim.latestRevision.contentModel,
- date: review.claim.latestRevision.date,
+ contentModel: review.target.latestRevision.contentModel,
+ date: review.target.latestRevision.date,
+ slug: review.target.latestRevision.slug,
+ title: review.target.latestRevision.title,
};
const isContentImage = claim.contentModel === ContentModelEnum.Image;
@@ -361,21 +385,21 @@ export class ClaimReviewService {
let reviewHref =
nameSpace !== NameSpaceEnum.Main
- ? `/${nameSpace}/claim/${review.claim.latestRevision.claimId}`
- : `/claim/${review.claim.latestRevision.claimId}`;
+ ? `/${nameSpace}/claim/${review.target.latestRevision.claimId}`
+ : `/claim/${review.target.latestRevision.claimId}`;
if (isContentInformativeNews) {
reviewHref =
nameSpace !== NameSpaceEnum.Main
- ? `/${nameSpace}/claim/${review.claim.slug}`
- : `/claim/${review.claim.slug}`;
+ ? `/${nameSpace}/claim/${review.target.slug}`
+ : `/claim/${review.target.slug}`;
}
if (personality) {
reviewHref =
nameSpace !== NameSpaceEnum.Main
- ? `/${nameSpace}/personality/${personality?.slug}/claim/${review.claim.slug}`
- : `/personality/${personality?.slug}/claim/${review.claim.slug}`;
+ ? `/${nameSpace}/personality/${personality?.slug}/claim/${review.target.slug}`
+ : `/personality/${personality?.slug}/claim/${review.target.slug}`;
}
reviewHref += isContentImage
@@ -384,8 +408,8 @@ export class ClaimReviewService {
if (isContentDebate) {
reviewHref =
nameSpace !== NameSpaceEnum.Main
- ? `/${nameSpace}/claim/${review.claim.latestRevision.claimId}/debate`
- : `/claim/${review.claim.latestRevision.claimId}/debate`;
+ ? `/${nameSpace}/claim/${review.target.latestRevision.claimId}/debate`
+ : `/claim/${review.target.latestRevision.claimId}/debate`;
}
return {
@@ -421,4 +445,23 @@ export class ClaimReviewService {
...htmlContent,
};
}
+
+ async personalityPostProcess(personality) {
+ if (personality) {
+ const wikidataExtract = await this.wikidata.fetchProperties({
+ wikidataId: personality.wikidata,
+ language: this.req.language || "en",
+ });
+
+ if (wikidataExtract.isAllowedProp === false) {
+ return;
+ }
+
+ return {
+ ...personality.toObject(),
+ ...wikidataExtract,
+ };
+ }
+ return personality;
+ }
}
diff --git a/server/claim-review/schemas/claim-review.schema.ts b/server/claim-review/schemas/claim-review.schema.ts
index f2155410b..db0cd6048 100644
--- a/server/claim-review/schemas/claim-review.schema.ts
+++ b/server/claim-review/schemas/claim-review.schema.ts
@@ -7,6 +7,7 @@ import type { ReportDocument } from "../../report/schemas/report.schema";
import { User } from "../../users/schemas/user.schema";
import { ReportModelEnum } from "../../types/enums";
import { NameSpaceEnum } from "../../auth/name-space/schemas/name-space.schema";
+import { Source } from "../../source/schemas/source.schema";
export type ClaimReviewDocument = ClaimReview & mongoose.Document;
@@ -14,17 +15,17 @@ export type ClaimReviewDocument = ClaimReview & mongoose.Document;
export class ClaimReview {
@Prop({
type: mongoose.Types.ObjectId,
- required: true,
- ref: "Claim",
+ required: false,
+ ref: "Personality",
})
- claim: Claim;
+ personality: Personality;
@Prop({
type: mongoose.Types.ObjectId,
required: false,
- ref: "Personality",
+ ref: "Source",
})
- personality: Personality;
+ target: mongoose.Types.ObjectId;
@Prop({
type: mongoose.Types.ObjectId,
@@ -68,6 +69,9 @@ export class ClaimReview {
@Prop({ default: NameSpaceEnum.Main, required: true })
nameSpace: string;
+
+ @Prop({ required: true, type: String })
+ targetModel: string;
}
const ClaimReviewSchemaRaw = SchemaFactory.createForClass(ClaimReview);
diff --git a/server/claim/claim-revision/claim-revision.service.ts b/server/claim/claim-revision/claim-revision.service.ts
index 61fbb6467..fc76763f3 100644
--- a/server/claim/claim-revision/claim-revision.service.ts
+++ b/server/claim/claim-revision/claim-revision.service.ts
@@ -1,6 +1,6 @@
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
-import { Model } from "mongoose";
+import { Model, Types } from "mongoose";
import slugify from "slugify";
import { ParserService } from "../parser/parser.service";
import { SourceService } from "../../source/source.service";
@@ -69,10 +69,14 @@ export class ClaimRevisionService {
lower: true, // convert to lower case, defaults to `false`
strict: true, // strip special characters except replacement, defaults to `false`
});
+ const newClaimRevision = new this.ClaimRevisionModel(claim);
+ const newclaimRevisionId = Types.ObjectId(newClaimRevision._id);
- claim.contentId = await this._createContentModel(claim);
+ newClaimRevision.contentId = await this._createContentModel(
+ claim,
+ newclaimRevisionId
+ );
- const newClaimRevision = new this.ClaimRevisionModel(claim);
await this._createSources(claim.sources, claimId);
return newClaimRevision.save();
}
@@ -91,7 +95,7 @@ export class ClaimRevisionService {
query: searchText,
path: "title",
fuzzy: {
- maxEdits: 2,
+ maxEdits: 1, // Using maxEdits: 1 to allow minor typos or spelling errors in search queries.
},
},
},
@@ -165,18 +169,30 @@ export class ClaimRevisionService {
return this.ClaimRevisionModel.findOne({ contentId });
}
- private async _createContentModel(claim) {
+ private async _createContentModel(claim, claimRevisionId) {
switch (claim.contentModel) {
case ContentModelEnum.Speech:
- return (await this.parserService.parse(claim.content))._id;
+ return (
+ await this.parserService.parse(
+ claim.content,
+ claimRevisionId
+ )
+ )._id;
case ContentModelEnum.Image:
- return (await this.imageService.create(claim.content))._id;
+ return (
+ await this.imageService.create(
+ claim.content,
+ claimRevisionId
+ )
+ )._id;
case ContentModelEnum.Debate:
- return (await this.debateService.create(claim))._id;
+ return (await this.debateService.create(claim, claimRevisionId))
+ ._id;
case ContentModelEnum.Unattributed:
return (
await this.parserService.parse(
claim.content,
+ claimRevisionId,
null,
claim.contentModel
)
diff --git a/server/claim/claim.controller.ts b/server/claim/claim.controller.ts
index 2ab76da44..cc7e402ce 100644
--- a/server/claim/claim.controller.ts
+++ b/server/claim/claim.controller.ts
@@ -12,7 +12,6 @@ import {
Req,
Res,
Header,
- Optional,
UseGuards,
} from "@nestjs/common";
import { ClaimReviewService } from "../claim-review/claim-review.service";
@@ -28,12 +27,11 @@ import { GetClaimsDTO } from "./dto/get-claims.dto";
import { UpdateClaimDTO } from "./dto/update-claim.dto";
import { IsPublic } from "../auth/decorators/is-public.decorator";
import { CaptchaService } from "../captcha/captcha.service";
-import { ClaimReviewTaskService } from "../claim-review-task/claim-review-task.service";
+import { ReviewTaskService } from "../review-task/review-task.service";
import { TargetModel } from "../history/schema/history.schema";
import { SentenceService } from "./types/sentence/sentence.service";
import type { BaseRequest } from "../types";
import slugify from "slugify";
-import { UnleashService } from "nestjs-unleash";
import { SentenceDocument } from "./types/sentence/schemas/sentence.schema";
import { ImageService } from "./types/image/image.service";
import { ImageDocument } from "./types/image/schemas/image.schema";
@@ -51,13 +49,17 @@ import { Roles } from "../auth/ability/ability.factory";
import { ApiTags } from "@nestjs/swagger";
import { HistoryService } from "../history/history.service";
import { NameSpaceEnum } from "../auth/name-space/schemas/name-space.schema";
+import { ClaimRevisionService } from "./claim-revision/claim-revision.service";
+import { FeatureFlagService } from "../feature-flag/feature-flag.service";
+import { Types } from "mongoose";
+import { GroupService } from "../group/group.service";
@Controller(":namespace?")
export class ClaimController {
private readonly logger = new Logger("ClaimController");
constructor(
private claimReviewService: ClaimReviewService,
- private claimReviewTaskService: ClaimReviewTaskService,
+ private reviewTaskService: ReviewTaskService,
private personalityService: PersonalityService,
private claimService: ClaimService,
private sentenceService: SentenceService,
@@ -69,7 +71,9 @@ export class ClaimController {
private editorService: EditorService,
private parserService: ParserService,
private historyService: HistoryService,
- @Optional() private readonly unleash: UnleashService
+ private claimRevisionService: ClaimRevisionService,
+ private featureFlagService: FeatureFlagService,
+ private groupService: GroupService
) {}
_verifyInputsQuery(query) {
@@ -220,9 +224,17 @@ export class ClaimController {
) {
const { content, personality, isLive } = updateClaimDebateDto;
let newSpeech;
+
+ const claimRevision = await this.claimRevisionService.getByContentId(
+ Types.ObjectId(debateId)
+ );
+
+ const claimRevisionId = Types.ObjectId(claimRevision._id);
+
if (content && personality) {
newSpeech = await this.parserService.parse(
updateClaimDebateDto.content,
+ claimRevisionId,
updateClaimDebateDto.personality
);
@@ -330,8 +342,8 @@ export class ClaimController {
) {
const hideDescriptions = {};
- const claimReviewTask =
- await this.claimReviewTaskService.getClaimReviewTaskByDataHashWithUsernames(
+ const reviewTask =
+ await this.reviewTaskService.getReviewTaskByDataHashWithUsernames(
data_hash
);
@@ -339,11 +351,14 @@ export class ClaimController {
data_hash
);
- const enableCollaborativeEditor = this.isEnableCollaborativeEditor();
- const enableCopilotChatBot = this.isEnableCopilotChatBot();
- const enableEditorAnnotations = this.isEnableEditorAnnotations();
+ const enableCollaborativeEditor =
+ this.featureFlagService.isEnableCollaborativeEditor();
+ const enableCopilotChatBot =
+ this.featureFlagService.isEnableCopilotChatBot();
+ const enableEditorAnnotations =
+ this.featureFlagService.isEnableEditorAnnotations();
const enableAddEditorSourcesWithoutSelecting =
- this.isEnableAddEditorSourcesWithoutSelecting();
+ this.featureFlagService.isEnableAddEditorSourcesWithoutSelecting();
hideDescriptions[TargetModel.Claim] =
await this.historyService.getDescriptionForHide(
@@ -367,7 +382,7 @@ export class ClaimController {
personality,
claim,
content,
- claimReviewTask,
+ reviewTask,
claimReview,
sitekey: this.configService.get("recaptcha_sitekey"),
hideDescriptions,
@@ -494,12 +509,16 @@ export class ClaimController {
@ApiTags("pages")
@Get("claim/create")
public async claimCreatePage(
- @Query() query: { personality?: string },
+ @Query() query: { personality?: string; verificationRequest?: string },
@Req() req: BaseRequest,
@Res() res: Response
) {
const parsedUrl = parse(req.url, true);
+ const verificationRequestGroup = query.verificationRequest
+ ? await this.groupService.getByContentId(query.verificationRequest)
+ : null;
+
const personality = query.personality
? await this.personalityService.getClaimsByPersonalitySlug(
{
@@ -518,6 +537,7 @@ export class ClaimController {
personality,
sitekey: this.configService.get("recaptcha_sitekey"),
nameSpace: req.params.namespace,
+ verificationRequestGroup,
})
);
}
@@ -551,11 +571,14 @@ export class ClaimController {
const { claimSlug, namespace = NameSpaceEnum.Main } = req.params;
const parsedUrl = parse(req.url, true);
const claim = await this.claimService.getByClaimSlug(claimSlug);
- const enableCollaborativeEditor = this.isEnableCollaborativeEditor();
- const enableCopilotChatBot = this.isEnableCopilotChatBot();
- const enableEditorAnnotations = this.isEnableEditorAnnotations();
+ const enableCollaborativeEditor =
+ this.featureFlagService.isEnableCollaborativeEditor();
+ const enableCopilotChatBot =
+ this.featureFlagService.isEnableCopilotChatBot();
+ const enableEditorAnnotations =
+ this.featureFlagService.isEnableEditorAnnotations();
const enableAddEditorSourcesWithoutSelecting =
- this.isEnableAddEditorSourcesWithoutSelecting();
+ this.featureFlagService.isEnableAddEditorSourcesWithoutSelecting();
this.redirectBasedOnPersonality(res, claim, namespace);
@@ -585,11 +608,14 @@ export class ClaimController {
const { claimSlug, revisionId, namespace } = req.params;
const parsedUrl = parse(req.url, true);
- const enableCollaborativeEditor = this.isEnableCollaborativeEditor();
- const enableCopilotChatBot = this.isEnableCopilotChatBot();
- const enableEditorAnnotations = this.isEnableEditorAnnotations();
+ const enableCollaborativeEditor =
+ this.featureFlagService.isEnableCollaborativeEditor();
+ const enableCopilotChatBot =
+ this.featureFlagService.isEnableCopilotChatBot();
+ const enableEditorAnnotations =
+ this.featureFlagService.isEnableEditorAnnotations();
const enableAddEditorSourcesWithoutSelecting =
- this.isEnableAddEditorSourcesWithoutSelecting();
+ this.featureFlagService.isEnableAddEditorSourcesWithoutSelecting();
const claim = await this.claimService.getByClaimSlug(
claimSlug,
@@ -625,11 +651,14 @@ export class ClaimController {
const { personalitySlug, claimSlug, namespace } = req.params;
const parsedUrl = parse(req.url, true);
- const enableCollaborativeEditor = this.isEnableCollaborativeEditor();
- const enableCopilotChatBot = this.isEnableCopilotChatBot();
- const enableEditorAnnotations = this.isEnableEditorAnnotations();
+ const enableCollaborativeEditor =
+ this.featureFlagService.isEnableCollaborativeEditor();
+ const enableCopilotChatBot =
+ this.featureFlagService.isEnableCopilotChatBot();
+ const enableEditorAnnotations =
+ this.featureFlagService.isEnableEditorAnnotations();
const enableAddEditorSourcesWithoutSelecting =
- this.isEnableAddEditorSourcesWithoutSelecting();
+ this.featureFlagService.isEnableAddEditorSourcesWithoutSelecting();
const personality =
await this.personalityService.getClaimsByPersonalitySlug(
@@ -688,11 +717,14 @@ export class ClaimController {
req.language
);
- const enableCollaborativeEditor = this.isEnableCollaborativeEditor();
- const enableCopilotChatBot = this.isEnableCopilotChatBot();
- const enableEditorAnnotations = this.isEnableEditorAnnotations();
+ const enableCollaborativeEditor =
+ this.featureFlagService.isEnableCollaborativeEditor();
+ const enableCopilotChatBot =
+ this.featureFlagService.isEnableCopilotChatBot();
+ const enableEditorAnnotations =
+ this.featureFlagService.isEnableEditorAnnotations();
const enableAddEditorSourcesWithoutSelecting =
- this.isEnableAddEditorSourcesWithoutSelecting();
+ this.featureFlagService.isEnableAddEditorSourcesWithoutSelecting();
const claim = await this.claimService.getByPersonalityIdAndClaimSlug(
personality._id,
@@ -878,18 +910,17 @@ export class ClaimController {
const { data_hash } = req.params;
const parsedUrl = parse(req.url, true);
- const claimReviewTask =
- await this.claimReviewTaskService.getClaimReviewTaskByDataHash(
- data_hash
- );
+ const reviewTask = await this.reviewTaskService.getReviewTaskByDataHash(
+ data_hash
+ );
await this.viewService.getNextServer().render(
req,
res,
"/history-page",
Object.assign(parsedUrl.query, {
- targetId: claimReviewTask._id,
- targetModel: TargetModel.ClaimReviewTask,
+ targetId: reviewTask._id,
+ targetModel: TargetModel.ReviewTask,
nameSpace: req.params.namespace,
})
);
@@ -921,38 +952,6 @@ export class ClaimController {
await this.returnClaimReviewPage(data_hash, req, res, claim, sentence);
}
- private isEnableCollaborativeEditor() {
- const config = this.configService.get("feature_flag");
-
- return config
- ? this.unleash.isEnabled("enable_collaborative_editor")
- : false;
- }
-
- private isEnableCopilotChatBot() {
- const config = this.configService.get("feature_flag");
-
- return config ? this.unleash.isEnabled("copilot_chat_bot") : false;
- }
-
- private isEnableEditorAnnotations() {
- const config = this.configService.get("feature_flag");
-
- return config
- ? this.unleash.isEnabled("enable_editor_annotations")
- : false;
- }
-
- private isEnableAddEditorSourcesWithoutSelecting() {
- const config = this.configService.get("feature_flag");
-
- return config
- ? this.unleash.isEnabled(
- "enable_add_editor_sources_without_selecting"
- )
- : false;
- }
-
private redirectBasedOnPersonality(
res,
claim,
diff --git a/server/claim/claim.module.ts b/server/claim/claim.module.ts
index 6cdd6c17d..62f2f0b07 100644
--- a/server/claim/claim.module.ts
+++ b/server/claim/claim.module.ts
@@ -8,11 +8,10 @@ import { ParserModule } from "./parser/parser.module";
import { PersonalityModule } from "../personality/personality.module";
import { ConfigModule } from "@nestjs/config";
import { ViewModule } from "../view/view.module";
-import { SourceModule } from "../source/source.module";
import { ClaimRevisionModule } from "./claim-revision/claim-revision.module";
import { HistoryModule } from "../history/history.module";
import { CaptchaModule } from "../captcha/captcha.module";
-import { ClaimReviewTaskModule } from "../claim-review-task/claim-review-task.module";
+import { ReviewTaskModule } from "../review-task/review-task.module";
import { SentenceModule } from "./types/sentence/sentence.module";
import { StateEventModule } from "../state-event/state-event.module";
import { ImageModule } from "./types/image/image.module";
@@ -20,6 +19,8 @@ import { DebateModule } from "./types/debate/debate.module";
import { EditorModule } from "../editor/editor.module";
import { AbilityModule } from "../auth/ability/ability.module";
import { UtilService } from "../util";
+import { FeatureFlagModule } from "../feature-flag/feature-flag.module";
+import { GroupModule } from "../group/group.module";
const ClaimModel = MongooseModule.forFeature([
{
@@ -32,7 +33,7 @@ const ClaimModel = MongooseModule.forFeature([
imports: [
ClaimModel,
ClaimReviewModule,
- ClaimReviewTaskModule,
+ ReviewTaskModule,
ClaimRevisionModule,
SentenceModule,
ParserModule,
@@ -41,12 +42,13 @@ const ClaimModel = MongooseModule.forFeature([
StateEventModule,
ConfigModule,
ViewModule,
- SourceModule,
CaptchaModule,
ImageModule,
DebateModule,
EditorModule,
AbilityModule,
+ FeatureFlagModule,
+ GroupModule,
],
exports: [ClaimService],
providers: [UtilService, ClaimService],
diff --git a/server/claim/claim.service.ts b/server/claim/claim.service.ts
index 125fe1e80..115740102 100644
--- a/server/claim/claim.service.ts
+++ b/server/claim/claim.service.ts
@@ -12,9 +12,10 @@ import { ISoftDeletedModel } from "mongoose-softdelete-typescript";
import { REQUEST } from "@nestjs/core";
import type { BaseRequest } from "../types";
import { ContentModelEnum } from "../types/enums";
-import { ClaimReviewTaskService } from "../claim-review-task/claim-review-task.service";
+import { ReviewTaskService } from "../review-task/review-task.service";
import { UtilService } from "../util";
import { NameSpaceEnum } from "../auth/name-space/schemas/name-space.schema";
+import { GroupService } from "../group/group.service";
type ClaimMatchParameters = (
| { _id: string; isHidden?: boolean; nameSpace?: string }
@@ -38,8 +39,9 @@ export class ClaimService {
private historyService: HistoryService,
private stateEventService: StateEventService,
private claimRevisionService: ClaimRevisionService,
- private claimReviewTaskService: ClaimReviewTaskService,
- private util: UtilService
+ private reviewTaskService: ReviewTaskService,
+ private util: UtilService,
+ private groupService: GroupService
) {}
async listAll(page, pageSize, order, query) {
@@ -126,6 +128,10 @@ export class ClaimService {
return Types.ObjectId(personality);
});
+ if (claim.group) {
+ claim.group = Types.ObjectId(claim.group);
+ }
+
const newClaim = new this.ClaimModel(claim);
const newClaimRevision = await this.claimRevisionService.create(
newClaim._id,
@@ -150,6 +156,9 @@ export class ClaimService {
this.historyService.createHistory(history);
this.stateEventService.createStateEvent(stateEvent);
+ if (claim.group) {
+ this.groupService.updateWithTargetId(claim.group, newClaim._id);
+ }
newClaim.save();
return {
@@ -383,10 +392,8 @@ export class ClaimService {
const reviews = await this.claimReviewService.getReviewsByClaimId(
claim._id
);
- const claimReviewTasks =
- await this.claimReviewTaskService.getReviewTasksByClaimId(
- claim._id
- );
+ const reviewTasks =
+ await this.reviewTaskService.getReviewTasksByClaimId(claim._id);
processedClaim.content = this.getClaimContent(processedClaim);
@@ -397,7 +404,7 @@ export class ClaimService {
const content = this.transformContentObject(
speech.content,
reviews,
- claimReviewTasks
+ reviewTasks
);
return { ...speech, content };
});
@@ -405,7 +412,7 @@ export class ClaimService {
processedClaim.content = this.transformContentObject(
processedClaim.content,
reviews,
- claimReviewTasks
+ reviewTasks
);
}
}
@@ -447,11 +454,8 @@ export class ClaimService {
};
}
- private transformContentObject(claimContent, reviews, claimReviewTasks) {
- if (
- !claimContent ||
- (reviews.length <= 0 && claimReviewTasks.length <= 0)
- ) {
+ private transformContentObject(claimContent, reviews, reviewTasks) {
+ if (!claimContent || (reviews.length <= 0 && reviewTasks.length <= 0)) {
return claimContent;
}
@@ -490,11 +494,11 @@ export class ClaimService {
);
}
- const claimReviewTask = claimReviewTasks.find(
+ const reviewTask = reviewTasks.find(
(task) => task?.data_hash === sentence.data_hash
);
- if (claimReviewTask) {
+ if (reviewTask) {
return processReview(sentence, "in-progress");
}
diff --git a/server/claim/dto/create-claim.dto.ts b/server/claim/dto/create-claim.dto.ts
index 4674fe740..c8a90402a 100644
--- a/server/claim/dto/create-claim.dto.ts
+++ b/server/claim/dto/create-claim.dto.ts
@@ -10,6 +10,7 @@ import {
import { ContentModelEnum } from "../../types/enums";
import { Personality } from "../../personality/schemas/personality.schema";
import { ApiProperty } from "@nestjs/swagger";
+import { Group } from "../../group/schemas/group.schema";
export class CreateClaimDTO {
@IsNotEmpty()
@@ -52,4 +53,9 @@ export class CreateClaimDTO {
@IsString()
@ApiProperty()
nameSpace: string;
+
+ @IsString()
+ @IsOptional()
+ @ApiProperty()
+ group: Group;
}
diff --git a/server/claim/parser/parser.service.spec.ts b/server/claim/parser/parser.service.spec.ts
index be0cfbee6..5d6b40694 100644
--- a/server/claim/parser/parser.service.spec.ts
+++ b/server/claim/parser/parser.service.spec.ts
@@ -1,37 +1,26 @@
import { Test, TestingModule } from "@nestjs/testing";
import { ParserService } from "./parser.service";
import * as fs from "fs";
-import { SpeechModule } from "../types/speech/speech.module";
-import { ParagraphModule } from "../types/paragraph/paragraph.module";
-import { SentenceModule } from "../types/sentence/sentence.module";
-import { MongooseModule } from "@nestjs/mongoose";
import { TestConfigOptions } from "../../tests/utils/TestConfigOptions";
import { MongoMemoryServer } from "mongodb-memory-server";
-import { UnattributedModule } from "../types/unattributed/unattributed.module";
+import { Types } from "mongoose";
+import { AppModule } from "../../app.module";
describe("ParserService", () => {
let parserService: ParserService;
let db: any;
+ const claimRevisionIdMock = new Types.ObjectId("66684a2a763175559d119818");
beforeAll(async () => {
db = await MongoMemoryServer.create({ instance: { port: 35025 } });
});
beforeEach(async () => {
- const testingModule: TestingModule = await Test.createTestingModule({
- imports: [
- MongooseModule.forRoot(
- TestConfigOptions.config.db.connection_uri,
- TestConfigOptions.config.db.options
- ),
- SpeechModule,
- ParagraphModule,
- SentenceModule,
- UnattributedModule,
- ],
- providers: [ParserService],
+ const moduleFixture: TestingModule = await Test.createTestingModule({
+ imports: [AppModule.register(TestConfigOptions.config)],
}).compile();
- parserService = testingModule.get(ParserService);
+
+ parserService = moduleFixture.get(ParserService);
});
describe("parse()", () => {
@@ -40,7 +29,7 @@ describe("ParserService", () => {
"Pellentesque auctor neque nec urna. Nulla facilisi. Praesent nec nisl a purus blandit viverra." +
"\n\nNam at tortor in tellus interdum sagittis. Ut leo. Praesent adipiscing. Curabitur nisi.";
const parseOutput = await (
- await parserService.parse(claimText)
+ await parserService.parse(claimText, claimRevisionIdMock)
)
.populate({
path: "content",
@@ -64,7 +53,7 @@ describe("ParserService", () => {
);
const parseOutput = (
await (
- await parserService.parse(claimText)
+ await parserService.parse(claimText, claimRevisionIdMock)
)
.populate({
path: "content",
@@ -83,7 +72,7 @@ describe("ParserService", () => {
const claimText = "Nulla facilisi.\n\nUt leo.";
const parseOutput = (
await (
- await parserService.parse(claimText)
+ await parserService.parse(claimText, claimRevisionIdMock)
)
.populate({
path: "content",
@@ -102,7 +91,7 @@ describe("ParserService", () => {
it("Ph.D word is not confused with end of sentence", async () => {
const claimText = "Jose is Ph.D. and Maria is a Ph.D.";
const parseOutput = await (
- await parserService.parse(claimText)
+ await parserService.parse(claimText, claimRevisionIdMock)
)
.populate({
path: "content",
@@ -120,7 +109,7 @@ describe("ParserService", () => {
const claimText =
"Mr. Jose and Mrs. Maria lives in St. Monica with Ms. Butterfly their Dr. of the year";
const parseOutput = await (
- await parserService.parse(claimText)
+ await parserService.parse(claimText, claimRevisionIdMock)
)
.populate({
path: "content",
diff --git a/server/claim/parser/parser.service.ts b/server/claim/parser/parser.service.ts
index 0becbc9d8..fa91b83eb 100644
--- a/server/claim/parser/parser.service.ts
+++ b/server/claim/parser/parser.service.ts
@@ -26,6 +26,7 @@ export class ParserService {
async parse(
content: string,
+ claimRevisionId: object,
personality = null,
contentModel = ContentModelEnum.Speech
): Promise {
@@ -51,8 +52,13 @@ export class ParserService {
props: {
id: paragraphId,
},
+ claimRevisionId: claimRevisionId,
content: sentences.map((sentence) =>
- this.parseSentence(sentence, paragraphDataHash)
+ this.parseSentence(
+ sentence,
+ paragraphDataHash,
+ claimRevisionId
+ )
),
})
);
@@ -73,6 +79,7 @@ export class ParserService {
return this.speechService.create({
content: object,
+ claimRevisionId: claimRevisionId,
personality,
});
}
@@ -99,7 +106,7 @@ export class ParserService {
return newSentences;
}
- parseSentence(sentenceContent, paragraphDataHash) {
+ parseSentence(sentenceContent, paragraphDataHash, claimRevisionId) {
const sentenceId = this.createSentenceId();
const sentenceDataHash = md5(
`${paragraphDataHash}${this.sentenceSequence}${sentenceContent}`
@@ -111,6 +118,7 @@ export class ParserService {
id: sentenceId,
},
content: sentenceContent,
+ claimRevisionId: claimRevisionId,
});
}
diff --git a/server/claim/schemas/claim.schema.ts b/server/claim/schemas/claim.schema.ts
index a722f053c..33f7586a3 100644
--- a/server/claim/schemas/claim.schema.ts
+++ b/server/claim/schemas/claim.schema.ts
@@ -4,6 +4,7 @@ import { Personality } from "../../personality/schemas/personality.schema";
import { ClaimRevision } from "../claim-revision/schema/claim-revision.schema";
import { softDeletePlugin } from "mongoose-softdelete-typescript";
import { NameSpaceEnum } from "../../auth/name-space/schemas/name-space.schema";
+import { Group } from "../../group/schemas/group.schema";
export type ClaimDocument = Claim & mongoose.Document & { revisions: any };
@@ -35,6 +36,13 @@ export class Claim {
@Prop({ default: NameSpaceEnum.Main, required: true })
nameSpace: string;
+
+ @Prop({
+ type: mongoose.Types.ObjectId,
+ required: false,
+ ref: "Group",
+ })
+ group: Group;
}
const ClaimSchemaRaw = SchemaFactory.createForClass(Claim);
diff --git a/server/claim/types/README.md b/server/claim/types/README.md
index cb7486d6e..c2f824798 100644
--- a/server/claim/types/README.md
+++ b/server/claim/types/README.md
@@ -28,6 +28,10 @@ interface ClaimSpec {
* Any structed data that allows the platform to retrieve a kind of media
*/
content: any;
+ /**
+ * The unique identifier of the claim revision.
+ */
+ claimRevisionId: objectId;
}
```
diff --git a/server/claim/types/debate/debate.service.ts b/server/claim/types/debate/debate.service.ts
index f8c0c1e42..3bfeff82d 100644
--- a/server/claim/types/debate/debate.service.ts
+++ b/server/claim/types/debate/debate.service.ts
@@ -32,7 +32,7 @@ export class DebateService {
.lean();
}
- async create(claim) {
+ async create(claim, claimRevisionId) {
let hashString = claim.personalities.join(" ");
hashString += ` ${claim.title} ${claim.date.toString()}`;
const data_hash = md5(hashString);
@@ -40,6 +40,7 @@ export class DebateService {
content: [],
isLive: false,
data_hash,
+ claimRevisionId: claimRevisionId,
};
const debateCreated = await this.DebateModel.create(debate);
await this.editorService.create(debateCreated._id);
diff --git a/server/claim/types/debate/schemas/debate.schema.ts b/server/claim/types/debate/schemas/debate.schema.ts
index 7bfe37ac9..d7ab67248 100644
--- a/server/claim/types/debate/schemas/debate.schema.ts
+++ b/server/claim/types/debate/schemas/debate.schema.ts
@@ -1,6 +1,7 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Speech } from "../../speech/schemas/speech.schema";
import * as mongoose from "mongoose";
+import { ClaimRevision } from "../../../../claim/claim-revision/schema/claim-revision.schema";
export type DebateDocument = Debate & mongoose.Document;
@@ -35,6 +36,13 @@ export class Debate {
@Prop({ type: Date })
updatedAt: Date;
+
+ @Prop({
+ type: mongoose.Types.ObjectId,
+ required: true,
+ ref: "ClaimRevision",
+ })
+ claimRevisionId: ClaimRevision;
}
const DebateSchemaRaw = SchemaFactory.createForClass(Debate);
diff --git a/server/claim/types/image/image.module.ts b/server/claim/types/image/image.module.ts
index e2fd61b0c..7323020d2 100644
--- a/server/claim/types/image/image.module.ts
+++ b/server/claim/types/image/image.module.ts
@@ -1,4 +1,4 @@
-import { Module } from "@nestjs/common";
+import { Module, forwardRef } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { ReportModule } from "../../../report/report.module";
import { HistoryModule } from "../../../history/history.module";
@@ -14,7 +14,7 @@ const ImageModel = MongooseModule.forFeature([
]);
@Module({
- imports: [ImageModel, HistoryModule, ReportModule],
+ imports: [ImageModel, HistoryModule, forwardRef(() => ReportModule)],
controllers: [ImageController],
providers: [ImageService],
exports: [ImageService],
diff --git a/server/claim/types/image/image.service.ts b/server/claim/types/image/image.service.ts
index 37607a855..000b5ab24 100644
--- a/server/claim/types/image/image.service.ts
+++ b/server/claim/types/image/image.service.ts
@@ -21,7 +21,7 @@ export class ImageService {
private reportService: ReportService
) {}
- async create(image) {
+ async create(image, claimRevisionId = null) {
const imageSchema = {
data_hash: image.DataHash,
props: {
@@ -29,6 +29,7 @@ export class ImageService {
extension: image.Extension,
},
content: image.FileURL,
+ claimRevisionId: claimRevisionId,
};
const newImage = await new this.ImageModel(imageSchema).save();
diff --git a/server/claim/types/image/schemas/image.schema.ts b/server/claim/types/image/schemas/image.schema.ts
index 6beb6110a..3e58fee62 100644
--- a/server/claim/types/image/schemas/image.schema.ts
+++ b/server/claim/types/image/schemas/image.schema.ts
@@ -2,6 +2,7 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { ContentModelEnum } from "../../../../types/enums";
import * as mongoose from "mongoose";
import { Topic } from "../../../../topic/schemas/topic.schema";
+import { ClaimRevision } from "../../../../claim/claim-revision/schema/claim-revision.schema";
export type ImageDocument = Image & mongoose.Document;
@@ -24,6 +25,13 @@ export class Image {
@Prop({ required: false })
topics: Topic[];
+
+ @Prop({
+ type: mongoose.Types.ObjectId,
+ required: false,
+ ref: "ClaimRevision",
+ })
+ claimRevisionId: ClaimRevision;
}
const ImageSchemaRaw = SchemaFactory.createForClass(Image);
diff --git a/server/claim/types/paragraph/schemas/paragraph.schema.ts b/server/claim/types/paragraph/schemas/paragraph.schema.ts
index 3a7640987..72a6ff05f 100644
--- a/server/claim/types/paragraph/schemas/paragraph.schema.ts
+++ b/server/claim/types/paragraph/schemas/paragraph.schema.ts
@@ -1,6 +1,7 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import * as mongoose from "mongoose";
import { Sentence } from "../../sentence/schemas/sentence.schema";
+import { ClaimRevision } from "../../../../claim/claim-revision/schema/claim-revision.schema";
export type ParagraphDocument = Paragraph & mongoose.Document;
@@ -29,6 +30,13 @@ export class Paragraph {
],
})
content: Sentence[];
+
+ @Prop({
+ type: mongoose.Types.ObjectId,
+ required: true,
+ ref: "ClaimRevision",
+ })
+ claimRevisionId: ClaimRevision;
}
const ParagraphSchemaRaw = SchemaFactory.createForClass(Paragraph);
diff --git a/server/claim/types/sentence/schemas/sentence.schema.ts b/server/claim/types/sentence/schemas/sentence.schema.ts
index 0c70701f5..c2ab00505 100644
--- a/server/claim/types/sentence/schemas/sentence.schema.ts
+++ b/server/claim/types/sentence/schemas/sentence.schema.ts
@@ -1,6 +1,7 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import * as mongoose from "mongoose";
import { Topic } from "../../../../topic/schemas/topic.schema";
+import { ClaimRevision } from "../../../../claim/claim-revision/schema/claim-revision.schema";
export type SentenceDocument = Sentence & mongoose.Document;
@@ -24,6 +25,13 @@ export class Sentence {
@Prop({ required: false })
topics: Topic[];
+
+ @Prop({
+ type: mongoose.Types.ObjectId,
+ required: true,
+ ref: "ClaimRevision",
+ })
+ claimRevisionId: ClaimRevision;
}
const SentenceSchemaRaw = SchemaFactory.createForClass(Sentence);
diff --git a/server/claim/types/sentence/sentence.service.ts b/server/claim/types/sentence/sentence.service.ts
index 4dab43f67..207467173 100644
--- a/server/claim/types/sentence/sentence.service.ts
+++ b/server/claim/types/sentence/sentence.service.ts
@@ -69,7 +69,7 @@ export class SentenceService {
query: searchText,
path: "content",
fuzzy: {
- maxEdits: 2,
+ maxEdits: 1, // Using maxEdits: 1 to allow minor typos or spelling errors in search queries.
},
},
},
@@ -98,27 +98,11 @@ export class SentenceService {
}
pipeline.push(
- {
- $lookup: {
- from: "paragraphs",
- localField: "_id",
- foreignField: "content",
- as: "claim",
- },
- },
- {
- $lookup: {
- from: "speeches",
- localField: "claim._id",
- foreignField: "content",
- as: "claim",
- },
- },
{
$lookup: {
from: "claimrevisions",
- localField: "claim._id",
- foreignField: "contentId",
+ localField: "claimRevisionId",
+ foreignField: "_id",
as: "claim",
},
},
@@ -139,13 +123,6 @@ export class SentenceService {
},
},
this.util.getVisibilityMatch(nameSpace),
- // Logic made to filter sentences from debates
- //TODO: Remove this when claim schema is changed
- {
- $match: {
- claim: { $ne: [] },
- },
- },
{
$project: {
content: 1,
diff --git a/server/claim/types/speech/schemas/speech.schema.ts b/server/claim/types/speech/schemas/speech.schema.ts
index 3cba011cb..0d7577814 100644
--- a/server/claim/types/speech/schemas/speech.schema.ts
+++ b/server/claim/types/speech/schemas/speech.schema.ts
@@ -2,6 +2,7 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import * as mongoose from "mongoose";
import { Personality } from "../../../../personality/schemas/personality.schema";
import { Paragraph } from "../../paragraph/schemas/paragraph.schema";
+import { ClaimRevision } from "../../../../claim/claim-revision/schema/claim-revision.schema";
export type SpeechDocument = Speech & mongoose.Document;
@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } })
@@ -29,6 +30,13 @@ export class Speech {
ref: "personality",
})
personality: Personality;
+
+ @Prop({
+ type: mongoose.Types.ObjectId,
+ required: true,
+ ref: "ClaimRevision",
+ })
+ claimRevisionId: ClaimRevision;
}
const SpeechSchemaRaw = SchemaFactory.createForClass(Speech);
diff --git a/server/copilot/copilot-chat.service.ts b/server/copilot/copilot-chat.service.ts
index 564f05094..715f1781b 100644
--- a/server/copilot/copilot-chat.service.ts
+++ b/server/copilot/copilot-chat.service.ts
@@ -118,35 +118,32 @@ export class CopilotChatService {
];
const prompt = ChatPromptTemplate.fromMessages([
- //TODO: Ensure that the agent only fack-checks the claim from the context
[
"system",
`
- You are helpful assistant, your objective is to gather relevant informations from the user about the {claim} that has to be fact-checked.
+ You are the Fact-checker Aletheia's Assistant, working with a fact-checker who requires assistance.
+ Your primary goal is to gather all relevant information from the user about the claim: {claim} that needs to be fact-checked.
+
+ Please follow these steps carefully
- A fact-checker is interacting with you because he needs assistance with his fact-check report.
+ 1. Confirm the claim for fact-checking:
+ - If the user requests assistance with fact-checking, ask the user to confirm the claim that he wants to review is the claim: {claim} stated by {personality}, assure to always compose this specific question using these values {claim} and {personality}.
- Follow these steps carefully:
+ 2. Analyze the {claim}:
+ - If your analysis indicates that the claim pertains to Brazilian municipalities or states, ask the following questions sequentially:
+ - "In which Brazilian city or state was the claim made?"
+ - "Do you have a specific time period during which we should search in the public gazettes (e.g. January 2022 to December 2022), or should we search up to the date the claim was stated: {date}?"
- 1. Confirm with the user the claim to be fack-checked:
- - If the user requests assistance with the fact-check, ask the user to confirm the claim that he wants to review is the claim:{claim} stated by {personality}, assure to always compose this specific question using these values {claim} and {personality}.
+ - If the claim is unrelated to Brazilian municipalities or concerns a different topic, ask:
+ - "Do you have any specific sources you suggest we consult for verifying this claim?"
- 2. Analyze the {claim}:
- - Based on your analyze assure if the claim is related to Brazilian municipalities or states proceed with the following questions strictly one at a time:
- - Ask the user Which Brazilian city or state was the claim made in?
- - Ask: Do you have any time expecific time period we should search in the public gazettes? (e.g., January 2022 to December 2022), or we should search until statement of the claim date:{date}?
+ If any information provided is ambiguous or incomplete, request clarification from the user without making assumptions.
+ Always pose your questions one at a time and in the specified order.
- - Based on your analyze if the claim is unrelated to Brazilian municipalities or is a totally different topic proceed with the following question:
- - Ask the user if he has any suggestion of sources that we should consult?
-
- If any information is ambiguous or missing, request clarification from the user without making assumptions.
- Always ask one question at a time and in the specified order.
-
- You need to make all possible questions even if the user tries to rush the review.
- Compose your responses using formal language and you MUST provide you answer in {language}.
- Always pass the {claim} specifically to the tool without translating.
- Only when you have made all questions and followed all steps to extracted information from the user then proceed to use the get-fact-checking-report tool, Do not proceed until you have asked all the questions.
- `,
+ Persist in asking all necessary questions, even if the user attempts to expedite the process.
+ Do not advance to this tool until you have thoroughly completed all preceding steps.
+ Maintain the use of formal language in your responses, ensuring that all communication is conducted in {language}.
+ Only after all questions have been addressed and all relevant information has been gathered from the user should you proceed to use the get-fact-checking-report tool.`,
],
new MessagesPlaceholder({ variableName: "chat_history" }),
["user", "{input}"],
diff --git a/server/daily-report/daily-report.module.ts b/server/daily-report/daily-report.module.ts
index 6da0c6ec1..c501aadb5 100644
--- a/server/daily-report/daily-report.module.ts
+++ b/server/daily-report/daily-report.module.ts
@@ -3,7 +3,7 @@ import { DailyReport, DailyReportSchema } from "./schemas/daily-report.schema";
import { DailyReportService } from "./daily-report.service";
import { MongooseModule } from "@nestjs/mongoose";
import { AbilityModule } from "../auth/ability/ability.module";
-import { SummarizationModule } from "../summarization/summarization.module";
+import { SummarizationCrawlerModule } from "../summarization/summarization-crawler.module";
import { ClaimReviewModule } from "../claim-review/claim-review.module";
import { NotificationModule } from "../notifications/notifications.module";
import { DailyReportController } from "./daily-report.controller";
@@ -22,7 +22,7 @@ export const DailyReportModel = MongooseModule.forFeature([
DailyReportModel,
ClaimReviewModule,
SourceModule,
- SummarizationModule,
+ SummarizationCrawlerModule,
AbilityModule,
NotificationModule,
ConfigModule,
diff --git a/server/daily-report/daily-report.service.ts b/server/daily-report/daily-report.service.ts
index f39fb717f..fdedec69a 100644
--- a/server/daily-report/daily-report.service.ts
+++ b/server/daily-report/daily-report.service.ts
@@ -5,7 +5,7 @@ import {
DailyReportDocument,
} from "./schemas/daily-report.schema";
import { InjectModel } from "@nestjs/mongoose";
-import { SummarizationService } from "../summarization/summarization.service";
+import { SummarizationCrawlerService } from "../summarization/summarization-crawler.service";
@Injectable({ scope: Scope.REQUEST })
export class DailyReportService {
@@ -13,7 +13,7 @@ export class DailyReportService {
constructor(
@InjectModel(DailyReport.name)
private DailyReportModel: Model,
- private summarizationService: SummarizationService
+ private summarizationCrawlerService: SummarizationCrawlerService
) {}
async create(dailyReportBody: DailyReport): Promise {
@@ -32,11 +32,11 @@ export class DailyReportService {
): Promise {
try {
const summarizedReviews =
- await this.summarizationService.getSummarizedReviews(
+ await this.summarizationCrawlerService.getSummarizedReviews(
dailyReviews
);
- return this.summarizationService.generateHTMLReport(
+ return this.summarizationCrawlerService.generateHTMLReport(
summarizedReviews,
nameSpace
);
diff --git a/server/editor-parse/editor-parse.service.spec.ts b/server/editor-parse/editor-parse.service.spec.ts
index aea204a74..4600bda21 100644
--- a/server/editor-parse/editor-parse.service.spec.ts
+++ b/server/editor-parse/editor-parse.service.spec.ts
@@ -1,7 +1,7 @@
import { Test, TestingModule } from "@nestjs/testing";
import { EditorParseModule } from "./editor-parse.module";
import { EditorParseService } from "./editor-parse.service";
-import { ReviewTaskMachineContextReviewData } from "../claim-review-task/dto/create-claim-review-task.dto";
+import { ReviewTaskMachineContextReviewData } from "../review-task/dto/create-review-task.dto";
import { RemirrorJSON } from "remirror";
describe("ParserService", () => {
diff --git a/server/feature-flag/feature-flag.module.ts b/server/feature-flag/feature-flag.module.ts
new file mode 100644
index 000000000..41cd1af2d
--- /dev/null
+++ b/server/feature-flag/feature-flag.module.ts
@@ -0,0 +1,10 @@
+import { Module } from "@nestjs/common";
+import { ConfigModule } from "@nestjs/config";
+import { FeatureFlagService } from "./feature-flag.service";
+
+@Module({
+ imports: [ConfigModule],
+ exports: [FeatureFlagService],
+ providers: [FeatureFlagService],
+})
+export class FeatureFlagModule {}
diff --git a/server/feature-flag/feature-flag.service.ts b/server/feature-flag/feature-flag.service.ts
new file mode 100644
index 000000000..bf0d1939d
--- /dev/null
+++ b/server/feature-flag/feature-flag.service.ts
@@ -0,0 +1,43 @@
+import { Injectable, Optional } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { UnleashService } from "nestjs-unleash";
+
+@Injectable()
+export class FeatureFlagService {
+ constructor(
+ private configService: ConfigService,
+ @Optional() private readonly unleash: UnleashService
+ ) {}
+
+ isEnableCollaborativeEditor() {
+ const config = this.configService.get("feature_flag");
+
+ return config
+ ? this.unleash.isEnabled("enable_collaborative_editor")
+ : false;
+ }
+
+ isEnableCopilotChatBot() {
+ const config = this.configService.get("feature_flag");
+
+ return config ? this.unleash.isEnabled("copilot_chat_bot") : false;
+ }
+
+ isEnableEditorAnnotations() {
+ const config = this.configService.get("feature_flag");
+
+ return config
+ ? this.unleash.isEnabled("enable_editor_annotations")
+ : false;
+ }
+
+ isEnableAddEditorSourcesWithoutSelecting() {
+ const config = this.configService.get("feature_flag");
+
+ return config
+ ? this.unleash.isEnabled(
+ "enable_add_editor_sources_without_selecting"
+ )
+ : false;
+ }
+}
diff --git a/server/group/group.module.ts b/server/group/group.module.ts
new file mode 100644
index 000000000..00d08f009
--- /dev/null
+++ b/server/group/group.module.ts
@@ -0,0 +1,18 @@
+import { Module } from "@nestjs/common";
+import { MongooseModule } from "@nestjs/mongoose";
+import { Group, GroupSchema } from "./schemas/group.schema";
+import { GroupService } from "./group.service";
+
+const GroupModel = MongooseModule.forFeature([
+ {
+ name: Group.name,
+ schema: GroupSchema,
+ },
+]);
+
+@Module({
+ imports: [GroupModel],
+ exports: [GroupService],
+ providers: [GroupService],
+})
+export class GroupModule {}
diff --git a/server/group/group.service.ts b/server/group/group.service.ts
new file mode 100644
index 000000000..0095dfb55
--- /dev/null
+++ b/server/group/group.service.ts
@@ -0,0 +1,114 @@
+import { Injectable } from "@nestjs/common";
+import { Model, Types } from "mongoose";
+import { InjectModel } from "@nestjs/mongoose";
+import { Group, GroupDocument } from "./schemas/group.schema";
+
+@Injectable()
+export class GroupService {
+ constructor(
+ @InjectModel(Group.name)
+ private GroupModel: Model
+ ) {}
+
+ /**
+ * gets the group document by an ID
+ * @param groupId group id typeof string
+ * @returns
+ */
+ async getById(groupId: string): Promise {
+ return this.GroupModel.findById(groupId);
+ }
+
+ /**
+ * gets the group by querying the content ID
+ * @param contentId content id string
+ * @returns the group document
+ */
+ async getByContentId(contentId): Promise {
+ return this.GroupModel.findOne({ content: contentId }).populate(
+ "content"
+ );
+ }
+
+ /**
+ * Creates or updates a group document
+ * @param group Group document data
+ * @returns the group document
+ */
+ async create(group: Partial): Promise {
+ try {
+ const existingGroup = await this.GroupModel.findOne({
+ content: { $in: group.content },
+ });
+
+ if (existingGroup) {
+ return await this.GroupModel.findByIdAndUpdate(
+ existingGroup._id,
+ group,
+ { new: true }
+ );
+ }
+
+ return await new this.GroupModel(group).save();
+ } catch (error) {
+ console.error("Failed to create or update group:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Updates the group document with the claim target ID
+ * @param groupId specific group which will be updated
+ * @param targetId claim target id
+ * @returns the group document updated
+ */
+ async updateWithTargetId(
+ groupId: string,
+ targetId: string
+ ): Promise {
+ const group = await this.GroupModel.findById(groupId);
+
+ return this.GroupModel.findByIdAndUpdate(
+ group._id,
+ { targetId: Types.ObjectId(targetId) },
+ { new: true }
+ );
+ }
+
+ /**
+ * Removes a verification request from content group.
+ * In case the content is an empty array, deletes the group document
+ * @param groupId group id
+ * @param contentId verification request id
+ * @returns the updated or deleted group document
+ */
+ async removeContent(
+ groupId: string,
+ contentId: string
+ ): Promise<
+ | GroupDocument
+ | ({ ok?: number; n?: number } & { deletedCount?: number })
+ > {
+ try {
+ const group = await this.GroupModel.findById(groupId);
+ const newContent = group.content.filter(
+ (content: any) => content.toString() !== contentId.toString()
+ );
+
+ if (!newContent.length) {
+ return await this.GroupModel.deleteOne({ _id: group._id });
+ }
+
+ if (group) {
+ return await this.GroupModel.findByIdAndUpdate(
+ group._id,
+ { content: newContent },
+ { new: true }
+ );
+ }
+ } catch (error) {
+ console.error("Failed to remove content:", error);
+ throw error;
+ }
+ }
+}
diff --git a/server/group/schemas/group.schema.ts b/server/group/schemas/group.schema.ts
new file mode 100644
index 000000000..fc4820cd3
--- /dev/null
+++ b/server/group/schemas/group.schema.ts
@@ -0,0 +1,38 @@
+import * as mongoose from "mongoose";
+import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
+import { VerificationRequest } from "../../verification-request/schemas/verification-request.schema";
+
+export type GroupDocument = Group & mongoose.Document;
+
+@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } })
+export class Group {
+ @Prop({
+ type: [
+ {
+ type: mongoose.Types.ObjectId,
+ required: false,
+ ref: "VerificationRequest",
+ },
+ ],
+ })
+ content: VerificationRequest[];
+
+ @Prop({
+ type: mongoose.Types.ObjectId,
+ required: false,
+ refPath: "onModel",
+ })
+ targetId: mongoose.Types.ObjectId;
+}
+
+const GroupSchemaRaw = SchemaFactory.createForClass(Group);
+
+GroupSchemaRaw.pre("find", function () {
+ this.populate("content");
+ this.populate({
+ path: "targetId",
+ model: "Claim",
+ });
+});
+
+export const GroupSchema = GroupSchemaRaw;
diff --git a/server/history/schema/history.schema.ts b/server/history/schema/history.schema.ts
index a865257b5..a8de88dca 100644
--- a/server/history/schema/history.schema.ts
+++ b/server/history/schema/history.schema.ts
@@ -9,8 +9,9 @@ export enum TargetModel {
Debate = "Debate",
Personality = "Personality",
ClaimReview = "ClaimReview",
- ClaimReviewTask = "ClaimReviewTask",
+ ReviewTask = "ReviewTask",
Image = "Image",
+ Source = "Source",
}
export enum HistoryType {
diff --git a/server/home/home.controller.ts b/server/home/home.controller.ts
index 61e2f82c2..38dfaf739 100644
--- a/server/home/home.controller.ts
+++ b/server/home/home.controller.ts
@@ -9,6 +9,8 @@ import type { BaseRequest } from "../types";
import { DebateService } from "../claim/types/debate/debate.service";
import { ClaimRevisionService } from "../claim/claim-revision/claim-revision.service";
import { ApiTags } from "@nestjs/swagger";
+import { ClaimReviewService } from "../claim-review/claim-review.service";
+import { NameSpaceEnum } from "../auth/name-space/schemas/name-space.schema";
@Controller("/")
export class HomeController {
@@ -17,7 +19,8 @@ export class HomeController {
private personalityService: PersonalityService,
private statsService: StatsService,
private debateService: DebateService,
- private claimRevisionService: ClaimRevisionService
+ private claimRevisionService: ClaimRevisionService,
+ private claimReviewService: ClaimReviewService
) {}
@ApiTags("pages")
@@ -33,6 +36,18 @@ export class HomeController {
@Header("Cache-Control", "max-age=60, must-revalidate")
public async showHome(@Req() req: BaseRequest, @Res() res: Response) {
const parsedUrl = parse(req.url, true);
+ const reviews = await this.claimReviewService.listAll({
+ page: 0,
+ pageSize: 6,
+ order: "asc",
+ query: {
+ isHidden: false,
+ isDeleted: false,
+ nameSpace: req.params.namespace || NameSpaceEnum.Main,
+ },
+ latest: true,
+ });
+
const { personalities } = await this.personalityService.combinedListAll(
{
language: req.language,
@@ -78,6 +93,7 @@ export class HomeController {
personalities,
stats,
claims,
+ reviews,
nameSpace: req.params.namespace,
})
);
diff --git a/server/home/home.module.ts b/server/home/home.module.ts
index c1f524e05..6117323d4 100644
--- a/server/home/home.module.ts
+++ b/server/home/home.module.ts
@@ -5,6 +5,7 @@ import { StatsModule } from "../stats/stats.module";
import { ViewModule } from "../view/view.module";
import { DebateModule } from "../claim/types/debate/debate.module";
import { ClaimRevisionModule } from "../claim/claim-revision/claim-revision.module";
+import { ClaimReviewModule } from "../claim-review/claim-review.module";
@Module({
imports: [
@@ -13,6 +14,7 @@ import { ClaimRevisionModule } from "../claim/claim-revision/claim-revision.modu
ViewModule,
DebateModule,
ClaimRevisionModule,
+ ClaimReviewModule,
],
providers: [],
controllers: [HomeController],
diff --git a/server/jest.config.json b/server/jest.config.json
index 561581b21..121f06f93 100644
--- a/server/jest.config.json
+++ b/server/jest.config.json
@@ -1,5 +1,4 @@
{
- "name": "server",
"displayName": "server",
"moduleFileExtensions": [
"js",
diff --git a/server/middleware/auth-zenvia-webhook.middleware.ts b/server/middleware/auth-zenvia-webhook.middleware.ts
new file mode 100644
index 000000000..749f8b2a6
--- /dev/null
+++ b/server/middleware/auth-zenvia-webhook.middleware.ts
@@ -0,0 +1,49 @@
+import {
+ Injectable,
+ Logger,
+ NestMiddleware,
+ UnauthorizedException,
+} from "@nestjs/common";
+import { Request, Response, NextFunction } from "express";
+import { ConfigService } from "@nestjs/config";
+
+@Injectable()
+export class AuthZenviaWebHookMiddleware implements NestMiddleware {
+ constructor(private configService: ConfigService) {}
+ private logger = new Logger("HTTP");
+
+ async use(
+ request: Request,
+ response: Response,
+ next: NextFunction
+ ): Promise {
+ const { ip, method, originalUrl } = request;
+ const { api_token } = this.configService.get("zenvia");
+ const userAgent = request.get("user-agent") || "";
+
+ const authHeader = request.headers["authorization"];
+ if (!authHeader) {
+ throw new UnauthorizedException("Authorization header is missing");
+ }
+
+ const token = authHeader.split(" ")[1];
+ if (!token) {
+ throw new UnauthorizedException("Token is missing");
+ }
+
+ if (token !== api_token) {
+ throw new UnauthorizedException("Invalid token");
+ }
+
+ response.on("finish", () => {
+ const { statusCode } = response;
+ const contentLength = response.get("content-length");
+
+ this.logger.log(
+ `${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`
+ );
+ });
+
+ next();
+ }
+}
diff --git a/server/mongo-pipelines/lookUpPersonalityties.ts b/server/mongo-pipelines/lookUpPersonalityties.ts
deleted file mode 100644
index 4faa93fa6..000000000
--- a/server/mongo-pipelines/lookUpPersonalityties.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { TargetModel } from "../history/schema/history.schema";
-
-export default function lookupPersonalities(target) {
- if (target === TargetModel.ClaimReviewTask) {
- return {
- $lookup: {
- from: "personalities",
- let: {
- personalityId: {
- $toObjectId: "$machine.context.claimReview.personality",
- },
- },
- pipeline: [
- { $match: { $expr: { $eq: ["$_id", "$$personalityId"] } } },
- { $project: { slug: 1, name: 1, _id: 1 } },
- ],
- as: "machine.context.claimReview.personality",
- },
- };
- } else if (target === TargetModel.ClaimReview) {
- return {
- $lookup: {
- from: "personalities",
- localField: "personality",
- foreignField: "_id",
- as: "personality",
- },
- };
- }
-}
diff --git a/server/mongo-pipelines/lookupClaimReviews.ts b/server/mongo-pipelines/lookupClaimReviews.ts
deleted file mode 100644
index 3a22ba5ee..000000000
--- a/server/mongo-pipelines/lookupClaimReviews.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export default function ({ localField = "data_hash", as }) {
- return {
- $lookup: {
- from: "claimreviews",
- localField,
- foreignField: "data_hash",
- as,
- },
- };
-}
diff --git a/server/mongo-pipelines/lookupClaimRevisions.ts b/server/mongo-pipelines/lookupClaimRevisions.ts
deleted file mode 100644
index 9c290de1d..000000000
--- a/server/mongo-pipelines/lookupClaimRevisions.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { TargetModel } from "../history/schema/history.schema";
-
-export default function lookupClaimRevisions(target) {
- if (target === TargetModel.ClaimReviewTask) {
- return {
- $lookup: {
- from: "claimrevisions",
- localField: "latestRevision",
- foreignField: "_id",
- as: "latestRevision",
- },
- };
- } else if (target === TargetModel.ClaimReview) {
- return {
- $lookup: {
- from: "claimrevisions",
- localField: "claim.latestRevision",
- foreignField: "_id",
- as: "claim.latestRevision",
- },
- };
- }
-}
diff --git a/server/mongo-pipelines/lookupClaims.ts b/server/mongo-pipelines/lookupClaims.ts
deleted file mode 100644
index f8ef38467..000000000
--- a/server/mongo-pipelines/lookupClaims.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { TargetModel } from "../history/schema/history.schema";
-
-export default function lookupClaims(target, params: any = {}) {
- if (target === TargetModel.ClaimReviewTask) {
- const { pipeline, as } = params;
- return {
- $lookup: {
- from: "claims",
- let: {
- claimId: {
- $toObjectId: "$machine.context.claimReview.claim",
- },
- },
- pipeline,
- as,
- },
- };
- } else if (target === TargetModel.ClaimReview) {
- return {
- $lookup: {
- from: "claims",
- localField: "claim",
- foreignField: "_id",
- as: "claim",
- },
- };
- }
-}
diff --git a/server/mongo-pipelines/lookupUsers.ts b/server/mongo-pipelines/lookupUsers.ts
deleted file mode 100644
index f45307f21..000000000
--- a/server/mongo-pipelines/lookupUsers.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export default function () {
- return {
- $lookup: {
- from: "users",
- let: { usersId: "$machine.context.reviewData.usersId" },
- pipeline: [
- { $match: { $expr: { $in: ["$_id", "$$usersId"] } } },
- { $project: { name: 1 } },
- ],
- as: "machine.context.reviewData.users",
- },
- };
-}
diff --git a/server/personality/personality.service.ts b/server/personality/personality.service.ts
index 40f1ffbf3..302dbafa4 100644
--- a/server/personality/personality.service.ts
+++ b/server/personality/personality.service.ts
@@ -445,7 +445,7 @@ export class PersonalityService {
query: searchText,
path: "name",
fuzzy: {
- maxEdits: 2,
+ maxEdits: 1, // Using maxEdits: 1 to allow minor typos or spelling errors in search queries.
},
},
},
diff --git a/server/report/report.module.ts b/server/report/report.module.ts
index 1ee8f023b..fc9d2dc6a 100644
--- a/server/report/report.module.ts
+++ b/server/report/report.module.ts
@@ -1,4 +1,4 @@
-import { Module } from "@nestjs/common";
+import { Module, forwardRef } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { ConfigModule } from "@nestjs/config";
import { Report, ReportSchema } from "./schemas/report.schema";
@@ -14,14 +14,9 @@ const ReportModel = MongooseModule.forFeature([
]);
@Module({
- imports: [
- ReportModel,
- ConfigModule,
- SourceModule,
- ],
+ imports: [ReportModel, ConfigModule, forwardRef(() => SourceModule)],
exports: [ReportService],
providers: [ReportService],
- controllers: [ReportController]
+ controllers: [ReportController],
})
-
export class ReportModule {}
diff --git a/server/report/report.service.ts b/server/report/report.service.ts
index 54d62ca36..bf2d3859c 100644
--- a/server/report/report.service.ts
+++ b/server/report/report.service.ts
@@ -13,7 +13,7 @@ export class ReportService {
private sourceService: SourceService
) {}
- async create(report) {
+ create(report) {
if (
!Object.values(ClassificationEnum).includes(report.classification)
) {
@@ -21,16 +21,37 @@ export class ReportService {
}
const newReport = new this.ReportModel(report);
- for (const source of report.sources) {
- await this.sourceService.create({
+ if (report.sources) {
+ this.createReportSources(report.sources, newReport.id);
+ } else {
+ this.updateReportSource(report, newReport.id);
+ }
+
+ newReport.save();
+ return newReport;
+ }
+
+ createReportSources(sources, targetId) {
+ for (const source of sources) {
+ this.sourceService.create({
href: source.href,
props: source?.props,
- targetId: newReport.id,
+ targetId,
});
}
+ }
- newReport.save();
- return newReport;
+ updateReportSource({ classification, summary, data_hash }, targetId) {
+ const newSourceBody = {
+ props: {
+ classification: classification,
+ summary: summary,
+ date: new Date(),
+ },
+ targetId,
+ };
+
+ return this.sourceService.update(data_hash, newSourceBody);
}
findByDataHash(data_hash) {
diff --git a/server/claim-review-task/comment/comment.controller.ts b/server/review-task/comment/comment.controller.ts
similarity index 100%
rename from server/claim-review-task/comment/comment.controller.ts
rename to server/review-task/comment/comment.controller.ts
diff --git a/server/claim-review-task/comment/comment.module.ts b/server/review-task/comment/comment.module.ts
similarity index 100%
rename from server/claim-review-task/comment/comment.module.ts
rename to server/review-task/comment/comment.module.ts
diff --git a/server/claim-review-task/comment/comment.service.ts b/server/review-task/comment/comment.service.ts
similarity index 100%
rename from server/claim-review-task/comment/comment.service.ts
rename to server/review-task/comment/comment.service.ts
diff --git a/server/claim-review-task/comment/schema/comment.schema.ts b/server/review-task/comment/schema/comment.schema.ts
similarity index 100%
rename from server/claim-review-task/comment/schema/comment.schema.ts
rename to server/review-task/comment/schema/comment.schema.ts
diff --git a/server/claim-review-task/dto/create-claim-review-task.dto.ts b/server/review-task/dto/create-review-task.dto.ts
similarity index 83%
rename from server/claim-review-task/dto/create-claim-review-task.dto.ts
rename to server/review-task/dto/create-review-task.dto.ts
index 092578f18..15f1c8940 100644
--- a/server/claim-review-task/dto/create-claim-review-task.dto.ts
+++ b/server/review-task/dto/create-review-task.dto.ts
@@ -1,11 +1,10 @@
import { IsEnum, IsNotEmpty, IsObject, IsString } from "class-validator";
import { ClassificationEnum } from "../../claim-review/dto/create-claim-review.dto";
-import { Claim } from "../../claim/schemas/claim.schema";
import { Personality } from "../../personality/schemas/personality.schema";
import { User } from "../../users/schemas/user.schema";
import { ApiProperty } from "@nestjs/swagger";
-import { ReportModelEnum } from "../../types/enums";
+import { ReportModelEnum, ReviewTaskTypeEnum } from "../../types/enums";
export type ReviewTaskMachineContextReviewData = {
usersId?: any[];
@@ -24,16 +23,16 @@ export type ReviewTaskMachineContextReviewData = {
crossCheckingClassification?: string;
crossCheckingComments?: any[];
crossCheckerId?: any;
+ group?: any[];
};
export type ReviewTaskMachineContext = {
reviewData: ReviewTaskMachineContextReviewData;
- claimReview: {
+ review: {
usersId?: User[];
- data_hash: string;
personality: Personality;
- claim: Claim;
isPartialReview: boolean;
+ targetId: string;
};
preloadedOptions: {
usersId?: any[];
@@ -47,7 +46,7 @@ export type Machine = {
value: string;
};
-export class CreateClaimReviewTaskDTO {
+export class CreateReviewTaskDTO {
@IsNotEmpty()
@IsObject()
@ApiProperty()
@@ -72,4 +71,14 @@ export class CreateClaimReviewTaskDTO {
@IsString()
@ApiProperty()
nameSpace: string;
+
+ @IsString()
+ @ApiProperty()
+ target: string;
+
+ @IsNotEmpty()
+ @IsEnum(ReviewTaskTypeEnum)
+ @IsString()
+ @ApiProperty()
+ reviewTaskType: string;
}
diff --git a/server/claim-review-task/dto/get-tasks.dto.ts b/server/review-task/dto/get-tasks.dto.ts
similarity index 65%
rename from server/claim-review-task/dto/get-tasks.dto.ts
rename to server/review-task/dto/get-tasks.dto.ts
index 6f20e5841..55ce669f6 100644
--- a/server/claim-review-task/dto/get-tasks.dto.ts
+++ b/server/review-task/dto/get-tasks.dto.ts
@@ -1,11 +1,15 @@
+import { ApiProperty } from "@nestjs/swagger";
import {
+ IsEnum,
IsInt,
+ IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
Min,
} from "class-validator";
+import { ReviewTaskTypeEnum } from "../../types/enums";
export class GetTasksDTO {
@IsNumber()
@@ -28,10 +32,15 @@ export class GetTasksDTO {
filterUser?: {
assigned: boolean;
crossChecked: boolean;
- reviewerd: boolean;
+ reviewed: boolean;
};
@IsString()
@IsOptional()
nameSpace?: string;
+
+ @IsNotEmpty()
+ @IsEnum(ReviewTaskTypeEnum)
+ @ApiProperty()
+ reviewTaskType: ReviewTaskTypeEnum;
}
diff --git a/server/review-task/dto/update-review-task.dto.ts b/server/review-task/dto/update-review-task.dto.ts
new file mode 100644
index 000000000..015eb60b5
--- /dev/null
+++ b/server/review-task/dto/update-review-task.dto.ts
@@ -0,0 +1,4 @@
+import { PartialType } from "@nestjs/mapped-types";
+import { CreateReviewTaskDTO } from "./create-review-task.dto";
+
+export class UpdateReviewTaskDTO extends PartialType(CreateReviewTaskDTO) {}
diff --git a/server/claim-review-task/mongo-utils.ts b/server/review-task/mongo-utils.ts
similarity index 76%
rename from server/claim-review-task/mongo-utils.ts
rename to server/review-task/mongo-utils.ts
index 8a6c8c012..e88b420f4 100644
--- a/server/claim-review-task/mongo-utils.ts
+++ b/server/review-task/mongo-utils.ts
@@ -12,4 +12,10 @@ export const getQueryMatchForMachineValue = (value) => {
: { [`machine.value.${value}`]: { $exists: true } };
};
-const plainValues = ["published", "submitted", "reported"];
+const plainValues = [
+ "published",
+ "submitted",
+ "reported",
+ "assignedRequest",
+ "rejectedRequest",
+];
diff --git a/server/review-task/review-task.controller.ts b/server/review-task/review-task.controller.ts
new file mode 100644
index 000000000..29a2a2a6f
--- /dev/null
+++ b/server/review-task/review-task.controller.ts
@@ -0,0 +1,190 @@
+import {
+ Body,
+ Controller,
+ Post,
+ Param,
+ Get,
+ Put,
+ Query,
+ Req,
+ Res,
+ Header,
+} from "@nestjs/common";
+import { ReviewTaskService } from "./review-task.service";
+import { CreateReviewTaskDTO } from "./dto/create-review-task.dto";
+import { UpdateReviewTaskDTO } from "./dto/update-review-task.dto";
+import { CaptchaService } from "../captcha/captcha.service";
+import { parse } from "url";
+import type { Request, Response } from "express";
+import { ViewService } from "../view/view.service";
+import { GetTasksDTO } from "./dto/get-tasks.dto";
+import { ConfigService } from "@nestjs/config";
+import { FeatureFlagService } from "../feature-flag/feature-flag.service";
+import { ApiTags } from "@nestjs/swagger";
+import { NameSpaceEnum } from "../auth/name-space/schemas/name-space.schema";
+
+@Controller(":namespace?")
+export class ReviewTaskController {
+ constructor(
+ private reviewTaskService: ReviewTaskService,
+ private captchaService: CaptchaService,
+ private viewService: ViewService,
+ private configService: ConfigService,
+ private featureFlagService: FeatureFlagService
+ ) {}
+
+ @ApiTags("review-task")
+ @Get("api/reviewtask")
+ @Header("Cache-Control", "no-cache")
+ public async getByMachineValue(@Query() getTasksDTO: GetTasksDTO) {
+ const {
+ page = 0,
+ pageSize = 10,
+ order = 1,
+ value,
+ filterUser,
+ reviewTaskType,
+ nameSpace = NameSpaceEnum.Main,
+ } = getTasksDTO;
+ return Promise.all([
+ this.reviewTaskService.listAll({
+ page,
+ pageSize,
+ order,
+ value,
+ filterUser,
+ nameSpace,
+ reviewTaskType,
+ }),
+ this.reviewTaskService.countReviewTasksNotDeleted(
+ value,
+ filterUser,
+ nameSpace,
+ reviewTaskType
+ ),
+ ]).then(([tasks, totalTasks]) => {
+ const totalPages = Math.ceil(totalTasks / pageSize);
+
+ return {
+ tasks,
+ totalTasks,
+ totalPages,
+ page,
+ pageSize,
+ };
+ });
+ }
+
+ @ApiTags("review-task")
+ @Get("api/reviewtask/:id")
+ @Header("Cache-Control", "no-cache")
+ async getById(@Param("id") id: string) {
+ return this.reviewTaskService.getById(id);
+ }
+
+ @ApiTags("review-task")
+ @Post("api/reviewtask")
+ @Header("Cache-Control", "no-cache")
+ async create(@Body() createReviewTask: CreateReviewTaskDTO) {
+ const validateCaptcha = await this.captchaService.validate(
+ createReviewTask.recaptcha
+ );
+ if (!validateCaptcha) {
+ throw new Error("Error validating captcha");
+ }
+ return this.reviewTaskService.create(createReviewTask);
+ }
+
+ @ApiTags("review-task")
+ @Put("api/reviewtask/:data_hash")
+ @Header("Cache-Control", "no-cache")
+ async autoSaveDraft(
+ @Param("data_hash") data_hash,
+ @Body() reviewTaskBody: UpdateReviewTaskDTO
+ ) {
+ const history = false;
+ return this.reviewTaskService
+ .getReviewTaskByDataHash(data_hash)
+ .then((review) => {
+ if (review) {
+ return this.reviewTaskService.update(
+ data_hash,
+ reviewTaskBody,
+ reviewTaskBody.nameSpace,
+ reviewTaskBody.reportModel,
+ history
+ );
+ }
+ });
+ }
+
+ // TODO: remove hash from the url
+ @ApiTags("review-task")
+ @Get("api/reviewtask/hash/:data_hash")
+ @Header("Cache-Control", "no-cache")
+ async getByDataHash(@Param("data_hash") data_hash: string) {
+ return this.reviewTaskService.getReviewTaskByDataHash(data_hash);
+ }
+
+ @ApiTags("review-task")
+ @Get("api/reviewtask/editor-content/:data_hash")
+ @Header("Cache-Control", "no-cache")
+ async getEditorContentByDataHash(
+ @Param("data_hash") data_hash: string,
+ @Query() query: { reportModel: string; reviewTaskType: string }
+ ) {
+ const reviewTask = await this.reviewTaskService.getReviewTaskByDataHash(
+ data_hash
+ );
+
+ return this.reviewTaskService.getEditorContentObject(
+ reviewTask?.machine?.context?.reviewData,
+ query.reportModel,
+ query.reviewTaskType
+ );
+ }
+
+ @ApiTags("review-task")
+ @Put("api/reviewtask/add-comment/:data_hash")
+ @Header("Cache-Control", "no-cache")
+ async addComment(@Param("data_hash") data_hash: string, @Body() body) {
+ return this.reviewTaskService.addComment(data_hash, body.comment);
+ }
+
+ @ApiTags("review-task")
+ @Put("api/reviewtask/delete-comment/:data_hash")
+ @Header("Cache-Control", "no-cache")
+ async deleteComment(@Param("data_hash") data_hash: string, @Body() body) {
+ return this.reviewTaskService.deleteComment(data_hash, body.commentId);
+ }
+
+ @ApiTags("pages")
+ @Get("kanban")
+ @Header("Cache-Control", "no-cache")
+ public async kanbanList(@Req() req: Request, @Res() res: Response) {
+ const parsedUrl = parse(req.url, true);
+ const enableCollaborativeEditor =
+ this.featureFlagService.isEnableCollaborativeEditor();
+ const enableCopilotChatBot =
+ this.featureFlagService.isEnableCopilotChatBot();
+ const enableEditorAnnotations =
+ this.featureFlagService.isEnableEditorAnnotations();
+ const enableAddEditorSourcesWithoutSelecting =
+ this.featureFlagService.isEnableAddEditorSourcesWithoutSelecting();
+
+ await this.viewService.getNextServer().render(
+ req,
+ res,
+ "/kanban-page",
+ Object.assign(parsedUrl.query, {
+ sitekey: this.configService.get("recaptcha_sitekey"),
+ enableCollaborativeEditor,
+ enableEditorAnnotations,
+ enableCopilotChatBot,
+ enableAddEditorSourcesWithoutSelecting,
+ websocketUrl: this.configService.get("websocketUrl"),
+ nameSpace: req.params.namespace,
+ })
+ );
+ }
+}
diff --git a/server/claim-review-task/claim-review-task.module.ts b/server/review-task/review-task.module.ts
similarity index 53%
rename from server/claim-review-task/claim-review-task.module.ts
rename to server/review-task/review-task.module.ts
index 2d93c402c..823d9daf1 100644
--- a/server/claim-review-task/claim-review-task.module.ts
+++ b/server/review-task/review-task.module.ts
@@ -1,11 +1,8 @@
-import { Module } from "@nestjs/common";
-import {
- ClaimReviewTask,
- ClaimReviewTaskSchema,
-} from "./schemas/claim-review-task.schema";
-import { ClaimReviewTaskService } from "./claim-review-task.service";
+import { Module, forwardRef } from "@nestjs/common";
+import { ReviewTask, ReviewTaskSchema } from "./schemas/review-task.schema";
+import { ReviewTaskService } from "./review-task.service";
import { MongooseModule } from "@nestjs/mongoose";
-import { ClaimReviewController } from "./claim-review-task.controller";
+import { ReviewTaskController } from "./review-task.controller";
import { ClaimReviewModule } from "../claim-review/claim-review.module";
import { ReportModule } from "../report/report.module";
import { CaptchaModule } from "../captcha/captcha.module";
@@ -17,31 +14,35 @@ import { ConfigModule } from "@nestjs/config";
import { ImageModule } from "../claim/types/image/image.module";
import { EditorParseModule } from "../editor-parse/editor-parse.module";
import { CommentModule } from "./comment/comment.module";
+import { FeatureFlagModule } from "../feature-flag/feature-flag.module";
+import { GroupModule } from "../group/group.module";
-export const ClaimReviewTaskModel = MongooseModule.forFeature([
+export const ReviewTaskModel = MongooseModule.forFeature([
{
- name: ClaimReviewTask.name,
- schema: ClaimReviewTaskSchema,
+ name: ReviewTask.name,
+ schema: ReviewTaskSchema,
},
]);
@Module({
imports: [
- ClaimReviewTaskModel,
- ClaimReviewModule,
- ReportModule,
+ ReviewTaskModel,
+ forwardRef(() => ClaimReviewModule),
+ forwardRef(() => ReportModule),
HistoryModule,
StateEventModule,
CaptchaModule,
ViewModule,
- SentenceModule,
+ forwardRef(() => SentenceModule),
ConfigModule,
ImageModule,
EditorParseModule,
CommentModule,
+ FeatureFlagModule,
+ GroupModule,
],
- providers: [ClaimReviewTaskService],
- exports: [ClaimReviewTaskService],
- controllers: [ClaimReviewController],
+ providers: [ReviewTaskService],
+ exports: [ReviewTaskService],
+ controllers: [ReviewTaskController],
})
-export class ClaimReviewTaskModule {}
+export class ReviewTaskModule {}
diff --git a/server/review-task/review-task.service.ts b/server/review-task/review-task.service.ts
new file mode 100644
index 000000000..24e588f93
--- /dev/null
+++ b/server/review-task/review-task.service.ts
@@ -0,0 +1,757 @@
+import { ForbiddenException, Inject, Injectable, Scope } from "@nestjs/common";
+import { Model, Types } from "mongoose";
+import { ReviewTask, ReviewTaskDocument } from "./schemas/review-task.schema";
+import { InjectModel } from "@nestjs/mongoose";
+import { CreateReviewTaskDTO, Machine } from "./dto/create-review-task.dto";
+import { UpdateReviewTaskDTO } from "./dto/update-review-task.dto";
+import { ClaimReviewService } from "../claim-review/claim-review.service";
+import { ReportService } from "../report/report.service";
+import { HistoryType, TargetModel } from "../history/schema/history.schema";
+import { HistoryService } from "../history/history.service";
+import { StateEventService } from "../state-event/state-event.service";
+import { TypeModel } from "../state-event/schema/state-event.schema";
+import { REQUEST } from "@nestjs/core";
+import type { BaseRequest } from "../types";
+import { SentenceService } from "../claim/types/sentence/sentence.service";
+import { getQueryMatchForMachineValue } from "./mongo-utils";
+import { Roles } from "../auth/ability/ability.factory";
+import { ImageService } from "../claim/types/image/image.service";
+import {
+ ContentModelEnum,
+ ReportModelEnum,
+ ReviewTaskTypeEnum,
+} from "../types/enums";
+import { EditorParseService } from "../editor-parse/editor-parse.service";
+import { CommentService } from "./comment/comment.service";
+import { CommentEnum } from "./comment/schema/comment.schema";
+import { User } from "../users/schemas/user.schema";
+import { Image } from "../claim/types/image/schemas/image.schema";
+import { Sentence } from "../claim/types/sentence/schemas/sentence.schema";
+import { Source } from "../source/schemas/source.schema";
+
+interface IListAllQuery {
+ value: any;
+ filterUser: { assigned: boolean; crossChecked: boolean; reviewed: boolean };
+ nameSpace: string;
+ reviewTaskType: ReviewTaskTypeEnum;
+ page: number;
+ pageSize: number;
+ order: string | number;
+}
+
+interface IPostProcess {
+ data_hash: string;
+ machine: Machine;
+ target: any;
+ reviewTaskType: string;
+}
+
+export interface IReviewTask {
+ content: Source | Sentence | Image;
+ usersName: string[];
+ value: string;
+ personalityName: string;
+ claimTitle: string;
+ targetId: string;
+ personalityId: string;
+ contentModel: string;
+}
+
+@Injectable({ scope: Scope.REQUEST })
+export class ReviewTaskService {
+ fieldMap: { assigned: string; crossChecked: string; reviewed: string };
+ constructor(
+ @Inject(REQUEST) private req: BaseRequest,
+ @InjectModel(ReviewTask.name)
+ private ReviewTaskModel: Model,
+ private claimReviewService: ClaimReviewService,
+ private reportService: ReportService,
+ private historyService: HistoryService,
+ private stateEventService: StateEventService,
+ private sentenceService: SentenceService,
+ private imageService: ImageService,
+ private editorParseService: EditorParseService,
+ private commentService: CommentService
+ ) {
+ this.fieldMap = {
+ assigned: "machine.context.reviewData.usersId",
+ crossChecked: "machine.context.reviewData.crossCheckerId",
+ reviewed: "machine.context.reviewData.reviewerId",
+ };
+ }
+
+ getQueryObject(value, filterUser) {
+ const query = getQueryMatchForMachineValue(value);
+
+ Object.keys(filterUser).forEach((key) => {
+ const value = filterUser[key];
+ if (value === true || value === "true") {
+ const queryPath = this.fieldMap[key];
+ query[queryPath] = Types.ObjectId(this.req.user._id);
+ }
+ });
+
+ return query;
+ }
+
+ _verifyMachineValueAndAddMatchPipeline(pipeline, value, reviewTaskType) {
+ if (
+ value === "published" &&
+ reviewTaskType !== ReviewTaskTypeEnum.VerificationRequest
+ ) {
+ return pipeline.push(
+ {
+ $lookup: {
+ from: "claimreviews",
+ localField: "data_hash",
+ foreignField: "data_hash",
+ as: "machine.context.claimReview.claimReview",
+ },
+ },
+ { $unwind: "$machine.context.claimReview.claimReview" },
+ {
+ $match: {
+ "machine.context.claimReview.claimReview.isDeleted":
+ false,
+ $or: [
+ { "target.isDeleted": false },
+ { "target.isDeleted": { $exists: false } },
+ ],
+ },
+ }
+ );
+ }
+
+ return pipeline.push({
+ $match: {
+ $or: [
+ { "target.isDeleted": false },
+ { "target.isDeleted": { $exists: false } },
+ ],
+ },
+ });
+ }
+
+ buildLookupPipeline(reviewTaskType) {
+ let pipeline: any = [
+ { $match: { $expr: { $eq: ["$_id", "$$targetId"] } } },
+ ];
+
+ if (reviewTaskType === ReviewTaskTypeEnum.Claim) {
+ pipeline.push(
+ {
+ $lookup: {
+ from: "claimrevisions",
+ localField: "latestRevision",
+ foreignField: "_id",
+ as: "latestRevision",
+ },
+ },
+ { $unwind: "$latestRevision" }
+ );
+ }
+
+ return pipeline;
+ }
+
+ _buildPipeline({
+ value,
+ filterUser,
+ nameSpace,
+ reviewTaskType,
+ }: IListAllQuery) {
+ const pipeline = [];
+ const query = this.getQueryObject(value, filterUser);
+
+ pipeline.push(
+ { $match: { ...query, reviewTaskType, nameSpace } },
+ {
+ $lookup: {
+ from: "users",
+ let: { usersId: "$machine.context.reviewData.usersId" },
+ pipeline: [
+ { $match: { $expr: { $in: ["$_id", "$$usersId"] } } },
+ { $project: { name: 1 } },
+ ],
+ as: "machine.context.reviewData.usersId",
+ },
+ },
+ {
+ $lookup: {
+ from: "personalities",
+ let: {
+ personalityId: {
+ $toObjectId: "$machine.context.review.personality",
+ },
+ },
+ pipeline: [
+ {
+ $match: {
+ $expr: { $eq: ["$_id", "$$personalityId"] },
+ },
+ },
+ { $project: { slug: 1, name: 1, _id: 1 } },
+ ],
+ as: "machine.context.review.personality",
+ },
+ },
+ {
+ $unwind: {
+ path: "$machine.context.review.personality",
+ preserveNullAndEmptyArrays: true,
+ },
+ },
+ {
+ $lookup: {
+ from: `${reviewTaskType.toLowerCase()}s`,
+ let: { targetId: { $toObjectId: "$target" } },
+ pipeline: this.buildLookupPipeline(reviewTaskType),
+ as: "target",
+ },
+ },
+ { $unwind: { path: "$target", preserveNullAndEmptyArrays: true } }
+ );
+
+ this._verifyMachineValueAndAddMatchPipeline(
+ pipeline,
+ value,
+ reviewTaskType
+ );
+
+ return pipeline;
+ }
+
+ async listAll(query: IListAllQuery): Promise {
+ const pipeline = this._buildPipeline(query);
+ pipeline.push(
+ { $sort: { _id: query.order === "asc" ? 1 : -1 } },
+ { $skip: query.page * query.pageSize },
+ { $limit: query.pageSize }
+ );
+
+ const reviewTasks = await this.ReviewTaskModel.aggregate(
+ pipeline
+ ).exec();
+
+ return Promise.all(
+ reviewTasks?.map((reviewTask) => this.postProcess(reviewTask))
+ );
+ }
+
+ async postProcess({
+ data_hash,
+ machine,
+ target,
+ reviewTaskType,
+ }: IPostProcess): Promise {
+ let content: Source | Sentence | Image = target;
+ const personality: { _id?: string; name?: string } =
+ machine.context.review.personality;
+ const contentModel: string = target?.latestRevision?.contentModel;
+ const claimTitle: string = target?.latestRevision?.title;
+ const usersId: User[] = machine.context.reviewData.usersId;
+ const isContentImage: boolean = contentModel === ContentModelEnum.Image;
+ const usersName: string[] = usersId.map((user) => user.name);
+
+ if (reviewTaskType === ReviewTaskTypeEnum.Claim) {
+ if (isContentImage) {
+ content = await this.imageService.getByDataHash(data_hash);
+ }
+
+ content = await this.sentenceService.getByDataHash(data_hash);
+ }
+
+ return {
+ content,
+ usersName,
+ value: machine.value,
+ personalityName: personality?.name,
+ claimTitle,
+ targetId: target._id,
+ personalityId: personality?._id,
+ contentModel,
+ };
+ }
+
+ getById(reviewTaskId: string) {
+ return this.ReviewTaskModel.findById(reviewTaskId);
+ }
+
+ _createReviewTaskHistory(newReviewTask, previousReviewTask = null) {
+ let historyType;
+
+ if (typeof newReviewTask.machine.value === "object") {
+ historyType =
+ newReviewTask.machine.value?.[
+ Object.keys(newReviewTask.machine.value)[0]
+ ] === "draft"
+ ? HistoryType.Draft
+ : Object.keys(newReviewTask.machine.value)[0];
+ }
+
+ const user = this.req.user;
+
+ const history = this.historyService.getHistoryParams(
+ newReviewTask._id,
+ TargetModel.ReviewTask,
+ user,
+ historyType || HistoryType.Published,
+ {
+ ...newReviewTask.machine.context.reviewData,
+ ...newReviewTask.machine.context.review.target,
+ value: newReviewTask.machine.value,
+ },
+ previousReviewTask && {
+ ...previousReviewTask.machine.context.reviewData,
+ ...previousReviewTask.machine.context.review.target,
+ value: previousReviewTask.machine.value,
+ }
+ );
+
+ this.historyService.createHistory(history);
+ }
+
+ _createStateEvent(newReviewTask) {
+ let typeModel;
+ let draft = false;
+
+ if (typeof newReviewTask.machine.value === "object") {
+ draft =
+ newReviewTask.machine.value?.[
+ Object.keys(newReviewTask.machine.value)[0]
+ ] === "draft"
+ ? true
+ : false;
+
+ typeModel = Object.keys(newReviewTask.machine.value)[0];
+ }
+
+ const stateEvent = this.stateEventService.getStateEventParams(
+ Types.ObjectId(newReviewTask.machine.context.review.target),
+ typeModel || TypeModel.Published,
+ draft,
+ newReviewTask._id
+ );
+
+ this.stateEventService.createStateEvent(stateEvent);
+ }
+
+ async _createReportAndClaimReview(
+ data_hash,
+ machine,
+ reportModel,
+ nameSpace,
+ target,
+ targetModel
+ ) {
+ const reviewData = machine.context.review;
+
+ const newReport = Object.assign(machine.context.reviewData, {
+ data_hash,
+ reportModel,
+ });
+
+ const report = await this.reportService.create(newReport);
+
+ this.claimReviewService.create(
+ {
+ ...reviewData,
+ report,
+ nameSpace,
+ target,
+ targetModel,
+ },
+ data_hash,
+ reportModel
+ );
+ }
+
+ _returnObjectId(data): any {
+ if (Array.isArray(data)) {
+ return data.map((item) =>
+ item._id ? Types.ObjectId(item._id) || "" : Types.ObjectId(item)
+ );
+ }
+ }
+
+ _createCrossCheckingComment(comment, text, targetId) {
+ const newCrossCheckingComment = {
+ comment,
+ text,
+ type: CommentEnum.crossChecking,
+ targetId,
+ user: this.req.user._id,
+ };
+ return this.commentService.create(newCrossCheckingComment);
+ }
+
+ async create(reviewTaskBody: CreateReviewTaskDTO) {
+ const reviewDataBody = reviewTaskBody.machine.context.reviewData;
+ const reviewTask = await this.getReviewTaskByDataHash(
+ reviewTaskBody.data_hash
+ );
+
+ const createCrossCheckingComment =
+ reviewTask?.machine?.value === "addCommentCrossChecking" &&
+ reviewDataBody.crossCheckingClassification &&
+ reviewDataBody.crossCheckingComment;
+
+ reviewTaskBody.machine.context.reviewData.usersId =
+ this._returnObjectId(reviewDataBody.usersId);
+
+ reviewTaskBody.machine.context.reviewData.group = this._returnObjectId(
+ reviewDataBody.group
+ );
+
+ if (reviewDataBody.reviewerId) {
+ reviewTaskBody.machine.context.reviewData.reviewerId =
+ Types.ObjectId(reviewDataBody.reviewerId) || "";
+ }
+
+ if (reviewDataBody.crossCheckerId) {
+ reviewTaskBody.machine.context.reviewData.crossCheckerId =
+ Types.ObjectId(reviewDataBody.crossCheckerId) || "";
+ }
+
+ if (reviewDataBody.reviewComments) {
+ this.commentService.updateManyComments(
+ reviewDataBody.reviewComments
+ );
+ reviewTaskBody.machine.context.reviewData.reviewComments =
+ this._returnObjectId(reviewDataBody.reviewComments);
+ }
+
+ if (reviewDataBody.crossCheckingComments) {
+ reviewTaskBody.machine.context.reviewData.crossCheckingComments =
+ this._returnObjectId(reviewDataBody.crossCheckingComments);
+ }
+
+ if (createCrossCheckingComment) {
+ const crossCheckingComment = await this._createCrossCheckingComment(
+ reviewDataBody.crossCheckingComment,
+ reviewDataBody.crossCheckingClassification,
+ reviewTask._id
+ );
+ reviewTaskBody.machine.context.reviewData.crossCheckingComments.push(
+ crossCheckingComment._id
+ );
+ }
+
+ if (reviewTask) {
+ return this.update(
+ reviewTaskBody.data_hash,
+ reviewTaskBody,
+ reviewTaskBody.nameSpace,
+ reviewTask.reportModel
+ );
+ } else {
+ const newReviewTask = new this.ReviewTaskModel(reviewTaskBody);
+ newReviewTask.save();
+ this._createReviewTaskHistory(newReviewTask);
+ this._createStateEvent(newReviewTask);
+ return newReviewTask;
+ }
+ }
+
+ async update(
+ data_hash: string,
+ { machine }: UpdateReviewTaskDTO,
+ nameSpace: string,
+ reportModel: string,
+ history: boolean = true
+ ) {
+ // This line may cause a false positive in sonarCloud because if we remove the await, we cannot iterate through the results
+ const reviewTask = await this.getReviewTaskByDataHash(data_hash);
+
+ const newReviewTaskMachine = {
+ ...reviewTask.machine,
+ ...machine,
+ };
+
+ const newReviewTask = {
+ ...reviewTask.toObject(),
+ machine: newReviewTaskMachine,
+ };
+
+ this._publishReviewTask(
+ newReviewTask,
+ nameSpace,
+ machine,
+ data_hash,
+ reportModel
+ );
+
+ if (history) {
+ this._createReviewTaskHistory(newReviewTask, reviewTask);
+ this._createStateEvent(newReviewTask);
+ }
+
+ return this.ReviewTaskModel.updateOne(
+ { _id: newReviewTask._id },
+ newReviewTask
+ );
+ }
+
+ getReviewTaskByDataHash(data_hash: string) {
+ const commentPopulation = [
+ {
+ path: "user",
+ select: "name",
+ },
+ {
+ path: "replies",
+ populate: {
+ path: "user",
+ select: "name",
+ },
+ },
+ ];
+
+ return this.ReviewTaskModel.findOne({ data_hash })
+ .populate({
+ path: "machine.context.reviewData.reviewComments",
+ model: "Comment",
+ populate: commentPopulation,
+ })
+ .populate({
+ path: "machine.context.reviewData.crossCheckingComments",
+ model: "Comment",
+ populate: commentPopulation,
+ });
+ }
+
+ async getReviewTasksByClaimId(targetId: string) {
+ return await this.ReviewTaskModel.find({
+ "machine.context.review.target": targetId.toString(),
+ "machine.value": { $ne: "published" },
+ });
+ }
+
+ async getReviewTaskByDataHashWithUsernames(data_hash: string) {
+ // This may cause a false positive in sonarCloud
+ const reviewTask = await this.getReviewTaskByDataHash(data_hash)
+ .populate({
+ path: "machine.context.reviewData.usersId",
+ model: "User",
+ select: "name",
+ })
+ .populate({
+ path: "machine.context.reviewData.crossCheckerId",
+ model: "User",
+ select: "name",
+ })
+ .populate({
+ path: "machine.context.reviewData.reviewerId",
+ model: "User",
+ select: "name",
+ });
+
+ if (reviewTask) {
+ const preloadedAsignees = [];
+ const usersId = [];
+ reviewTask.machine.context.reviewData.usersId.forEach(
+ (assignee) => {
+ preloadedAsignees.push({
+ value: assignee._id,
+ label: assignee.name,
+ });
+ usersId.push(assignee._id);
+ }
+ );
+ reviewTask.machine.context.reviewData.usersId = usersId;
+ reviewTask.machine.context.preloadedOptions = {
+ usersId: preloadedAsignees,
+ };
+
+ if (reviewTask.machine.context.reviewData.crossCheckerId) {
+ const crossCheckerUser =
+ reviewTask.machine.context.reviewData.crossCheckerId;
+ reviewTask.machine.context.preloadedOptions.crossCheckerId = [
+ {
+ value: crossCheckerUser._id,
+ label: crossCheckerUser.name,
+ },
+ ];
+ reviewTask.machine.context.reviewData.crossCheckerId =
+ crossCheckerUser._id;
+ }
+
+ if (reviewTask.machine.context.reviewData.reviewerId) {
+ const reviewerUser =
+ reviewTask.machine.context.reviewData.reviewerId;
+ reviewTask.machine.context.preloadedOptions.reviewerId = [
+ {
+ value: reviewerUser._id,
+ label: reviewerUser.name,
+ },
+ ];
+ reviewTask.machine.context.reviewData.reviewerId =
+ reviewerUser._id;
+ }
+ }
+
+ return reviewTask;
+ }
+
+ count(query: any = {}) {
+ return this.ReviewTaskModel.countDocuments().where(query);
+ }
+
+ async countReviewTasksNotDeleted(
+ value,
+ filterUser,
+ nameSpace,
+ reviewTaskType
+ ) {
+ try {
+ const query: any = this.getQueryObject(value, filterUser);
+
+ const pipeline = [
+ { $match: { ...query, nameSpace, reviewTaskType } },
+ {
+ $lookup: {
+ from: "claimreviews",
+ localField: "data_hash",
+ foreignField: "data_hash",
+ as: "machine.context.review.claimReview",
+ },
+ },
+ {
+ $lookup: {
+ from:
+ reviewTaskType === ReviewTaskTypeEnum.Claim
+ ? "claims"
+ : "sources",
+ let: { targetId: { $toObjectId: "$target" } },
+ pipeline: [
+ {
+ $match: {
+ $expr: { $eq: ["$_id", "$$targetId"] },
+ },
+ },
+ { $project: { isDeleted: 1 } },
+ ],
+ as: "target",
+ },
+ },
+ {
+ $match: {
+ "machine.context.review.claimReview.isDeleted": {
+ $ne: true,
+ },
+ $or: [
+ { "target.isDeleted": false },
+ { "target.isDeleted": { $exists: false } },
+ ],
+ },
+ },
+ { $count: "count" },
+ ];
+
+ const result = await this.ReviewTaskModel.aggregate(
+ pipeline
+ ).exec();
+
+ if (result.length > 0) {
+ return result[0].count;
+ }
+
+ return 0;
+ } catch (error) {
+ console.error("Error in countReviewTasksNotDeleted:", error);
+ throw error;
+ }
+ }
+
+ getEditorContentObject(schema, reportModel, reviewTaskType) {
+ return this.editorParseService.schema2editor(
+ schema,
+ reportModel,
+ reviewTaskType
+ );
+ }
+
+ async addComment(data_hash, comment) {
+ const reviewTask = await this.getReviewTaskByDataHash(data_hash);
+ const reviewData = reviewTask.machine.context.reviewData;
+ const newComment = await this.commentService.create({
+ ...comment,
+ targetId: reviewTask._id,
+ });
+
+ if (!reviewData.reviewComments) {
+ reviewData.reviewComments = [];
+ }
+
+ reviewData.reviewComments.push(Types.ObjectId(newComment?._id));
+
+ const { machine } = await this.ReviewTaskModel.findOneAndUpdate(
+ { _id: reviewTask._id },
+ { "machine.context.reviewData": reviewData },
+ { new: true }
+ );
+
+ return {
+ reviewData: machine.context.reviewData,
+ comment: newComment,
+ };
+ }
+
+ async deleteComment(data_hash, commentId) {
+ const commentIdObject = Types.ObjectId(commentId);
+ const reviewTask = await this.getReviewTaskByDataHash(data_hash);
+ const reviewData = reviewTask.machine.context.reviewData;
+ reviewData.reviewComments = reviewData.reviewComments.filter(
+ (comment) => !comment._id.equals(commentIdObject)
+ );
+ reviewData.reviewComments = reviewData.crossCheckingComments.filter(
+ (comment) => !comment._id.equals(commentIdObject)
+ );
+
+ return this.ReviewTaskModel.findByIdAndUpdate(reviewTask._id, {
+ "machine.context.reviewData": reviewData,
+ });
+ }
+
+ async getHtmlFromSchema(schema) {
+ const htmlContent = this.editorParseService.schema2html(schema);
+ return {
+ ...schema,
+ ...htmlContent,
+ };
+ }
+
+ private _publishReviewTask(
+ reviewTaskMachine,
+ nameSpace,
+ machine,
+ data_hash,
+ reportModel
+ ) {
+ const loggedInUser = this.req.user;
+
+ if (
+ reviewTaskMachine.machine.value === "published" &&
+ reportModel !== ReportModelEnum.Request
+ ) {
+ if (
+ loggedInUser.role[nameSpace] !== Roles.Admin &&
+ loggedInUser.role[nameSpace] !== Roles.SuperAdmin &&
+ loggedInUser._id !==
+ machine.context.reviewData.reviewerId.toString()
+ ) {
+ throw new ForbiddenException(
+ "This user does not have permission to publish the report"
+ );
+ }
+ this._createReportAndClaimReview(
+ data_hash,
+ reviewTaskMachine.machine,
+ reportModel,
+ nameSpace,
+ reviewTaskMachine.target,
+ reviewTaskMachine.reviewTaskType
+ );
+ }
+ }
+}
diff --git a/server/review-task/schemas/review-task.schema.ts b/server/review-task/schemas/review-task.schema.ts
new file mode 100644
index 000000000..e99e5cc42
--- /dev/null
+++ b/server/review-task/schemas/review-task.schema.ts
@@ -0,0 +1,65 @@
+import * as mongoose from "mongoose";
+import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
+import type { Machine } from "../dto/create-review-task.dto";
+import { ReportModelEnum, ReviewTaskTypeEnum } from "../../types/enums";
+import { NameSpaceEnum } from "../../auth/name-space/schemas/name-space.schema";
+
+export type ReviewTaskDocument = ReviewTask &
+ mongoose.Document & { content: any };
+
+@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } })
+export class ReviewTask {
+ @Prop({ type: Object, required: true })
+ machine: Machine;
+
+ @Prop({ unique: true, required: true })
+ data_hash: string;
+
+ @Prop({
+ required: true,
+ validate: {
+ validator: (v) => {
+ return Object.values(ReportModelEnum).includes(v);
+ },
+ },
+ message: (tag) => `${tag} is not a valid report type.`,
+ })
+ reportModel: ReportModelEnum;
+
+ @Prop({ default: NameSpaceEnum.Main, required: true })
+ nameSpace: string;
+
+ @Prop({
+ type: mongoose.Types.ObjectId,
+ required: true,
+ refPath: "onModel",
+ })
+ target: mongoose.Types.ObjectId;
+
+ @Prop({
+ required: true,
+ validate: {
+ validator: (v) => {
+ return Object.values(ReviewTaskTypeEnum).includes(v);
+ },
+ },
+ message: (tag) => `${tag} is not a valid review task type.`,
+ })
+ reviewTaskType: ReviewTaskTypeEnum;
+}
+
+const ReviewTaskSchemaRaw = SchemaFactory.createForClass(ReviewTask);
+
+ReviewTaskSchemaRaw.virtual("content", {
+ ref: () => ["Claim", "Source"],
+ localField: "target",
+ foreignField: "_id",
+});
+
+ReviewTaskSchemaRaw.virtual("reviews", {
+ ref: "ClaimReview",
+ localField: "data_hash",
+ foreignField: "data_hash",
+});
+
+export const ReviewTaskSchema = ReviewTaskSchemaRaw;
diff --git a/server/source/dto/create-source.dto.ts b/server/source/dto/create-source.dto.ts
index 9eee1d62d..d42cf5397 100644
--- a/server/source/dto/create-source.dto.ts
+++ b/server/source/dto/create-source.dto.ts
@@ -1,4 +1,4 @@
-import { IsNotEmpty, IsString, IsObject } from "class-validator";
+import { IsNotEmpty, IsString, IsObject, IsOptional } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { User } from "../../users/schemas/user.schema";
@@ -18,8 +18,8 @@ export class CreateSourceDTO {
@ApiProperty()
href: string;
- @IsNotEmpty()
@IsObject()
+ @IsOptional()
@ApiProperty()
props: SourceProps;
@@ -35,4 +35,9 @@ export class CreateSourceDTO {
@IsString()
@ApiProperty()
recaptcha: string;
+
+ @IsString()
+ @IsOptional()
+ @ApiProperty()
+ targetId: string;
}
diff --git a/server/source/schemas/source.schema.ts b/server/source/schemas/source.schema.ts
index 1c62563ea..05f6fe416 100644
--- a/server/source/schemas/source.schema.ts
+++ b/server/source/schemas/source.schema.ts
@@ -38,6 +38,9 @@ export class Source {
})
user: User;
+ @Prop({ unique: true, required: true })
+ data_hash: string;
+
@Prop({ default: NameSpaceEnum.Main, required: false })
nameSpace: string;
}
diff --git a/server/source/source.controller.ts b/server/source/source.controller.ts
index 959d101bf..1c37f9ed4 100644
--- a/server/source/source.controller.ts
+++ b/server/source/source.controller.ts
@@ -4,6 +4,7 @@ import {
Get,
Header,
Logger,
+ Optional,
Param,
Post,
Query,
@@ -20,6 +21,11 @@ import type { Response } from "express";
import { IsPublic } from "../auth/decorators/is-public.decorator";
import { CreateSourceDTO } from "./dto/create-source.dto";
import { CaptchaService } from "../captcha/captcha.service";
+import { UnleashService } from "nestjs-unleash";
+import { TargetModel } from "../history/schema/history.schema";
+import { HistoryService } from "../history/history.service";
+import { ReviewTaskService } from "../review-task/review-task.service";
+import { ClaimReviewService } from "../claim-review/claim-review.service";
@Controller(":namespace?")
export class SourceController {
@@ -28,12 +34,25 @@ export class SourceController {
private sourceService: SourceService,
private viewService: ViewService,
private configService: ConfigService,
- private captchaService: CaptchaService
+ private captchaService: CaptchaService,
+ private claimReviewService: ClaimReviewService,
+ private reviewTaskService: ReviewTaskService,
+ private historyService: HistoryService,
+ @Optional() private readonly unleash: UnleashService
) {}
@ApiTags("source")
- @Get("api/source/:targetId")
- public async getSourcesClaim(@Param() params, @Query() getSources: any) {
+ @Get("api/source/:id")
+ async getById(@Param("id") sourceId: string) {
+ return this.sourceService.getById(sourceId);
+ }
+
+ @ApiTags("source")
+ @Get("api/source/target/:targetId")
+ public async getSourcesByTargetId(
+ @Param() params,
+ @Query() getSources: any
+ ) {
const { targetId } = params;
const { page, order } = getSources;
const pageSize = parseInt(getSources.pageSize, 10);
@@ -64,7 +83,7 @@ export class SourceController {
}
@ApiTags("pages")
- @Get("sources/create")
+ @Get("source/create")
public async sourceCreatePage(
@Req() req: BaseRequest,
@Res() res: Response
@@ -109,7 +128,8 @@ export class SourceController {
@IsPublic()
@ApiTags("pages")
- @Get("sources")
+ @Get("source")
+ @Header("Cache-Control", "max-age=60, must-revalidate")
public async sourcesPage(@Req() req: BaseRequest, @Res() res: Response) {
const parsedUrl = parse(req.url, true);
@@ -122,4 +142,99 @@ export class SourceController {
})
);
}
+
+ @IsPublic()
+ @ApiTags("pages")
+ @Get("source/:dataHash")
+ @Header("Cache-Control", "max-age=60, must-revalidate")
+ public async sourceReviewPage(
+ @Req() req: BaseRequest,
+ @Res() res: Response
+ ) {
+ const source = await this.sourceService.getByDataHash(
+ req.params.dataHash
+ );
+
+ const reviewTask =
+ await this.reviewTaskService.getReviewTaskByDataHashWithUsernames(
+ source.data_hash
+ );
+ const claimReview = await this.claimReviewService.getReviewByDataHash(
+ source.data_hash
+ );
+
+ const enableCollaborativeEditor = this.isEnableCollaborativeEditor();
+ const enableCopilotChatBot = this.isEnableCopilotChatBot();
+ const enableEditorAnnotations = this.isEnableEditorAnnotations();
+ const enableAddEditorSourcesWithoutSelecting =
+ this.isEnableAddEditorSourcesWithoutSelecting();
+
+ const hideDescriptions = {};
+
+ hideDescriptions[TargetModel.Source] =
+ await this.historyService.getDescriptionForHide(
+ source,
+ TargetModel.Claim
+ );
+
+ hideDescriptions[TargetModel.ClaimReview] =
+ await this.historyService.getDescriptionForHide(
+ claimReview,
+ TargetModel.ClaimReview
+ );
+
+ const parsedUrl = parse(req.url, true);
+
+ await this.viewService.getNextServer().render(
+ req,
+ res,
+ "/source-review",
+ Object.assign(parsedUrl.query, {
+ source,
+ reviewTask,
+ claimReview,
+ sitekey: this.configService.get("recaptcha_sitekey"),
+ hideDescriptions,
+ enableCollaborativeEditor,
+ enableEditorAnnotations,
+ enableCopilotChatBot,
+ enableAddEditorSourcesWithoutSelecting,
+ websocketUrl: this.configService.get("websocketUrl"),
+ nameSpace: req.params.namespace,
+ })
+ );
+ }
+
+ //TODO: Create service to get feature flags config
+ private isEnableCollaborativeEditor() {
+ const config = this.configService.get("feature_flag");
+
+ return config
+ ? this.unleash.isEnabled("enable_collaborative_editor")
+ : false;
+ }
+
+ private isEnableCopilotChatBot() {
+ const config = this.configService.get("feature_flag");
+
+ return config ? this.unleash.isEnabled("copilot_chat_bot") : false;
+ }
+
+ private isEnableEditorAnnotations() {
+ const config = this.configService.get("feature_flag");
+
+ return config
+ ? this.unleash.isEnabled("enable_editor_annotations")
+ : false;
+ }
+
+ private isEnableAddEditorSourcesWithoutSelecting() {
+ const config = this.configService.get("feature_flag");
+
+ return config
+ ? this.unleash.isEnabled(
+ "enable_add_editor_sources_without_selecting"
+ )
+ : false;
+ }
}
diff --git a/server/source/source.module.ts b/server/source/source.module.ts
index eb88cc83f..4bd568448 100644
--- a/server/source/source.module.ts
+++ b/server/source/source.module.ts
@@ -1,4 +1,4 @@
-import { Module } from "@nestjs/common";
+import { Module, forwardRef } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { Source, SourceSchema } from "./schemas/source.schema";
import { SourceController } from "./source.controller";
@@ -6,6 +6,9 @@ import { SourceService } from "./source.service";
import { ViewModule } from "../view/view.module";
import { ConfigModule } from "@nestjs/config";
import { CaptchaModule } from "../captcha/captcha.module";
+import { HistoryModule } from "../history/history.module";
+import { ClaimReviewModule } from "../claim-review/claim-review.module";
+import { ReviewTaskModule } from "../review-task/review-task.module";
const SourceModel = MongooseModule.forFeature([
{
@@ -15,7 +18,15 @@ const SourceModel = MongooseModule.forFeature([
]);
@Module({
- imports: [SourceModel, ViewModule, ConfigModule, CaptchaModule],
+ imports: [
+ SourceModel,
+ ViewModule,
+ ConfigModule,
+ CaptchaModule,
+ HistoryModule,
+ forwardRef(() => ClaimReviewModule),
+ ReviewTaskModule,
+ ],
providers: [SourceService],
exports: [SourceService],
controllers: [SourceController],
diff --git a/server/source/source.service.ts b/server/source/source.service.ts
index da72c77bc..8b72fe2fe 100644
--- a/server/source/source.service.ts
+++ b/server/source/source.service.ts
@@ -1,7 +1,8 @@
-import { Injectable } from "@nestjs/common";
+import { Injectable, NotFoundException } from "@nestjs/common";
import { Model, Types } from "mongoose";
import { SourceDocument, Source } from "./schemas/source.schema";
import { InjectModel } from "@nestjs/mongoose";
+const md5 = require("md5");
@Injectable()
export class SourceService {
@@ -35,6 +36,7 @@ export class SourceService {
if (data?.props?.date) {
data.props.date = new Date(data.props.date);
}
+ data.data_hash = md5(data.href);
data.user = Types.ObjectId(data.user);
//TODO: don't create duplicate sources in one claim review task
return await new this.SourceModel(data).save();
@@ -62,7 +64,33 @@ export class SourceService {
}
getById(_id) {
- return this.SourceModel.findById(_id, { _id: 1, href: 1 });
+ return this.SourceModel.findById(_id);
+ }
+
+ async getByDataHash(data_hash) {
+ const source = await this.SourceModel.findOne({ data_hash });
+
+ if (source) {
+ return source;
+ } else {
+ throw new NotFoundException();
+ }
+ }
+
+ async update(data_hash, sourceBodyUpdate) {
+ const source = await this.getByDataHash(data_hash);
+
+ const newSource = Object.assign(source, sourceBodyUpdate);
+ const sourceUpdated = await this.SourceModel.findByIdAndUpdate(
+ source._id,
+ newSource,
+ {
+ new: true,
+ upsert: true,
+ }
+ );
+
+ return sourceUpdated;
}
count(query) {
diff --git a/server/state-event/schema/state-event.schema.ts b/server/state-event/schema/state-event.schema.ts
index efd650176..0becde79a 100644
--- a/server/state-event/schema/state-event.schema.ts
+++ b/server/state-event/schema/state-event.schema.ts
@@ -1,7 +1,7 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import * as mongoose from "mongoose";
import { Claim } from "../../claim/schemas/claim.schema";
-import { ClaimReviewTask } from "../../claim-review-task/schemas/claim-review-task.schema";
+import { ReviewTask } from "../../review-task/schemas/review-task.schema";
export type StateEventDocument = StateEvent & mongoose.Document;
@@ -29,9 +29,9 @@ export class StateEvent {
@Prop({
type: mongoose.Types.ObjectId,
required: false,
- ref: "ClaimReviewTask",
+ ref: "ReviewTask",
})
- taskId: ClaimReviewTask;
+ taskId: ReviewTask;
@Prop({
type: Boolean,
diff --git a/server/summarization/summarization-chain.service.ts b/server/summarization/summarization-crawler-chain.service.ts
similarity index 98%
rename from server/summarization/summarization-chain.service.ts
rename to server/summarization/summarization-crawler-chain.service.ts
index 30d42d4de..379c9196d 100644
--- a/server/summarization/summarization-chain.service.ts
+++ b/server/summarization/summarization-crawler-chain.service.ts
@@ -7,7 +7,7 @@ import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { ConfigService } from "@nestjs/config";
@Injectable()
-export class SummarizationChainService {
+export class SummarizationCrawlerChainService {
private readonly logger = new Logger("SummarizationChainLogger");
constructor(private configService: ConfigService) {}
diff --git a/server/summarization/summarization-crawler.controller.ts b/server/summarization/summarization-crawler.controller.ts
new file mode 100644
index 000000000..a02b0c26e
--- /dev/null
+++ b/server/summarization/summarization-crawler.controller.ts
@@ -0,0 +1,27 @@
+import { Controller, Get, Query, Req, UseGuards } from "@nestjs/common";
+import { ApiTags } from "@nestjs/swagger";
+import { SummarizationCrawlerService } from "./summarization-crawler.service";
+import type { BaseRequest } from "../types";
+import { AbilitiesGuard } from "../auth/ability/abilities.guard";
+import {
+ CheckAbilities,
+ FactCheckerUserAbility,
+} from "../auth/ability/ability.decorator";
+
+@Controller()
+export class SummarizationCrawlerController {
+ constructor(
+ private summarizationCrawlerService: SummarizationCrawlerService
+ ) {}
+
+ @ApiTags("source")
+ @Get("api/summarization")
+ @UseGuards(AbilitiesGuard)
+ @CheckAbilities(new FactCheckerUserAbility())
+ create(@Req() req: BaseRequest, @Query() query) {
+ return this.summarizationCrawlerService.summarizePage(
+ query.source,
+ req.language
+ );
+ }
+}
diff --git a/server/summarization/summarization-crawler.module.ts b/server/summarization/summarization-crawler.module.ts
new file mode 100644
index 000000000..591dd91e6
--- /dev/null
+++ b/server/summarization/summarization-crawler.module.ts
@@ -0,0 +1,15 @@
+import { Module } from "@nestjs/common";
+import { SummarizationCrawlerChainService } from "./summarization-crawler-chain.service";
+import { SummarizationCrawlerService } from "./summarization-crawler.service";
+import { ClaimReviewModule } from "../claim-review/claim-review.module";
+import { AbilityModule } from "../auth/ability/ability.module";
+import { ConfigModule } from "@nestjs/config";
+import { SummarizationCrawlerController } from "./summarization-crawler.controller";
+
+@Module({
+ imports: [ClaimReviewModule, AbilityModule, ConfigModule],
+ providers: [SummarizationCrawlerChainService, SummarizationCrawlerService],
+ exports: [SummarizationCrawlerService],
+ controllers: [SummarizationCrawlerController],
+})
+export class SummarizationCrawlerModule {}
diff --git a/server/summarization/summarization.service.ts b/server/summarization/summarization-crawler.service.ts
similarity index 67%
rename from server/summarization/summarization.service.ts
rename to server/summarization/summarization-crawler.service.ts
index e24ebb0ce..1ff03a967 100644
--- a/server/summarization/summarization.service.ts
+++ b/server/summarization/summarization-crawler.service.ts
@@ -1,10 +1,22 @@
import { Injectable, Logger } from "@nestjs/common";
-import { SummarizationChainService } from "./summarization-chain.service";
+import { SummarizationCrawlerChainService } from "./summarization-crawler-chain.service";
+import { WebBrowser } from "langchain/tools/webbrowser";
+import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
+import { openAI } from "../copilot/openAI.constants";
+import { AgentExecutor, createOpenAIToolsAgent } from "langchain/agents";
+import { ConfigService } from "@nestjs/config";
+import {
+ ChatPromptTemplate,
+ MessagesPlaceholder,
+} from "@langchain/core/prompts";
@Injectable()
-export class SummarizationService {
+export class SummarizationCrawlerService {
private readonly logger = new Logger("SummarizationLogger");
- constructor(private chainService: SummarizationChainService) {}
+ constructor(
+ private chainService: SummarizationCrawlerChainService,
+ private configService: ConfigService
+ ) {}
async getSummarizedReviews(dailyReviews: any[]): Promise {
try {
@@ -116,4 +128,39 @@ export class SummarizationService {
const stuffChain = this.chainService.createBulletPointsChain();
return await this.chainService.generateAnswer(stuffChain, content);
}
+
+ async summarizePage(source, language = "pt") {
+ language = language === "pt" ? "Portuguese" : "English";
+ const prompt = ChatPromptTemplate.fromMessages([
+ [
+ "system",
+ `Summarize the content found at the provided URL {source}. Please provide a concise and accurate summary,
+ utilizing your understanding of the content's main points, key information, and essential details. Consider summarizing
+ in 1-2 paragraphs. Compose your responses using formal language and you MUST provide you answer in {language}.`,
+ ],
+ new MessagesPlaceholder({ variableName: "agent_scratchpad" }),
+ ]);
+
+ const llm = new ChatOpenAI({
+ temperature: +openAI.BASIC_CHAT_OPENAI_TEMPERATURE,
+ modelName: openAI.GPT_3_5_TURBO_1106.toString(),
+ apiKey: this.configService.get("openai.api_key"),
+ });
+
+ const embeddings = new OpenAIEmbeddings();
+ const tools = [new WebBrowser({ model: llm, embeddings })];
+
+ const agent = await createOpenAIToolsAgent({
+ llm,
+ tools,
+ prompt,
+ });
+
+ const agentExecutor = new AgentExecutor({
+ agent,
+ tools,
+ });
+
+ return agentExecutor.invoke({ source, language });
+ }
}
diff --git a/server/summarization/summarization.module.ts b/server/summarization/summarization.module.ts
deleted file mode 100644
index c979a871b..000000000
--- a/server/summarization/summarization.module.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Module } from "@nestjs/common";
-import { SummarizationChainService } from "./summarization-chain.service";
-import { SummarizationService } from "./summarization.service";
-import { ClaimReviewModule } from "../claim-review/claim-review.module";
-import { AbilityModule } from "../auth/ability/ability.module";
-import { ConfigModule } from "@nestjs/config";
-
-@Module({
- imports: [ClaimReviewModule, AbilityModule, ConfigModule],
- providers: [SummarizationChainService, SummarizationService],
- exports: [SummarizationService],
-})
-export class SummarizationModule {}
diff --git a/server/tests/claim-review.e2e.spec.ts b/server/tests/claim-review.e2e.spec.ts
index 6bf45b180..938f1ce09 100644
--- a/server/tests/claim-review.e2e.spec.ts
+++ b/server/tests/claim-review.e2e.spec.ts
@@ -134,7 +134,7 @@ describe("ClaimReviewController (e2e)", () => {
.expect(200)
.expect(({ body }) => {
claimReviewId = body.review._id;
- expect(body.review.claim).toEqual(claimId.toString());
+ expect(body.review.target).toEqual(claimId.toString());
expect(body.review.personality).toEqual(
personalitiesId[0].toString()
);
diff --git a/server/tests/claim.e2e.spec.ts b/server/tests/claim.e2e.spec.ts
index c22e9f134..54a11187a 100644
--- a/server/tests/claim.e2e.spec.ts
+++ b/server/tests/claim.e2e.spec.ts
@@ -19,7 +19,10 @@ describe("ClaimController (e2e)", () => {
let db: any;
let personalitiesId: string[];
let claimId: string;
- const sources: string[] = ["http://wikipedia.org"];
+ const speechSources: string[] = ["http://wikipedia.org"];
+ const imageSources1: string[] = ["http://wikimedia.org"];
+ const imageSources2: string[] = ["http://wikidata.org"];
+ const debateSources: string[] = ["http://aletheiafact.org"];
const date: string = "2023-11-25T14:49:30.992Z";
beforeAll(async () => {
@@ -69,7 +72,7 @@ describe("ClaimController (e2e)", () => {
content: "Speech Claim Content Lorem Ipsum Dolor Sit Amet...",
slug: "speech-claim-title",
date,
- sources,
+ speechSources,
})
.expect(201)
.expect(({ body }) => {
@@ -102,7 +105,7 @@ describe("ClaimController (e2e)", () => {
nameSpace: NameSpaceEnum.Main,
title: "Image Claim Title With Personality",
date,
- sources,
+ imageSources1,
content: {
FileURL: "http://localhost:4566/aletheia/imageTest1.png",
Key: "imageTest1.png",
@@ -127,7 +130,7 @@ describe("ClaimController (e2e)", () => {
nameSpace: NameSpaceEnum.Main,
title: "Image Claim Title Without Personality",
date,
- sources,
+ imageSources2,
content: {
FileURL: "http://localhost:4566/aletheia/imageTest2.png",
Key: "imageTest2.png",
@@ -152,7 +155,7 @@ describe("ClaimController (e2e)", () => {
nameSpace: NameSpaceEnum.Main,
title: "Debate Claim Title",
date,
- sources,
+ debateSources,
})
.expect(201)
.expect(({ body }) =>
diff --git a/server/tests/source.e2e.spec.ts b/server/tests/source.e2e.spec.ts
new file mode 100644
index 000000000..dd9f7ad21
--- /dev/null
+++ b/server/tests/source.e2e.spec.ts
@@ -0,0 +1,133 @@
+import { MongoMemoryServer } from "mongodb-memory-server";
+import * as request from "supertest";
+import { Test, TestingModule } from "@nestjs/testing";
+import { AppModule } from "../app.module";
+import { SessionGuard } from "../auth/session.guard";
+import { SessionGuardMock } from "./mocks/SessionGuardMock";
+import { TestConfigOptions } from "./utils/TestConfigOptions";
+import { SeedTestUser } from "./utils/SeedTestUser";
+import { AbilitiesGuard } from "../auth/ability/abilities.guard";
+import { AbilitiesGuardMock } from "./mocks/AbilitiesGuardMock";
+import { NameSpaceEnum } from "../auth/name-space/schemas/name-space.schema";
+import { SeedTestPersonality } from "./utils/SeedTestPersonality";
+import { SeedTestClaim } from "./utils/SeedTestClaim";
+const ObjectId = require("mongodb").ObjectID;
+
+jest.setTimeout(10000);
+
+describe("SourceController (e2e)", () => {
+ let app: any;
+ let db: any;
+ let userId: string;
+ let personalitiesId: string[];
+ let claimId: string;
+ let targetId: string;
+
+ beforeAll(async () => {
+ db = await MongoMemoryServer.create({ instance: { port: 35025 } });
+ const user = await SeedTestUser(
+ TestConfigOptions.config.db.connection_uri
+ );
+ userId = user.insertedId;
+
+ const { insertedIds } = await SeedTestPersonality(
+ TestConfigOptions.config.db.connection_uri
+ );
+ personalitiesId = [insertedIds["0"], insertedIds["1"]];
+
+ const claimRevisionId = new ObjectId();
+ const claim = await SeedTestClaim(
+ TestConfigOptions.config.db.connection_uri,
+ personalitiesId,
+ claimRevisionId
+ );
+ claimId = claim.insertedId;
+
+ const moduleFixture: TestingModule = await Test.createTestingModule({
+ imports: [AppModule.register(TestConfigOptions.config)],
+ })
+ .overrideProvider(SessionGuard)
+ .useValue(SessionGuardMock)
+ .overrideGuard(AbilitiesGuard)
+ .useValue(AbilitiesGuardMock)
+ .compile();
+
+ app = moduleFixture.createNestApplication();
+ await app.init();
+ });
+
+ it("/api/source (GET) - sources should be empty", () => {
+ return request(app.getHttpServer())
+ .get("/api/source")
+ .query({
+ page: 0,
+ order: "asc",
+ pageSize: 5,
+ language: "pt",
+ nameSpace: NameSpaceEnum.Main,
+ })
+ .expect(200)
+ .expect(({ body }) => expect(body?.sources.length).toEqual(0));
+ });
+
+ it("/api/source (POST) - should create a new source with valid captcha", async () => {
+ const sourceData = {
+ href: "https://www.wikipedia.org/",
+ props: {
+ summary: "mock_summary",
+ classification: "mock_classification",
+ },
+ user: ObjectId(userId),
+ nameSpace: NameSpaceEnum.Main,
+ targetId: claimId,
+ recaptcha: "valid_recaptcha_token",
+ };
+
+ return request(app.getHttpServer())
+ .post(`/api/source`)
+ .send(sourceData)
+ .expect(201)
+ .expect(({ body }) => {
+ targetId = body.targetId;
+ expect(body.href).toEqual("https://www.wikipedia.org/");
+ });
+ });
+
+ it("/api/source/target/:targetId (GET) - Should get sources by targetId", () => {
+ return request(app.getHttpServer())
+ .get(`/api/source/target/${targetId}`)
+ .query({
+ page: 0,
+ order: "asc",
+ pageSize: 5,
+ language: "pt",
+ nameSpace: NameSpaceEnum.Main,
+ })
+ .expect(200)
+ .expect(({ body }) => {
+ expect(body.sources[0].targetId).toEqual(targetId);
+ });
+ });
+
+ it("/api/source (GET) - should findAll sources", () => {
+ return request(app.getHttpServer())
+ .get("/api/source")
+ .query({
+ page: 0,
+ order: "asc",
+ pageSize: 5,
+ language: "pt",
+ nameSpace: NameSpaceEnum.Main,
+ })
+ .expect(200)
+ .expect(({ body }) => {
+ expect(body?.sources.length).toEqual(1);
+ });
+ });
+
+ afterAll(async () => {
+ jest.restoreAllMocks();
+ await db.stop();
+ app.close();
+ });
+});
diff --git a/server/tests/utils/ClaimReviewMock.ts b/server/tests/utils/ClaimReviewMock.ts
index f9b6ee6df..677298429 100644
--- a/server/tests/utils/ClaimReviewMock.ts
+++ b/server/tests/utils/ClaimReviewMock.ts
@@ -8,7 +8,8 @@ export const ReviewMock = (claimId, personalitiesId, reportId, userId) => ({
isDeleted: false,
deletedAt: null,
personality: ObjectId(personalitiesId[0]),
- claim: ObjectId(claimId),
+ target: ObjectId(claimId),
+ targetModel: "Claim",
usersId: [ObjectId(userId)],
report: ObjectId(reportId),
data_hash: "4be75d25957a3cc0dbc6975a6939a385",
diff --git a/server/tests/utils/ClaimRevisionMock.ts b/server/tests/utils/ClaimRevisionMock.ts
index 3c28e7de9..368508f8a 100644
--- a/server/tests/utils/ClaimRevisionMock.ts
+++ b/server/tests/utils/ClaimRevisionMock.ts
@@ -9,7 +9,7 @@ export const ClaimRevisionMock = (
_id: claimRevisionId,
personalities: [ObjectId(personalitiesId[0])],
contentModel: "Speech",
- tittle: "Mock Tittle",
+ title: "Mock Tittle",
date: "2024-04-18T16:31:16.372+00:00",
claimId: ObjectId(claimId),
slug: "test",
diff --git a/server/types/enums.ts b/server/types/enums.ts
index 708169325..fa6405a64 100644
--- a/server/types/enums.ts
+++ b/server/types/enums.ts
@@ -8,6 +8,13 @@ enum ContentModelEnum {
enum ReportModelEnum {
FactChecking = "Fact-checking",
InformativeNews = "Informative News",
+ Request = "Request",
}
-export { ContentModelEnum, ReportModelEnum };
+enum ReviewTaskTypeEnum {
+ Claim = "Claim",
+ Source = "Source",
+ VerificationRequest = "VerificationRequest",
+}
+
+export { ContentModelEnum, ReportModelEnum, ReviewTaskTypeEnum };
diff --git a/server/verification-request/dto/create-verification-request-dto.ts b/server/verification-request/dto/create-verification-request-dto.ts
new file mode 100644
index 000000000..0dae5221d
--- /dev/null
+++ b/server/verification-request/dto/create-verification-request-dto.ts
@@ -0,0 +1,23 @@
+import { ApiProperty } from "@nestjs/swagger";
+import { IsArray, IsDate, IsOptional, IsString } from "class-validator";
+
+export class CreateVerificationRequestDTO {
+ @IsString()
+ @ApiProperty()
+ content: string;
+
+ @IsDate()
+ @ApiProperty()
+ @IsOptional()
+ date: Date;
+
+ @IsArray()
+ @ApiProperty()
+ @IsOptional()
+ sources: string[];
+
+ @IsString()
+ @IsOptional()
+ @ApiProperty()
+ data_hash: string;
+}
diff --git a/server/verification-request/dto/update-verification-request.dto.ts b/server/verification-request/dto/update-verification-request.dto.ts
new file mode 100644
index 000000000..4882166e9
--- /dev/null
+++ b/server/verification-request/dto/update-verification-request.dto.ts
@@ -0,0 +1,36 @@
+import { PartialType } from "@nestjs/mapped-types";
+import { CreateVerificationRequestDTO } from "./create-verification-request-dto";
+import { IsArray, IsBoolean, IsOptional, IsString } from "class-validator";
+import { ApiProperty } from "@nestjs/swagger";
+import { Group } from "../../group/schemas/group.schema";
+import { Transform } from "class-transformer";
+
+export class UpdateVerificationRequestDTO extends PartialType(
+ CreateVerificationRequestDTO
+) {
+ @IsString()
+ @IsOptional()
+ @ApiProperty()
+ targetId: string;
+
+ @IsArray()
+ @IsOptional()
+ @ApiProperty()
+ group: Group;
+
+ @IsOptional()
+ @ApiProperty()
+ usersId: string[];
+
+ @IsBoolean()
+ @IsOptional()
+ @ApiProperty()
+ isSensitive: boolean;
+
+ @ApiProperty()
+ @IsOptional()
+ @Transform(({ value }) => {
+ return [true, "enabled", "true", 1, "1"].indexOf(value) > -1;
+ })
+ rejected?: boolean;
+}
diff --git a/server/verification-request/schemas/verification-request.schema.ts b/server/verification-request/schemas/verification-request.schema.ts
new file mode 100644
index 000000000..c4f7f3d63
--- /dev/null
+++ b/server/verification-request/schemas/verification-request.schema.ts
@@ -0,0 +1,34 @@
+import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
+import * as mongoose from "mongoose";
+import { Group } from "../../group/schemas/group.schema";
+
+export type VerificationRequestDocument = VerificationRequest &
+ mongoose.Document;
+
+@Schema()
+export class VerificationRequest {
+ @Prop({ required: true, unique: true })
+ data_hash: string;
+
+ @Prop({ required: true, type: String })
+ content: string;
+
+ @Prop({ required: false, default: new Date() })
+ date: Date;
+
+ @Prop({
+ type: mongoose.Types.ObjectId,
+ required: false,
+ ref: "Group",
+ })
+ group: Group;
+
+ @Prop({ required: false, type: Boolean })
+ rejected: boolean;
+
+ @Prop({ required: false, type: Boolean })
+ isSensitive: boolean;
+}
+
+export const VerificationRequestSchema =
+ SchemaFactory.createForClass(VerificationRequest);
diff --git a/server/verification-request/verification-request.controller.ts b/server/verification-request/verification-request.controller.ts
new file mode 100644
index 000000000..61bfd7ea7
--- /dev/null
+++ b/server/verification-request/verification-request.controller.ts
@@ -0,0 +1,153 @@
+import {
+ Controller,
+ Post,
+ Body,
+ Get,
+ Res,
+ Req,
+ Header,
+ Query,
+ Param,
+ Put,
+} from "@nestjs/common";
+import { ApiTags } from "@nestjs/swagger";
+import { VerificationRequestService } from "./verification-request.service";
+import type { BaseRequest } from "../types";
+import { parse } from "url";
+import { ConfigService } from "@nestjs/config";
+import { ViewService } from "../view/view.service";
+import type { Response } from "express";
+import { ReviewTaskService } from "../review-task/review-task.service";
+import { CreateVerificationRequestDTO } from "./dto/create-verification-request-dto";
+import { UpdateVerificationRequestDTO } from "./dto/update-verification-request.dto";
+import { IsPublic } from "../auth/decorators/is-public.decorator";
+
+@Controller(":namespace?")
+export class VerificationRequestController {
+ constructor(
+ private verificationRequestService: VerificationRequestService,
+ private configService: ConfigService,
+ private viewService: ViewService,
+ private reviewTaskService: ReviewTaskService
+ ) {}
+
+ @ApiTags("verification-request")
+ @Get("api/verification-request")
+ @Header("Cache-Control", "max-age=60, must-revalidate")
+ public async listAll(@Query() getVerificationRequest) {
+ const { pageSize, page } = getVerificationRequest;
+
+ return Promise.all([
+ this.verificationRequestService.listAll(getVerificationRequest),
+ this.verificationRequestService.count({}),
+ ]).then(([verificationRequests, totalVerificationRequests]) => {
+ const totalPages = Math.ceil(totalVerificationRequests / pageSize);
+
+ return {
+ verificationRequests,
+ totalVerificationRequests,
+ totalPages,
+ page: page,
+ pageSize: pageSize,
+ };
+ });
+ }
+
+ @ApiTags("verification-request")
+ @Get("api/verification-request/search")
+ @Header("Cache-Control", "max-age=60, must-revalidate")
+ public async getAll(@Query() getVerificationRequest) {
+ return this.verificationRequestService.findAll(getVerificationRequest);
+ }
+
+ @ApiTags("verification-request")
+ @Get("api/verification-request/:id")
+ @Header("Cache-Control", "max-age=60, must-revalidate")
+ public async getById(@Param("id") verificationRequestId: string) {
+ return this.verificationRequestService.getById(verificationRequestId);
+ }
+
+ @ApiTags("verification-request")
+ @Post("api/verification-request")
+ create(@Body() verificationRequestBody: CreateVerificationRequestDTO) {
+ return this.verificationRequestService.create(verificationRequestBody);
+ }
+
+ @ApiTags("verification-request")
+ @Put("api/verification-request/:verificationRequestId")
+ async updateVerificationRequest(
+ @Param("verificationRequestId") verificationRequestId: string,
+ @Body() updateVerificationRequestDto: UpdateVerificationRequestDTO
+ ) {
+ return this.verificationRequestService.update(
+ verificationRequestId,
+ updateVerificationRequestDto
+ );
+ }
+
+ @ApiTags("verification-request")
+ @Put("api/verification-request/:verificationRequestId/group")
+ async removeVerificationRequestFromGroup(
+ @Param("verificationRequestId") verificationRequestId: string,
+ @Body() { group }: { group: string }
+ ) {
+ return this.verificationRequestService.removeVerificationRequestFromGroup(
+ verificationRequestId,
+ group
+ );
+ }
+
+ @IsPublic()
+ @ApiTags("pages")
+ @Get("verification-request")
+ @Header("Cache-Control", "max-age=60, must-revalidate")
+ public async verificationRequestPage(
+ @Req() req: BaseRequest,
+ @Res() res: Response
+ ) {
+ const parsedUrl = parse(req.url, true);
+
+ await this.viewService.getNextServer().render(
+ req,
+ res,
+ "/verification-request-page",
+ Object.assign(parsedUrl.query, {
+ nameSpace: req.params.namespace,
+ })
+ );
+ }
+
+ @IsPublic()
+ @ApiTags("pages")
+ @Get("verification-request/:dataHash")
+ @Header("Cache-Control", "max-age=60, must-revalidate")
+ public async verificationRequestReviewPage(
+ @Req() req: BaseRequest,
+ @Res() res: Response
+ ) {
+ const parsedUrl = parse(req.url, true);
+ const { dataHash } = req.params;
+
+ const verificationRequest =
+ await this.verificationRequestService.findByDataHash(dataHash);
+
+ const reviewTask =
+ await this.reviewTaskService.getReviewTaskByDataHashWithUsernames(
+ dataHash
+ );
+
+ await this.viewService.getNextServer().render(
+ req,
+ res,
+ "/verification-request-review-page",
+ Object.assign(parsedUrl.query, {
+ reviewTask,
+ sitekey: this.configService.get("recaptcha_sitekey"),
+ hideDescriptions: {},
+ websocketUrl: this.configService.get("websocketUrl"),
+ nameSpace: req.params.namespace,
+ verificationRequest,
+ })
+ );
+ }
+}
diff --git a/server/verification-request/verification-request.module.ts b/server/verification-request/verification-request.module.ts
new file mode 100644
index 000000000..ebc62c6e0
--- /dev/null
+++ b/server/verification-request/verification-request.module.ts
@@ -0,0 +1,37 @@
+import { Module } from "@nestjs/common";
+import { MongooseModule } from "@nestjs/mongoose";
+import {
+ VerificationRequestSchema,
+ VerificationRequest,
+} from "./schemas/verification-request.schema";
+import { VerificationRequestService } from "./verification-request.service";
+import { VerificationRequestController } from "./verification-request.controller";
+import { SourceModule } from "../source/source.module";
+import { ViewModule } from "../view/view.module";
+import { ConfigModule } from "@nestjs/config";
+import { ReviewTaskModule } from "../review-task/review-task.module";
+import { HistoryModule } from "../history/history.module";
+import { GroupModule } from "../group/group.module";
+
+const VerificationRequestModel = MongooseModule.forFeature([
+ {
+ name: VerificationRequest.name,
+ schema: VerificationRequestSchema,
+ },
+]);
+
+@Module({
+ imports: [
+ VerificationRequestModel,
+ SourceModule,
+ ViewModule,
+ ConfigModule,
+ ReviewTaskModule,
+ HistoryModule,
+ GroupModule,
+ ],
+ exports: [VerificationRequestService],
+ providers: [VerificationRequestService],
+ controllers: [VerificationRequestController],
+})
+export class VerificationRequestModule {}
diff --git a/server/verification-request/verification-request.service.ts b/server/verification-request/verification-request.service.ts
new file mode 100644
index 000000000..28d39e80c
--- /dev/null
+++ b/server/verification-request/verification-request.service.ts
@@ -0,0 +1,285 @@
+import { Injectable } from "@nestjs/common";
+import { Model, Types } from "mongoose";
+import { SourceService } from "../source/source.service";
+import {
+ VerificationRequest,
+ VerificationRequestDocument,
+} from "./schemas/verification-request.schema";
+import { InjectModel } from "@nestjs/mongoose";
+import { GroupService } from "../group/group.service";
+import { CreateVerificationRequestDTO } from "./dto/create-verification-request-dto";
+import { UpdateVerificationRequestDTO } from "./dto/update-verification-request.dto";
+const md5 = require("md5");
+
+@Injectable()
+export class VerificationRequestService {
+ constructor(
+ @InjectModel(VerificationRequest.name)
+ private VerificationRequestModel: Model,
+ private sourceService: SourceService,
+ private groupService: GroupService
+ ) {}
+
+ async listAll({ page, pageSize, order }): Promise {
+ return this.VerificationRequestModel.find({})
+ .skip(page * parseInt(pageSize, 10))
+ .limit(parseInt(pageSize, 10))
+ .sort({ _id: order })
+ .lean();
+ }
+
+ /**
+ * Find all verification requests that query matches
+ * @param verifiedRequestQuery query parameter
+ * @returns an array of verification request documents
+ */
+ async findAll(verifiedRequestQuery: {
+ searchContent: string;
+ }): Promise {
+ return this.VerificationRequestModel.find({
+ content: {
+ $regex: verifiedRequestQuery.searchContent || "",
+ $options: "i",
+ },
+ });
+ }
+
+ /**
+ * Finds a document by an id parameter
+ * @param verificationRequestId verification request ID string
+ * @returns the verification request document
+ */
+ async getById(verificationRequestId: string): Promise {
+ return this.VerificationRequestModel.findById(
+ verificationRequestId
+ ).populate("group");
+ }
+
+ /**
+ * Creates a new verification request document
+ * For each sources in verification request, creates a new source document
+ * @param verificationRequest verificationRequestBody
+ * @returns the verification request document
+ */
+ create(
+ verificationRequest: CreateVerificationRequestDTO
+ ): Promise {
+ try {
+ verificationRequest.data_hash = md5(verificationRequest.content);
+ const newVerificationRequest = new this.VerificationRequestModel(
+ verificationRequest
+ );
+ if (verificationRequest.sources.length) {
+ for (const source of verificationRequest.sources) {
+ this.sourceService.create({
+ href: source,
+ targetId: newVerificationRequest.id,
+ });
+ }
+ }
+
+ return newVerificationRequest.save();
+ } catch (e) {
+ console.error("Failed to create verification request", e);
+ throw new Error(e);
+ }
+ }
+
+ /**
+ * Finds a document by an data_hash parameter
+ * @param data_hash encrypted hash
+ * @returns the verification request document
+ */
+ async findByDataHash(
+ data_hash: string
+ ): Promise {
+ return this.VerificationRequestModel.findOne({ data_hash }).populate(
+ "group"
+ );
+ }
+
+ /**
+ * Find the removed ids based on an initial object and an updated object
+ * @param initial initial object
+ * @param updated updated object
+ * @returns the removed ids
+ */
+ findRemovedIds(initial, updated): string[] {
+ return initial.content.filter(
+ (id) => !updated.content.includes(id.toString())
+ );
+ }
+
+ /**
+ * Removes a specific verification request document in the group content document
+ * @param verificationRequestId verification request ID
+ * @param groupId group ID
+ * @returns updated verification request document
+ */
+ async removeVerificationRequestFromGroup(
+ verificationRequestId: string,
+ groupId: string
+ ): Promise {
+ try {
+ const verificationRequest =
+ await this.VerificationRequestModel.findById(
+ verificationRequestId
+ );
+
+ await this.groupService.removeContent(
+ groupId,
+ verificationRequest._id
+ );
+
+ return await this.VerificationRequestModel.findByIdAndUpdate(
+ verificationRequest._id,
+ { $unset: { group: null } },
+ { new: true, upsert: true }
+ );
+ } catch (error) {
+ console.error(
+ "Failed to remove verification request from group:",
+ error
+ );
+ throw error;
+ }
+ }
+
+ /**
+ * Updates the verification request document
+ * @param verificationRequestId verification request id
+ * @param verificationRequestBodyUpdate verification request updated object
+ * @param postProcess boolean
+ * @returns the updated verification request document
+ */
+ async update(
+ verificationRequestId: string,
+ verificationRequestBodyUpdate: Partial,
+ postProcess: boolean = true
+ ): Promise {
+ try {
+ const verificationRequest =
+ await this.VerificationRequestModel.findById(
+ verificationRequestId
+ ).populate("group");
+
+ if (!verificationRequest) {
+ throw new Error("Verification request not found");
+ }
+
+ const updatedVerificationRequestData = {
+ ...verificationRequest.toObject(),
+ ...verificationRequestBodyUpdate,
+ };
+
+ if (
+ postProcess &&
+ Array.isArray(updatedVerificationRequestData.group)
+ ) {
+ updatedVerificationRequestData.group =
+ await this.handleGroupPostProcessing(
+ verificationRequest,
+ updatedVerificationRequestData
+ );
+ }
+
+ return await this.VerificationRequestModel.findByIdAndUpdate(
+ verificationRequest._id,
+ updatedVerificationRequestData,
+ { new: true, upsert: true }
+ );
+ } catch (error) {
+ console.error("Failed to update verification request:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * Finds the removed group ids and update their document
+ * Creates a new group document and updates all verification requests that are part of this group with the group ID
+ * @param originalVerificationRequest verification request original object
+ * @param updatedVerificationRequest verification request updated object
+ * @returns the group id
+ */
+ private async handleGroupPostProcessing(
+ originalVerificationRequest,
+ updatedVerificationRequest
+ ) {
+ if (originalVerificationRequest?.group) {
+ await this.delete(
+ originalVerificationRequest,
+ updatedVerificationRequest
+ );
+ }
+
+ return await this.createGroupAndUpdateVerificationRequests(
+ originalVerificationRequest,
+ updatedVerificationRequest
+ );
+ }
+
+ /**
+ * Finds the removed group ids and update their document
+ * @param originalVerificationRequest verification request original object
+ * @param updatedVerificationRequest verification request updated object
+ */
+ private async delete(
+ originalVerificationRequest,
+ updatedVerificationRequest
+ ) {
+ const removedRequestIds = await Promise.all(
+ this.findRemovedIds(originalVerificationRequest?.group, {
+ content: [
+ originalVerificationRequest._id.toString(),
+ ...updatedVerificationRequest.group,
+ ],
+ })
+ );
+
+ const flattenedRemovedIds = removedRequestIds.flat();
+
+ if (flattenedRemovedIds?.length) {
+ await Promise.all(
+ flattenedRemovedIds.map((id) =>
+ this.update(id, { group: null }, false)
+ )
+ );
+ }
+ }
+
+ /**
+ * Creates a new group document and updates all verification requests that are part of this group with the group ID
+ * @param originalVerificationRequest verification request original object
+ * @param updatedVerificationRequest verification request updated object
+ * @returns the group id
+ */
+ private async createGroupAndUpdateVerificationRequests(
+ originalVerificationRequest,
+ updatedVerificationRequest
+ ) {
+ const contentIds =
+ updatedVerificationRequest?.group?.map((item) =>
+ Types.ObjectId(item?._id || item)
+ ) || [];
+
+ const groupId = (
+ await this.groupService.create({
+ content: [originalVerificationRequest._id, ...contentIds],
+ })
+ )._id;
+
+ if (contentIds.length) {
+ await Promise.all(
+ contentIds.map((itemId) =>
+ this.update(itemId, { group: groupId }, false)
+ )
+ );
+ }
+
+ return groupId;
+ }
+
+ count(query) {
+ return this.VerificationRequestModel.countDocuments().where(query);
+ }
+}
diff --git a/src/api/AutomatedFactCheckingApi.ts b/src/api/AutomatedFactCheckingApi.ts
index 5dfc63afe..0ba31b5fd 100644
--- a/src/api/AutomatedFactCheckingApi.ts
+++ b/src/api/AutomatedFactCheckingApi.ts
@@ -5,7 +5,7 @@ const request = axios.create({
baseURL: `/api/ai-fact-checking`,
});
-const createClaimReviewTaskUsingAIAgents = (params) => {
+const createReviewTaskUsingAIAgents = (params) => {
return request
.post("/", { ...params })
.then((response) => {
@@ -17,7 +17,7 @@ const createClaimReviewTaskUsingAIAgents = (params) => {
};
const AutomatedFactCheckingApi = {
- createClaimReviewTaskUsingAIAgents,
+ createReviewTaskUsingAIAgents,
};
export default AutomatedFactCheckingApi;
diff --git a/src/api/ClaimReviewTaskApi.ts b/src/api/reviewTaskApi.ts
similarity index 80%
rename from src/api/ClaimReviewTaskApi.ts
rename to src/api/reviewTaskApi.ts
index cf98334a3..e4d54829e 100644
--- a/src/api/ClaimReviewTaskApi.ts
+++ b/src/api/reviewTaskApi.ts
@@ -4,16 +4,17 @@ import { NameSpaceEnum } from "../types/Namespace";
const request = axios.create({
withCredentials: true,
- baseURL: `/api/claimreviewtask`,
+ baseURL: `/api/reviewtask`,
});
-const getClaimReviewTasks = (options) => {
+const getReviewTasks = (options) => {
const params = {
page: options.page ? options.page - 1 : 0,
order: options.order || "asc",
pageSize: options.pageSize ? options.pageSize : 5,
value: options.value,
filterUser: options.filterUser,
+ reviewTaskType: options.reviewTaskType,
nameSpace: options.nameSpace ? options.nameSpace : NameSpaceEnum.Main,
};
return request
@@ -43,15 +44,15 @@ const getMachineByDataHash = (params) => {
});
};
-const createClaimReviewTask = (params, t, type) => {
+const createReviewTask = (params, t, type) => {
return request
.post("/", { ...params })
.then((response) => {
- message.success(t(`claimReviewTask:${type}_SUCCESS`));
+ message.success(t(`reviewTask:${type}_SUCCESS`));
return response.data;
})
.catch((err) => {
- message.error(t(`claimReviewTask:${type}_ERROR`));
+ message.error(t(`reviewTask:${type}_ERROR`));
throw err;
});
};
@@ -60,7 +61,7 @@ const autoSaveDraft = (params, t) => {
return request
.put(`/${params.data_hash}`, { ...params })
.then((response) => {
- message.success(t(`claimReviewTask:SAVE_DRAFT_SUCCESS`));
+ message.success(t(`reviewTask:SAVE_DRAFT_SUCCESS`));
return response.data;
})
.catch((err) => {
@@ -68,9 +69,9 @@ const autoSaveDraft = (params, t) => {
});
};
-const getEditorContentObject = (data_hash, reportModel) => {
+const getEditorContentObject = (data_hash, params) => {
return request
- .get(`/editor-content/${data_hash}`, { params: { reportModel } })
+ .get(`/editor-content/${data_hash}`, { params })
.then((response) => {
return response.data;
})
@@ -101,14 +102,14 @@ const deleteComment = (hash, commentId) => {
});
};
-const ClaimReviewTaskApi = {
+const ReviewTaskApi = {
getMachineByDataHash,
- createClaimReviewTask,
- getClaimReviewTasks,
+ createReviewTask,
+ getReviewTasks,
autoSaveDraft,
getEditorContentObject,
addComment,
deleteComment,
};
-export default ClaimReviewTaskApi;
+export default ReviewTaskApi;
diff --git a/src/api/sourceApi.ts b/src/api/sourceApi.ts
index c484488a7..235f965c1 100644
--- a/src/api/sourceApi.ts
+++ b/src/api/sourceApi.ts
@@ -23,7 +23,7 @@ const getByTargetId = (options: optionsType) => {
language: options?.i18n?.languages[0],
};
return request
- .get(`${options.targetId}`, { params })
+ .get(`/target/${options.targetId}`, { params })
.then((response) => {
const { sources, totalPages, totalSources } = response.data;
return {
@@ -82,10 +82,22 @@ const createSource = (t, router, source: any = {}) => {
});
};
+const getById = (id, t, params = {}) => {
+ return request
+ .get(`/${id}`, { params })
+ .then((response) => {
+ return response.data;
+ })
+ .catch(() => {
+ message.error("error"); //TODO: Improve feedback message
+ });
+};
+
const SourceApi = {
getByTargetId,
createSource,
get,
+ getById,
};
export default SourceApi;
diff --git a/src/api/summarizationApi.ts b/src/api/summarizationApi.ts
new file mode 100644
index 000000000..e2a0cd75c
--- /dev/null
+++ b/src/api/summarizationApi.ts
@@ -0,0 +1,23 @@
+import axios from "axios";
+
+const request = axios.create({
+ withCredentials: true,
+ baseURL: `/api/summarization`,
+});
+
+const summarizeSource = (source) => {
+ return request
+ .get("/", { params: { source } })
+ .then((response) => {
+ return response.data;
+ })
+ .catch((err) => {
+ throw err;
+ });
+};
+
+const SummarizationApi = {
+ summarizeSource,
+};
+
+export default SummarizationApi;
diff --git a/src/api/verificationRequestApi.ts b/src/api/verificationRequestApi.ts
new file mode 100644
index 000000000..61e3d19d7
--- /dev/null
+++ b/src/api/verificationRequestApi.ts
@@ -0,0 +1,89 @@
+import axios from "axios";
+import { NameSpaceEnum } from "../types/Namespace";
+
+const request = axios.create({
+ withCredentials: true,
+ baseURL: `/api/verification-request`,
+});
+
+const get = (options) => {
+ const params = {
+ page: options.page ? options.page - 1 : 0,
+ order: options.order || "asc",
+ pageSize: options.pageSize ? options.pageSize : 10,
+ nameSpace: options?.nameSpace || NameSpaceEnum.Main,
+ };
+
+ return request
+ .get(`/`, { params })
+ .then((response) => {
+ const {
+ verificationRequests,
+ totalPages,
+ totalVerificationRequests,
+ } = response.data;
+
+ return {
+ data: verificationRequests,
+ total: totalVerificationRequests,
+ totalPages,
+ };
+ })
+ .catch((err) => {
+ console.log(err);
+ });
+};
+
+const getVerificationRequests = (params) => {
+ return request
+ .get(`/search`, { params })
+ .then((response) => {
+ return response?.data;
+ })
+ .catch((e) => {
+ console.error("error while getting verification requests", e);
+ });
+};
+
+const getById = (id, _t = null, params = {}) => {
+ return request
+ .get(`/${id}`, { params })
+ .then((response) => {
+ return response.data;
+ })
+ .catch((e) => {
+ console.error("error while getting verification request", e);
+ });
+};
+
+const updateVerificationRequest = (id, params) => {
+ return request
+ .put(`/${id}`, params)
+ .then((response) => {
+ return response.data;
+ })
+ .catch((e) => {
+ console.error("error while updating verification request", e);
+ });
+};
+
+const removeVerificationRequestFromGroup = (id, params) => {
+ return request
+ .put(`/${id}/group`, params)
+ .then((response) => {
+ return response.data;
+ })
+ .catch((e) => {
+ console.error("error while removing verification request", e);
+ });
+};
+
+const verificationRequestApi = {
+ get,
+ getVerificationRequests,
+ getById,
+ updateVerificationRequest,
+ removeVerificationRequestFromGroup,
+};
+
+export default verificationRequestApi;
diff --git a/src/components/AffixButton/AffixButton.tsx b/src/components/AffixButton/AffixButton.tsx
index cb6dd465b..f4cd22fba 100644
--- a/src/components/AffixButton/AffixButton.tsx
+++ b/src/components/AffixButton/AffixButton.tsx
@@ -73,9 +73,9 @@ const AffixButton = ({ personalitySlug }: AffixButtonProps) => {
tooltip: t("affix:affixButtonCreateVerifiedSources"),
href:
nameSpace !== NameSpaceEnum.Main
- ? `/${nameSpace}/sources/create`
- : `/sources/create`,
- dataCy: "testFloatButtonAddVerifiedSources",
+ ? `/${nameSpace}/source/create`
+ : `/source/create`,
+ dataCy: "testFloatButtonAddSources",
}
);
diff --git a/src/components/AletheiaMenu.tsx b/src/components/AletheiaMenu.tsx
index ea9814402..ee01ea913 100644
--- a/src/components/AletheiaMenu.tsx
+++ b/src/components/AletheiaMenu.tsx
@@ -67,15 +67,27 @@ const AletheiaMenu = () => {
{t("menu:sourcesItem")}
+
+ {t("menu:verificationRequestItem")}
+
+
{role !== Roles.Regular && (
{
const { t } = useTranslation();
const dispatch = useDispatch();
- const { selectedClaim } = useAppSelector((state) => state);
+ const { selectedTarget } = useAppSelector((state) => state);
const review = claim?.stats?.reviews[0];
const paragraphs = content || claim.content;
const [claimContent, setClaimContent] = useState("");
@@ -45,7 +45,7 @@ const ClaimCard = ({
const isUnattributed =
claim?.contentModel === ContentModelEnum.Unattributed;
const isInsideDebate =
- selectedClaim?.contentModel === ContentModelEnum.Debate;
+ selectedTarget?.contentModel === ContentModelEnum.Debate;
const shouldCreateFirstParagraph = isSpeech || isUnattributed;
const dispatchPersonalityAndClaim = () => {
@@ -53,7 +53,7 @@ const ClaimCard = ({
// when selecting a claim from the debate page to review or to read,
// we don't want to change the selected claim
// se we can keep reference to the debate
- dispatch(actions.setSelectClaim(claim));
+ dispatch(actions.setSelectTarget(claim));
}
dispatch(actions.setSelectPersonality(personality));
};
diff --git a/src/components/Claim/ClaimInfo.tsx b/src/components/Claim/ClaimInfo.tsx
index cf678b2b0..208577fc4 100644
--- a/src/components/Claim/ClaimInfo.tsx
+++ b/src/components/Claim/ClaimInfo.tsx
@@ -17,21 +17,26 @@ const ClaimInfoParagraph = styled(Paragraph)`
color: ${colors.blackSecondary};
`;
-const ClaimInfo = ({ isImage, date }) => {
+const ClaimInfo = ({
+ isImage,
+ date,
+ speechTypeTranslation = "claim:typeSpeech",
+ style = {},
+}) => {
const { t } = useTranslation();
return (
<>
{!isImage ? (
-
+
{t("claim:cardHeader1")}
{t("claim:cardHeader2")}
- {t("claim:typeSpeech")}
+ {t(speechTypeTranslation)}
) : (
-
+
{t("claim:cardHeader3")}
diff --git a/src/components/Claim/ClaimSummaryContent.tsx b/src/components/Claim/ClaimSummaryContent.tsx
index 93f0a7337..e197f23a8 100644
--- a/src/components/Claim/ClaimSummaryContent.tsx
+++ b/src/components/Claim/ClaimSummaryContent.tsx
@@ -1,13 +1,9 @@
import colors from "../../styles/colors";
-import { Col, Typography } from "antd";
+import { Col } from "antd";
import React from "react";
import { useTranslation } from "next-i18next";
-import ImageClaim from "../ImageClaim";
import { ContentModelEnum } from "../../types/enums";
-import { NameSpaceEnum } from "../../types/Namespace";
-import { useAtom } from "jotai";
-import { currentNameSpace } from "../../atoms/namespace";
-const { Paragraph } = Typography;
+import ReviewContent from "../ClaimReview/ReviewContent";
interface ClaimSummaryContentProps {
claimContent: any;
@@ -25,7 +21,6 @@ const ClaimSummaryContent = ({
contentModel,
}: ClaimSummaryContentProps) => {
const { t } = useTranslation();
- const [nameSpace] = useAtom(currentNameSpace);
const isImage = contentModel === ContentModelEnum.Image;
const contentProps = {
[ContentModelEnum.Speech]: {
@@ -62,47 +57,32 @@ const ClaimSummaryContent = ({
return (
-
-
-
- {title}
-
- {isImage && (
-
- )}
-
-
-
- {t(linkText)}
-
+
+ {title}
+
+ )
+ }
+ content={claimContent.content}
+ isImage={isImage}
+ contentPath={href}
+ linkText={t(linkText)}
+ ellipsis={true}
+ />
);
};
diff --git a/src/components/Claim/ClaimView.tsx b/src/components/Claim/ClaimView.tsx
index 31e6977c0..c5ee93ecd 100644
--- a/src/components/Claim/ClaimView.tsx
+++ b/src/components/Claim/ClaimView.tsx
@@ -29,14 +29,14 @@ const ClaimView = ({ personality, claim, href, hideDescriptions }) => {
const sources = claim?.sources?.map((source) => source.href);
const dispatchPersonalityAndClaim = () => {
- dispatch(actions.setSelectClaim(claim));
+ dispatch(actions.setSelectTarget(claim));
dispatch(actions.setSelectPersonality(personality));
};
const [showHighlights, setShowHighlights] = useState(true);
useEffect(() => {
- dispatch(actions.setSelectClaim(claim));
+ dispatch(actions.setSelectTarget(claim));
dispatch(actions.setSelectPersonality(personality));
if (isImage) {
dispatch(actions.setSelectContent(claimContent));
diff --git a/src/components/Claim/CreateClaim/ClaimSelectPersonality.tsx b/src/components/Claim/CreateClaim/ClaimSelectPersonality.tsx
index 2c93601c7..661545a33 100644
--- a/src/components/Claim/CreateClaim/ClaimSelectPersonality.tsx
+++ b/src/components/Claim/CreateClaim/ClaimSelectPersonality.tsx
@@ -113,7 +113,12 @@ const ClaimSelectPersonality = () => {
personalities.map((personality) => (
-
+
diff --git a/src/components/Claim/CreateClaim/CreateClaimView.tsx b/src/components/Claim/CreateClaim/CreateClaimView.tsx
index 4435da8dc..73cf1b696 100644
--- a/src/components/Claim/CreateClaim/CreateClaimView.tsx
+++ b/src/components/Claim/CreateClaim/CreateClaimView.tsx
@@ -1,6 +1,6 @@
import { Col, Row } from "antd";
import { useAtom } from "jotai";
-import React from "react";
+import React, { useState } from "react";
import { createClaimMachineAtom } from "../../../machines/createClaim/provider";
import {
@@ -17,9 +17,18 @@ import ClaimSelectPersonality from "./ClaimSelectPersonality";
import ClaimSelectType from "./ClaimSelectType";
import ClaimUploadImage from "./ClaimUploadImage";
import { CreateClaimHeader } from "./CreateClaimHeader";
+import colors from "../../../styles/colors";
+import LargeDrawer from "../../LargeDrawer";
+import VerificationRequestCard from "../../VerificationRequest/VerificationRequestCard";
+import AletheiaButton from "../../Button";
+import { DeleteOutlined } from "@ant-design/icons";
+import { CreateClaimEvents } from "../../../machines/createClaim/types";
+import verificationRequestApi from "../../../api/verificationRequestApi";
+import { useTranslation } from "next-i18next";
const CreateClaimView = () => {
- const [state] = useAtom(createClaimMachineAtom);
+ const { t } = useTranslation();
+ const [state, send] = useAtom(createClaimMachineAtom);
const setupImage = stateSelector(state, "setupImage");
const notStarted = stateSelector(state, "notStarted");
const setupSpeech = stateSelector(state, "setupSpeech");
@@ -42,9 +51,58 @@ const CreateClaimView = () => {
addUnattributed
);
+ const [open, setOpen] = useState(false);
+ const onCloseDrawer = () => {
+ setOpen(false);
+ };
+
+ const onRemove = (id) => {
+ //TODO: Show confirmation dialog
+ const contentGroup = claimData.group.content.filter(
+ (verificationRequest) => verificationRequest?._id !== id
+ );
+ verificationRequestApi
+ .removeVerificationRequestFromGroup(id, {
+ group: claimData.group._id,
+ })
+ .then(() => {
+ send({
+ type: CreateClaimEvents.updateGroup,
+ claimData: {
+ group: { ...claimData.group, content: contentGroup },
+ },
+ });
+ });
+ };
+
return (
-
+
+ {!isLoading &&
+ claimData?.group &&
+ claimData?.group?.content?.length > 0 && (
+ setOpen(true)}
+ role="button"
+ aria-pressed="false"
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === "Space") {
+ setOpen(true);
+ e.preventDefault();
+ }
+ }}
+ style={{
+ color: colors.lightBlueMain,
+ textDecoration: "underline",
+ cursor: "pointer",
+ }}
+ >
+ {t(
+ "verificationRequest:manageVerificationRequests"
+ )}
+
+ )}
{showPersonality && !!claimData.personalities?.length && (
)}
@@ -58,6 +116,35 @@ const CreateClaimView = () => {
{isLoading && }
{addUnattributed && }
+
+
+
+ {t("verificationRequest:verificationRequestTitle")}
+ {claimData?.group ? (
+ claimData.group.content.map(({ _id, content }) => (
+ onRemove(_id)}
+ loading={isLoading}
+ >
+
+ ,
+ ]}
+ />
+ ))
+ ) : (
+ <>>
+ )}
+
+
);
};
diff --git a/src/components/Claim/CreateSource/BaseSourceForm.tsx b/src/components/Claim/CreateSource/BaseSourceForm.tsx
deleted file mode 100644
index dc61936b6..000000000
--- a/src/components/Claim/CreateSource/BaseSourceForm.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-import React, { useState } from "react";
-import { Checkbox, Col, Form, Row } from "antd";
-import { useTranslation } from "next-i18next";
-import { useRouter } from "next/router";
-
-import colors from "../../../styles/colors";
-import AletheiaCaptcha from "../../AletheiaCaptcha";
-import Input from "../../AletheiaInput";
-import Button, { ButtonType } from "../../Button";
-import TextArea from "../../TextArea";
-import { SelectInput } from "../../Form/ClaimReviewSelect";
-import { Option } from "antd/lib/mentions";
-import ClassificationText from "../../ClassificationText";
-
-interface BaseSourceFormProps {
- handleSubmit: (values: any) => void;
- disableFutureDates?: boolean;
- isLoading: boolean;
- disclaimer?: string;
- dateExtraText: string;
-}
-
-const BaseSourceForm = ({
- handleSubmit,
- isLoading,
- disclaimer,
-}: BaseSourceFormProps) => {
- const { t } = useTranslation();
- const router = useRouter();
- const [disableSubmit, setDisableSubmit] = useState(true);
- const [recaptcha, setRecaptcha] = useState("");
-
- const onChangeCaptcha = (captchaString) => {
- setRecaptcha(captchaString);
- const hasRecaptcha = !!captchaString;
- setDisableSubmit(!hasRecaptcha);
- };
-
- const onFinish = (values) => {
- handleSubmit({ ...values, recaptcha });
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {disclaimer && (
-
- {disclaimer}
-
- )}
-
-
- {t("claimForm:checkboxAcceptTerms")}
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default BaseSourceForm;
diff --git a/src/components/Claim/CreateSource/CreateSourceView.tsx b/src/components/Claim/CreateSource/CreateSourceView.tsx
deleted file mode 100644
index 3c47a502c..000000000
--- a/src/components/Claim/CreateSource/CreateSourceView.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React, { useState } from "react";
-import { useTranslation } from "next-i18next";
-import BaseSourceForm from "./BaseSourceForm";
-import { Col, Row } from "antd";
-import sourceApi from "../../../api/sourceApi";
-import { useAtom } from "jotai";
-import { currentUserId } from "../../../atoms/currentUser";
-import { useRouter } from "next/router";
-import { currentNameSpace } from "../../../atoms/namespace";
-
-const CreateSourceView = () => {
- const { t } = useTranslation();
- const router = useRouter();
- const [isLoading, setIsLoading] = useState(false);
- const [nameSpace] = useAtom(currentNameSpace);
- const [userId] = useAtom(currentUserId);
-
- const handleSubmit = ({ source, summary, classification, recaptcha }) => {
- if (!isLoading) {
- setIsLoading(true);
- sourceApi
- .createSource(t, router, {
- href: source,
- user: userId,
- recaptcha,
- nameSpace: nameSpace,
- props: {
- summary,
- classification,
- date: new Date(),
- },
- })
- .then(() => {
- setIsLoading(false);
- });
- }
- };
-
- return (
-
-
-
-
-
- );
-};
-
-export default CreateSourceView;
diff --git a/src/components/Claim/SentencePopover.tsx b/src/components/Claim/SentencePopover.tsx
index d16472ba1..0c819390e 100644
--- a/src/components/Claim/SentencePopover.tsx
+++ b/src/components/Claim/SentencePopover.tsx
@@ -19,7 +19,7 @@ const SentencePopover = () => {
}}
>
- {t("claimReviewTask:sentenceInfo")}
+ {t("reviewTask:sentenceInfo")}
);
diff --git a/src/components/ClaimReview/ClaimReviewDrawer.tsx b/src/components/ClaimReview/ClaimReviewDrawer.tsx
index 30a1a6589..0c40fc364 100644
--- a/src/components/ClaimReview/ClaimReviewDrawer.tsx
+++ b/src/components/ClaimReview/ClaimReviewDrawer.tsx
@@ -14,11 +14,12 @@ import AletheiaButton, { ButtonType } from "../Button";
import ClaimReviewView from "./ClaimReviewView";
import Loading from "../Loading";
import LargeDrawer from "../LargeDrawer";
-import { CollaborativeEditorProvider } from "../Collaborative/CollaborativeEditorProvider";
+import { VisualEditorProvider } from "../Collaborative/VisualEditorProvider";
import { useAtom } from "jotai";
import { currentNameSpace } from "../../atoms/namespace";
import colors from "../../styles/colors";
-import { generateClaimContentPath } from "../../utils/GetClaimContentHref";
+import { generateReviewContentPath } from "../../utils/GetReviewContentHref";
+import { ReviewTaskTypeEnum } from "../../machines/reviewTask/enums";
const ClaimReviewDrawer = () => {
const [isLoading, setIsLoading] = useState(true);
@@ -29,7 +30,7 @@ const ClaimReviewDrawer = () => {
reviewDrawerCollapsed,
vw,
personality,
- claim,
+ target,
content,
data_hash,
enableCopilotChatBot,
@@ -40,22 +41,27 @@ const ClaimReviewDrawer = () => {
: true,
vw: state?.vw,
personality: state?.selectedPersonality,
- claim: state?.selectedClaim,
+ target: state?.selectedTarget,
content: state?.selectedContent,
data_hash: state?.selectedDataHash,
enableCopilotChatBot: state?.enableCopilotChatBot,
}));
- useEffect(() => setIsLoading(false), [claim, data_hash]);
+ useEffect(() => setIsLoading(false), [target, data_hash]);
return (
dispatch(actions.closeReviewDrawer())}
>
- {claim && data_hash && !isLoading ? (
-
-
+ {target && data_hash && !isLoading ? (
+
+
{
setIsLoading(true)}
type={ButtonType.gray}
@@ -93,7 +100,7 @@ const ClaimReviewDrawer = () => {
}}
data-cy="testSeeFullReview"
>
- {t("claimReviewTask:seeFullPage")}
+ {t("reviewTask:seeFullPage")}
@@ -126,10 +133,10 @@ const ClaimReviewDrawer = () => {
-
+
) : (
diff --git a/src/components/ClaimReview/ClaimReviewForm.tsx b/src/components/ClaimReview/ClaimReviewForm.tsx
index 08d639074..9b82d5152 100644
--- a/src/components/ClaimReview/ClaimReviewForm.tsx
+++ b/src/components/ClaimReview/ClaimReviewForm.tsx
@@ -1,4 +1,3 @@
-import Button, { ButtonType } from "../Button";
import { Col, Row } from "antd";
import React, { useContext, useEffect, useState } from "react";
import {
@@ -14,30 +13,25 @@ import {
} from "../../atoms/currentUser";
import DynamicReviewTaskForm from "./form/DynamicReviewTaskForm";
-import { PlusOutlined } from "@ant-design/icons";
import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
import { Roles } from "../../types/enums";
import colors from "../../styles/colors";
import { useAtom } from "jotai";
import { useSelector } from "@xstate/react";
-import { useTranslation } from "next-i18next";
-import CopilotDrawer from "../Copilot/CopilotDrawer";
-import { useAppSelector } from "../../store/store";
-import { ReportModelEnum } from "../../machines/reviewTask/enums";
+import ReportModelButtons from "./ReportModelButtons";
+import LoginButton from "../LoginButton";
const ClaimReviewForm = ({
- claim,
- personalityId,
dataHash,
userIsReviewer,
- sentenceContent,
componentStyle,
+ target,
+ personalityId = null,
}) => {
- const { t } = useTranslation();
- const [role] = useAtom(currentUserRole);
const [isLoggedIn] = useAtom(isUserLoggedIn);
+ const [role] = useAtom(currentUserRole);
const [userId] = useAtom(currentUserId);
- const { machineService, reportModel, recreateMachine } = useContext(
+ const { machineService, reportModel } = useContext(
ReviewTaskMachineContext
);
const reviewData = useSelector(machineService, reviewDataSelector);
@@ -50,15 +44,6 @@ const ClaimReviewForm = ({
isUnassigned && !reportModel
);
const userIsAdmin = role === Roles.Admin || role === Roles.SuperAdmin;
- const { enableCopilotChatBot, reviewDrawerCollapsed } = useAppSelector(
- (state) => ({
- enableCopilotChatBot: state?.enableCopilotChatBot,
- reviewDrawerCollapsed:
- state?.reviewDrawerCollapsed !== undefined
- ? state?.reviewDrawerCollapsed
- : true,
- })
- );
const [showForm, setShowForm] = useState(false);
useEffect(() => {
@@ -82,11 +67,6 @@ const ClaimReviewForm = ({
userIsCrossChecker,
]);
- const toggleFormCollapse = (event) => {
- setFormCollapsed(!formCollapsed);
- recreateMachine(event.currentTarget.id);
- };
-
useEffect(() => {
setFormCollapsed(isUnassigned && !reportModel);
}, [isUnassigned]);
@@ -100,74 +80,16 @@ const ClaimReviewForm = ({
>
{formCollapsed && (
-
-
- {isLoggedIn && (
- }
- data-cy={
- "testAddInformativeNewsReviewButton"
- }
- id={ReportModelEnum.InformativeNews}
- >
- {t(
- "claimReviewForm:addInformativeNewsButton"
- )}
-
- )}
- {isLoggedIn && (
- }
- data-cy={"testAddFactCheckReviewButton"}
- id={ReportModelEnum.FactChecking}
- >
- {t("claimReviewForm:addReviewButton")}
-
- )}
-
-
+
)}
-
- {!isLoggedIn && (
-
- )}
-
+ {!isLoggedIn && }
{!formCollapsed && showForm && (
)}
- {showForm && enableCopilotChatBot && reviewDrawerCollapsed && (
-
- )}
);
diff --git a/src/components/ClaimReview/ClaimReviewHeader.tsx b/src/components/ClaimReview/ClaimReviewHeader.tsx
index a0e9ee61e..1811f52af 100644
--- a/src/components/ClaimReview/ClaimReviewHeader.tsx
+++ b/src/components/ClaimReview/ClaimReviewHeader.tsx
@@ -1,165 +1,78 @@
import { useSelector } from "@xstate/react";
import { Col, Row } from "antd";
-import { useTranslation } from "next-i18next";
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext } from "react";
import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
-import { ClassificationEnum, Roles, TargetModel } from "../../types/enums";
-import {
- reviewingSelector,
- publishedSelector,
- reviewNotStartedSelector,
- crossCheckingSelector,
-} from "../../machines/reviewTask/selectors";
+import { ClassificationEnum, TargetModel } from "../../types/enums";
+import { publishedSelector } from "../../machines/reviewTask/selectors";
import { useAppSelector } from "../../store/store";
import colors from "../../styles/colors";
-import AletheiaAlert from "../AletheiaAlert";
import SentenceReportCard from "../SentenceReport/SentenceReportCard";
import TopicInput from "./TopicInput";
import { Content } from "../../types/Content";
-import { useAtom } from "jotai";
-import { currentUserRole, isUserLoggedIn } from "../../atoms/currentUser";
+import ReviewAlert from "./ReviewAlert";
+import { ReviewTaskTypeEnum } from "../../machines/reviewTask/enums";
interface ClaimReviewHeaderProps {
personality?: string;
- claim: any;
+ target?: any;
content: Content;
classification?: ClassificationEnum;
hideDescription: string;
- userIsReviewer: boolean;
- userIsAssignee: boolean;
userIsNotRegular: boolean;
componentStyle: any;
}
const ClaimReviewHeader = ({
personality,
- claim,
content,
classification,
hideDescription,
- userIsReviewer,
- userIsAssignee,
userIsNotRegular,
componentStyle,
+ target,
}: ClaimReviewHeaderProps) => {
- const { t } = useTranslation();
const { reviewDrawerCollapsed } = useAppSelector((state) => ({
reviewDrawerCollapsed:
state?.reviewDrawerCollapsed !== undefined
? state?.reviewDrawerCollapsed
: true,
}));
- const [isLoggedIn] = useAtom(isUserLoggedIn);
- const [role] = useAtom(currentUserRole);
- const { machineService, publishedReview } = useContext(
+ const { machineService, publishedReview, reviewTaskType } = useContext(
ReviewTaskMachineContext
);
const isHidden = publishedReview?.review?.isHidden;
- const [hide, setHide] = useState(isHidden);
-
- const reviewNotStarted = useSelector(
- machineService,
- reviewNotStartedSelector
- );
- const isCrossChecking = useSelector(machineService, crossCheckingSelector);
- const isReviewing = useSelector(machineService, reviewingSelector);
- const userHasPermission = userIsReviewer || userIsAssignee;
const isPublished =
useSelector(machineService, publishedSelector) ||
publishedReview?.review;
const isPublishedOrCanSeeHidden =
isPublished && (!isHidden || userIsNotRegular);
- const userIsAdmin = role === Roles.Admin || role === Roles.SuperAdmin;
-
- const alertTypes = {
- hiddenReport: {
- show: true,
- description: hideDescription?.[TargetModel.ClaimReview],
- title: "claimReview:warningAlertTitle",
- },
- crossChecking: {
- show: true,
- description: "",
- title: "claimReviewTask:crossCheckingAlertTitle",
- },
- reviewing: {
- show: true,
- description: "",
- title: "claimReviewTask:reviewingAlertTitle",
- },
- hasStarted: {
- show: true,
- description: "",
- title: "claimReviewTask:hasStartedAlertTitle",
- },
- noAlert: {
- show: false,
- description: "",
- title: "",
- },
- };
-
- const [alert, setAlert] = useState(alertTypes.noAlert);
- const getAlert = () => {
- if (!isLoggedIn) {
- return alertTypes.noAlert;
- }
- if (hide && !userIsAdmin) {
- return alertTypes.hiddenReport;
- }
- if (!isPublished) {
- if (isCrossChecking && (!userIsAdmin || !userHasPermission)) {
- return alertTypes.crossChecking;
- }
- if (isReviewing && (!userIsAdmin || !userHasPermission)) {
- return alertTypes.reviewing;
- }
- if (!userHasPermission && !userIsAdmin && !reviewNotStarted) {
- return alertTypes.hasStarted;
- }
- }
- return alertTypes.noAlert;
- };
-
- useEffect(() => {
- const newAlert = getAlert();
- setAlert(newAlert);
- }, [
- isCrossChecking,
- isReviewing,
- hide,
- isLoggedIn,
- userHasPermission,
- userIsAdmin,
- ]);
-
- useEffect(() => {
- setHide(isHidden);
- }, [isHidden]);
return (
-
+
-
+ {reviewTaskType === ReviewTaskTypeEnum.Claim && (
-
+ )}
- {alert.show && (
-
-
-
- )}
+
+
diff --git a/src/components/ClaimReview/ClaimReviewView.tsx b/src/components/ClaimReview/ClaimReviewView.tsx
index d2e3b35cc..278c889fc 100644
--- a/src/components/ClaimReview/ClaimReviewView.tsx
+++ b/src/components/ClaimReview/ClaimReviewView.tsx
@@ -2,7 +2,7 @@ import { useSelector } from "@xstate/react";
import React, { useContext } from "react";
import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
-import { ContentModelEnum, Roles, TargetModel } from "../../types/enums";
+import { Roles, TargetModel } from "../../types/enums";
import { reviewDataSelector } from "../../machines/reviewTask/selectors";
import SentenceReportView from "../SentenceReport/SentenceReportView";
import SocialMediaShare from "../SocialMediaShare";
@@ -13,22 +13,22 @@ import { currentUserId, currentUserRole } from "../../atoms/currentUser";
import { useAtom } from "jotai";
import AdminToolBar from "../Toolbar/AdminToolBar";
import ClaimReviewApi from "../../api/claimReviewApi";
-import { NameSpaceEnum } from "../../types/Namespace";
import { currentNameSpace } from "../../atoms/namespace";
import ReviewTaskAdminToolBar from "../Toolbar/ReviewTaskAdminToolBar";
import { useAppSelector } from "../../store/store";
import { ReviewTaskStates } from "../../machines/reviewTask/enums";
+import { generateReviewContentPath } from "../../utils/GetReviewContentHref";
export interface ClaimReviewViewProps {
- personality?: any;
- claim: any;
content: Content;
hideDescriptions?: any;
+ personality?: any;
+ target?: any;
}
const ClaimReviewView = (props: ClaimReviewViewProps) => {
- const { personality, claim, content, hideDescriptions } = props;
- const { machineService, publishedReview } = useContext(
+ const { personality, target, content, hideDescriptions } = props;
+ const { machineService, publishedReview, reviewTaskType } = useContext(
ReviewTaskMachineContext
);
const { reviewDrawerCollapsed } = useAppSelector((state) => ({
@@ -38,16 +38,14 @@ const ClaimReviewView = (props: ClaimReviewViewProps) => {
: true,
}));
const { review } = publishedReview || {};
- const [nameSpace] = useAtom(currentNameSpace);
const reviewData = useSelector(machineService, reviewDataSelector);
const [role] = useAtom(currentUserRole);
const [userId] = useAtom(currentUserId);
-
+ const [nameSpace] = useAtom(currentNameSpace);
const userIsNotRegular = !(role === Roles.Regular || role === null);
const userIsReviewer = reviewData.reviewerId === userId;
const userIsCrossChecker = reviewData.crossCheckerId === userId;
const userIsAssignee = reviewData.usersId.includes(userId);
- const isContentImage = claim.contentModel === ContentModelEnum.Image;
const hasStartedTask =
machineService.state.value !== ReviewTaskStates.unassigned;
const origin = window.location.origin ? window.location.origin : "";
@@ -62,20 +60,16 @@ const ClaimReviewView = (props: ClaimReviewViewProps) => {
componentStyle.offset = 1;
}
- let contentPath =
- nameSpace !== NameSpaceEnum.Main ? `${nameSpace}/claim` : `/claim`;
-
- if (personality) {
- contentPath =
- nameSpace !== NameSpaceEnum.Main
- ? `${nameSpace}/personality/${personality?.slug}/claim`
- : `/personality/${personality?.slug}/claim`;
- }
+ const reviewContentPath = generateReviewContentPath(
+ nameSpace,
+ personality,
+ target,
+ target?.contentModel,
+ content.data_hash,
+ reviewTaskType
+ );
- contentPath += isContentImage
- ? `/${claim?._id}`
- : `/${claim?.slug}/sentence/${content.data_hash}`;
- const href = `${origin}${contentPath}`;
+ const href = `${origin}${reviewContentPath}`;
return (
@@ -88,7 +82,7 @@ const ClaimReviewView = (props: ClaimReviewViewProps) => {
changeHideStatusFunction={
ClaimReviewApi.updateClaimReviewHiddenStatus
}
- target={TargetModel.ClaimReview}
+ target={TargetModel.ClaimReview} // TODO: rename to review
hideDescriptions={hideDescriptions}
/>
) : (
@@ -101,9 +95,7 @@ const ClaimReviewView = (props: ClaimReviewViewProps) => {
review?.report?.classification || reviewData?.classification
}
hideDescription={hideDescriptions}
- userIsReviewer={userIsReviewer}
userIsNotRegular={userIsNotRegular}
- userIsAssignee={userIsAssignee}
componentStyle={componentStyle}
{...props}
/>
@@ -120,19 +112,18 @@ const ClaimReviewView = (props: ClaimReviewViewProps) => {
{!review?.isPublished && (
)}
{review?.isPublished && (
)}
diff --git a/src/components/ClaimReview/ReportModelButtons.tsx b/src/components/ClaimReview/ReportModelButtons.tsx
new file mode 100644
index 000000000..106b9a9ba
--- /dev/null
+++ b/src/components/ClaimReview/ReportModelButtons.tsx
@@ -0,0 +1,110 @@
+import { Col, Row } from "antd";
+import React, { useContext } from "react";
+import Button, { ButtonType } from "../Button";
+import {
+ ReportModelEnum,
+ ReviewTaskTypeEnum,
+} from "../../machines/reviewTask/enums";
+import { isUserLoggedIn } from "../../atoms/currentUser";
+import { useAtom } from "jotai";
+import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
+import { PlusOutlined } from "@ant-design/icons";
+import { useTranslation } from "next-i18next";
+
+const ReportModelButtons = ({ setFormCollapsed }) => {
+ const { t } = useTranslation();
+ const [isLoggedIn] = useAtom(isUserLoggedIn);
+ const { recreateMachine, reviewTaskType } = useContext(
+ ReviewTaskMachineContext
+ );
+ const isClaim = reviewTaskType === ReviewTaskTypeEnum.Claim;
+ const isSource = reviewTaskType === ReviewTaskTypeEnum.Source;
+ const isVerificationRequest =
+ reviewTaskType === ReviewTaskTypeEnum.VerificationRequest;
+
+ const toggleFormCollapse = (event) => {
+ setFormCollapsed((prev) => !prev);
+ recreateMachine(event.currentTarget.id);
+ };
+
+ return (
+
+
+ {isLoggedIn && (
+ <>
+ {isClaim && (
+ <>
+ }
+ data-cy={
+ "testAddInformativeNewsReviewButton"
+ }
+ id={ReportModelEnum.InformativeNews}
+ >
+ {t(
+ "claimReviewForm:addInformativeNewsButton"
+ )}
+
+ }
+ data-cy={"testAddFactCheckReviewButton"}
+ id={ReportModelEnum.FactChecking}
+ >
+ {t(
+ "claimReviewForm:addFactCheckingReviewButton"
+ )}
+
+ >
+ )}
+ {isSource && (
+ }
+ data-cy={"testAddFactCheckReviewButton"}
+ id={ReportModelEnum.FactChecking}
+ >
+ {t("claimReviewForm:addSourceReviewButton")}
+
+ )}
+ {isVerificationRequest && (
+ }
+ data-cy={
+ "testAddVerificationRequestReviewButton"
+ }
+ id={ReportModelEnum.Request}
+ >
+ {t(
+ "claimReviewForm:addVerificationRequestButton"
+ )}
+
+ )}
+ >
+ )}
+
+
+ );
+};
+
+export default ReportModelButtons;
diff --git a/src/components/ClaimReview/ReviewAlert.tsx b/src/components/ClaimReview/ReviewAlert.tsx
new file mode 100644
index 000000000..7720f4554
--- /dev/null
+++ b/src/components/ClaimReview/ReviewAlert.tsx
@@ -0,0 +1,129 @@
+import { Col } from "antd";
+import React, { useContext, useEffect, useState } from "react";
+import AletheiaAlert from "../AletheiaAlert";
+import { useTranslation } from "next-i18next";
+import { useSelector } from "@xstate/react";
+import {
+ reviewingSelector,
+ reviewNotStartedSelector,
+ crossCheckingSelector,
+ reviewDataSelector,
+} from "../../machines/reviewTask/selectors";
+import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
+import { useAtom } from "jotai";
+import {
+ currentUserId,
+ currentUserRole,
+ isUserLoggedIn,
+} from "../../atoms/currentUser";
+import { Roles, TargetModel } from "../../types/enums";
+
+const ReviewAlert = ({ isHidden, isPublished, hideDescription }) => {
+ const { t } = useTranslation();
+ const [role] = useAtom(currentUserRole);
+ const [isLoggedIn] = useAtom(isUserLoggedIn);
+ const [userId] = useAtom(currentUserId);
+
+ const { machineService } = useContext(ReviewTaskMachineContext);
+ const reviewNotStarted = useSelector(
+ machineService,
+ reviewNotStartedSelector
+ );
+ const reviewData = useSelector(machineService, reviewDataSelector);
+ const userIsAdmin = role === Roles.Admin || role === Roles.SuperAdmin;
+ const isCrossChecking = useSelector(machineService, crossCheckingSelector);
+ const isReviewing = useSelector(machineService, reviewingSelector);
+ const userIsReviewer = reviewData.reviewerId === userId;
+ const userIsAssignee = reviewData.usersId.includes(userId);
+ const userHasPermission = userIsReviewer || userIsAssignee;
+ const [hide, setHide] = useState(isHidden);
+
+ useEffect(() => {
+ setHide(isHidden);
+ }, [isHidden]);
+
+ const alertTypes = {
+ hiddenReport: {
+ show: true,
+ description: hideDescription?.[TargetModel.ClaimReview],
+ title: "claimReview:warningAlertTitle",
+ },
+ crossChecking: {
+ show: true,
+ description: "",
+ title: "reviewTask:crossCheckingAlertTitle",
+ },
+ reviewing: {
+ show: true,
+ description: "",
+ title: "reviewTask:reviewingAlertTitle",
+ },
+ hasStarted: {
+ show: true,
+ description: "",
+ title: "reviewTask:hasStartedAlertTitle",
+ },
+ noAlert: {
+ show: false,
+ description: "",
+ title: "",
+ },
+ };
+
+ const [alert, setAlert] = useState(alertTypes.noAlert);
+ const getAlert = () => {
+ if (!isLoggedIn) {
+ return alertTypes.noAlert;
+ }
+ if (hide && !userIsAdmin) {
+ return alertTypes.hiddenReport;
+ }
+ if (!isPublished) {
+ if (isCrossChecking && (!userIsAdmin || !userHasPermission)) {
+ return alertTypes.crossChecking;
+ }
+ if (isReviewing && (!userIsAdmin || !userHasPermission)) {
+ return alertTypes.reviewing;
+ }
+ if (!userHasPermission && !userIsAdmin && !reviewNotStarted) {
+ return alertTypes.hasStarted;
+ }
+ }
+ return alertTypes.noAlert;
+ };
+
+ useEffect(() => {
+ const newAlert = getAlert();
+ setAlert(newAlert);
+ }, [
+ isCrossChecking,
+ isReviewing,
+ hide,
+ isLoggedIn,
+ userHasPermission,
+ userIsAdmin,
+ ]);
+
+ return (
+ <>
+ {alert.show && (
+
+
+
+ )}
+ >
+ );
+};
+
+export default ReviewAlert;
diff --git a/src/components/ClaimReview/ReviewCard.style.tsx b/src/components/ClaimReview/ReviewCard.style.tsx
new file mode 100644
index 000000000..a59fc5d41
--- /dev/null
+++ b/src/components/ClaimReview/ReviewCard.style.tsx
@@ -0,0 +1,53 @@
+import styled from "styled-components";
+import { queries } from "../../styles/mediaQueries";
+
+const ReviewCardStyled = styled.div`
+ display: flex;
+ gap: 16px;
+ padding: 32px;
+ width: 100%;
+ flex-wrap: wrap;
+
+ .personality-card {
+ width: 120px;
+ }
+
+ .review-content {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ gap: 16px;
+ justify-content: space-between;
+ }
+
+ .review-info {
+ height: auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ }
+
+ .sentence-content {
+ display: flex;
+ gap: 16px;
+ }
+
+ .review-actions {
+ margin-top: 16px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 8px;
+ }
+
+ @media ${queries.sm} {
+ padding: 16px;
+
+ .personality-card {
+ width: 100%;
+ }
+ }
+`;
+
+export default ReviewCardStyled;
diff --git a/src/components/ClaimReview/ReviewCard.tsx b/src/components/ClaimReview/ReviewCard.tsx
new file mode 100644
index 000000000..91fe397ae
--- /dev/null
+++ b/src/components/ClaimReview/ReviewCard.tsx
@@ -0,0 +1,140 @@
+import React from "react";
+import PersonalityMinimalCard from "../Personality/PersonalityMinimalCard";
+import CardBase from "../CardBase";
+import { Col } from "antd";
+import { useTranslation } from "next-i18next";
+import reviewColors from "../../constants/reviewColors";
+import TagsList from "./TagsList";
+import AletheiaButton, { ButtonType } from "../Button";
+import { ContentModelEnum } from "../../types/enums";
+import { generateSentenceContentPath } from "../../utils/GetSentenceContentHref";
+import { useAtom } from "jotai";
+import { currentNameSpace } from "../../atoms/namespace";
+import ReviewCardStyled from "./ReviewCard.style";
+import ClaimInfo from "../Claim/ClaimInfo";
+import ReviewClassification from "./ReviewClassification";
+import ReviewContent from "./ReviewContent";
+
+const ReviewCard = ({ review, summarized = false }) => {
+ const { personality, claim, content, reviewHref } = review;
+ const { t } = useTranslation();
+ const [nameSpace] = useAtom(currentNameSpace);
+
+ const claimItem =
+ Array.isArray(claim) && claim.length > 0 ? claim[0] : claim;
+ const personalityItem =
+ Array.isArray(personality) && personality.length > 0
+ ? personality[0]
+ : personality;
+ const isImage = claimItem?.contentModel === ContentModelEnum.Image;
+ const contentPath = generateSentenceContentPath(
+ nameSpace,
+ personalityItem,
+ claimItem,
+ claimItem.contentModel
+ );
+
+ const contentProps = {
+ [ContentModelEnum.Speech]: {
+ linkText: "claim:cardLinkToFullText",
+ title: `"(...) ${content.content}"`,
+ },
+ [ContentModelEnum.Image]: {
+ linkText: "claim:cardLinkToImage",
+ title: claimItem?.title,
+ },
+ [ContentModelEnum.Debate]: {
+ linkText: "claim:cardLinkToDebate",
+ title: `"(...) ${content.content}"`,
+ },
+ [ContentModelEnum.Unattributed]: {
+ linkText: "claim:cardLinkToFullText",
+ title: `"(...) ${content.content}"`,
+ },
+ };
+
+ const { linkText, title } = contentProps[claimItem.contentModel];
+ const href = reviewHref
+ ? reviewHref
+ : generateSentenceContentPath(
+ nameSpace,
+ personalityItem,
+ claimItem,
+ claimItem?.contentModel,
+ content?.data_hash
+ );
+
+ return (
+
+
+ {!summarized && personalityItem && (
+
+
+
+ )}
+
+
+
+ {content?.props?.classification && (
+
+ )}
+
+
+ {content?.props?.classification && (
+
+ )}
+
+
+
+
+
+
+ {t("home:reviewsCarouselOpen")}
+
+
+
+
+
+ );
+};
+
+export default ReviewCard;
diff --git a/src/components/ClaimReview/ReviewCardActions.tsx b/src/components/ClaimReview/ReviewCardActions.tsx
deleted file mode 100644
index 7c180d330..000000000
--- a/src/components/ClaimReview/ReviewCardActions.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React, { useState } from "react";
-import AletheiaButton, { ButtonType } from "../Button";
-import colors from "../../styles/colors";
-import ClassificationText from "../ClassificationText";
-import { useTranslation } from "next-i18next";
-
-const ClaimReviewCardActions = ({ href, content }) => {
- const { t } = useTranslation();
- const [isButtonLoading, setIsButtonLoading] = useState(false);
- return (
-
-
- {content.props.classification && (
-
- {t("claimReview:claimReview")}
-
-
- )}
-
-
-
setIsButtonLoading(true)}
- loading={isButtonLoading}
- style={{ right: 0 }}
- >
-
- {t("home:reviewsCarouselOpen")}
-
-
-
-
- );
-};
-
-export default ClaimReviewCardActions;
diff --git a/src/components/ClaimReview/ReviewCardComment.tsx b/src/components/ClaimReview/ReviewCardComment.tsx
deleted file mode 100644
index 651de4310..000000000
--- a/src/components/ClaimReview/ReviewCardComment.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from "react";
-import { Comment } from "antd";
-import ClaimCardHeader from "../Claim/ClaimCardHeader";
-import styled from "styled-components";
-import TagsList from "./TagsList";
-import { ContentModelEnum } from "../../types/enums";
-import ReviewCardCommentContent from "./ReviewCardCommentContent";
-import ReviewCardActions from "./ReviewCardActions";
-
-const StyledComment = styled(Comment)`
- .ant-comment-actions > li {
- width: 100%;
- }
-`;
-
-const ClaimReviewCard = ({ review, ...props }) => {
- const { reviewHref, content, claim, personality } = review;
- const isImage = review?.claim.contentModel === ContentModelEnum.Image;
-
- return (
-
-
- }
- content={
-
- }
- actions={[
- ,
- ,
- ]}
- />
-
- );
-};
-
-export default ClaimReviewCard;
diff --git a/src/components/ClaimReview/ReviewCardCommentContent.tsx b/src/components/ClaimReview/ReviewCardCommentContent.tsx
deleted file mode 100644
index 46bafad35..000000000
--- a/src/components/ClaimReview/ReviewCardCommentContent.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from "react";
-
-import { Typography } from "antd";
-import colors from "../../styles/colors";
-import ClaimSummary from "../Claim/ClaimSummary";
-import ImageClaim from "../ImageClaim";
-const { Paragraph } = Typography;
-
-const ClaimReviewCardContent = ({ isImage, content }) => {
- return (
-
-
-
- {isImage ? : content}
-
-
-
- );
-};
-
-export default ClaimReviewCardContent;
diff --git a/src/components/ClaimReview/ReviewClassification.tsx b/src/components/ClaimReview/ReviewClassification.tsx
new file mode 100644
index 000000000..a76a5e240
--- /dev/null
+++ b/src/components/ClaimReview/ReviewClassification.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+import ClassificationText from "../ClassificationText";
+
+const ReviewClassification = ({
+ label,
+ classification,
+ classificationTextStyle = {},
+ style = {},
+}) => {
+ return (
+
+ {label}
+
+
+ );
+};
+
+export default ReviewClassification;
diff --git a/src/components/ClaimReview/ReviewContent.style.tsx b/src/components/ClaimReview/ReviewContent.style.tsx
new file mode 100644
index 000000000..3c653563f
--- /dev/null
+++ b/src/components/ClaimReview/ReviewContent.style.tsx
@@ -0,0 +1,29 @@
+import styled from "styled-components";
+import colors from "../../styles/colors";
+
+const ReviewContentStyled = styled.div`
+ display: flex;
+ flex-direction: column;
+ font-size: 16px;
+ flex-wrap: wrap;
+
+ p {
+ color: ${colors.blackSecondary};
+ }
+
+ cite {
+ font-style: normal;
+ }
+
+ a {
+ color: ${colors.bluePrimary};
+ font-weight: 700;
+ margin-left: 10px;
+ }
+
+ a:hover {
+ color: ${colors.bluePrimary};
+ }
+`;
+
+export default ReviewContentStyled;
diff --git a/src/components/ClaimReview/ReviewContent.tsx b/src/components/ClaimReview/ReviewContent.tsx
new file mode 100644
index 000000000..5a22e3020
--- /dev/null
+++ b/src/components/ClaimReview/ReviewContent.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+import ImageClaim from "../ImageClaim";
+import ReviewContentStyled from "./ReviewContent.style";
+import { Typography } from "antd";
+
+const ReviewContent = ({
+ title,
+ content,
+ isImage,
+ contentPath = null,
+ linkText = null,
+ style = {},
+ ellipsis = false,
+}) => {
+ return (
+
+
+ {title}
+ {isImage && }
+ {!ellipsis && contentPath && linkText && (
+
+ {linkText}
+
+ )}
+
+ {ellipsis && contentPath && linkText && (
+
+ {linkText}
+
+ )}
+
+ );
+};
+
+export default ReviewContent;
diff --git a/src/components/ClaimReview/ReviewsCarousel.tsx b/src/components/ClaimReview/ReviewsCarousel.tsx
deleted file mode 100644
index fb91e4ed0..000000000
--- a/src/components/ClaimReview/ReviewsCarousel.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { LeftOutlined, RightOutlined } from "@ant-design/icons";
-import { Button } from "antd";
-import CardBase from "../CardBase";
-import ClaimReviewApi from "../../api/claimReviewApi";
-import ReviewCarouselSkeleton from "../Skeleton/ReviewCarouselSkeleton";
-import ReviewCardComment from "./ReviewCardComment";
-import { useAtom } from "jotai";
-import { currentNameSpace } from "../../atoms/namespace";
-
-const ReviewsCarousel = () => {
- const [currIndex, setCurrIndex] = useState(0);
- const [loading, setLoading] = useState(true);
- const [reviewsList, setReviewsList] = useState([]);
- const [nameSpace] = useAtom(currentNameSpace);
-
- useEffect(() => {
- ClaimReviewApi.get({
- page: 0,
- order: "desc",
- pageSize: 5,
- nameSpace,
- }).then(({ data }) => {
- setReviewsList(data);
- setLoading(false);
- });
- }, [nameSpace]);
-
- const currentReview = reviewsList[currIndex];
-
- const nextCard = () => {
- if (currIndex < reviewsList.length - 1) setCurrIndex(currIndex + 1);
- else setCurrIndex(0);
- };
-
- const previousCard = () => {
- if (currIndex > 0) setCurrIndex(currIndex - 1);
- else setCurrIndex(reviewsList.length - 1);
- };
-
- if (loading) {
- return ;
- }
-
- return (
-
- {reviewsList.length > 0 ? (
-
-
-
-
-
-
-
- ) : (
- <>>
- )}
-
- );
-};
-
-export default ReviewsCarousel;
diff --git a/src/components/ClaimReview/ReviewsGrid.tsx b/src/components/ClaimReview/ReviewsGrid.tsx
new file mode 100644
index 000000000..84b8cdd59
--- /dev/null
+++ b/src/components/ClaimReview/ReviewsGrid.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import GridList from "../GridList";
+import ReviewCard from "./ReviewCard";
+
+const ReviewsGrid = ({ reviews, title }) => {
+ return (
+ }
+ />
+ );
+};
+
+export default ReviewsGrid;
diff --git a/src/components/ClaimReview/TopicInput.tsx b/src/components/ClaimReview/TopicInput.tsx
index be3a2e8ce..5e3c50dcc 100644
--- a/src/components/ClaimReview/TopicInput.tsx
+++ b/src/components/ClaimReview/TopicInput.tsx
@@ -12,9 +12,11 @@ import { useAtom } from "jotai";
import { isUserLoggedIn } from "../../atoms/currentUser";
import { ContentModelEnum } from "../../types/enums";
import ImageApi from "../../api/image";
+import { useDispatch } from "react-redux";
const TopicInput = ({ contentModel, data_hash, topics }) => {
const { t } = useTranslation();
+ const dispatch = useDispatch();
const [isLoggedIn] = useAtom(isUserLoggedIn);
const [showTopicsInput, setShowTopicsInput] = useState(false);
const [topicsArray, setTopicsArray] = useState(topics);
@@ -30,6 +32,7 @@ const TopicInput = ({ contentModel, data_hash, topics }) => {
const topicSearchResults = await topicApi.getTopics({
topicName: topic,
t: t,
+ dispatch: dispatch,
});
return topicSearchResults.map((topic) => ({
value: topic.name,
@@ -93,9 +96,11 @@ const TopicInput = ({ contentModel, data_hash, topics }) => {
<>
{
+const DynamicReviewTaskForm = ({ data_hash, personality, target }) => {
const {
handleSubmit,
control,
@@ -41,27 +38,14 @@ const DynamicReviewTaskForm = ({ data_hash, personality, claim }) => {
formState: { errors },
watch,
} = useForm();
- const dispatch = useDispatch();
const { reportModel } = useContext(ReviewTaskMachineContext);
- const { machineService, events, form, setFormAndEvents } = useContext(
- ReviewTaskMachineContext
- );
+ const { machineService, events, form, setFormAndEvents, reviewTaskType } =
+ useContext(ReviewTaskMachineContext);
const isReviewing = useSelector(machineService, reviewingSelector);
const isCrossChecking = useSelector(machineService, crossCheckingSelector);
const isReported = useSelector(machineService, reportSelector);
- const { editorContentObject, comments } = useContext(
- CollaborativeEditorContext
- );
+ const { comments } = useContext(VisualEditorContext);
const reviewData = useSelector(machineService, reviewDataSelector);
- const { enableCopilotChatBot, reviewDrawerCollapsed } = useAppSelector(
- (state) => ({
- enableCopilotChatBot: state?.enableCopilotChatBot,
- reviewDrawerCollapsed:
- state?.reviewDrawerCollapsed !== undefined
- ? state?.reviewDrawerCollapsed
- : true,
- })
- );
const { t } = useTranslation();
const [nameSpace] = useAtom(currentNameSpace);
const [role] = useAtom(currentUserRole);
@@ -88,9 +72,6 @@ const DynamicReviewTaskForm = ({ data_hash, personality, claim }) => {
useEffect(() => {
if (isLoggedIn) {
setFormAndEvents(machineService.machine.config.initial);
- if (enableCopilotChatBot && reviewDrawerCollapsed) {
- dispatch(actions.openCopilotDrawer());
- }
}
}, [isLoggedIn]);
@@ -100,7 +81,7 @@ const DynamicReviewTaskForm = ({ data_hash, personality, claim }) => {
setReviewerError(false);
}, [events]);
- useAutoSaveDraft(data_hash, personality, claim, watch);
+ useAutoSaveDraft(data_hash, personality, target, watch);
const sendEventToMachine = (formData, eventName) => {
setIsLoading((current) => ({ ...current, [eventName]: true }));
@@ -115,10 +96,11 @@ const DynamicReviewTaskForm = ({ data_hash, personality, claim }) => {
) || reviewData.reviewComments,
crossCheckingComments: reviewData.crossCheckingComments,
},
- claimReview: {
+ review: {
personality,
- claim,
},
+ reviewTaskType,
+ target,
type: eventName,
t,
recaptchaString,
@@ -179,13 +161,7 @@ const DynamicReviewTaskForm = ({ data_hash, personality, claim }) => {
if (shouldShowFinishReportWarning) {
setFinishReportWarningModal(!finishReportWarningModal);
} else {
- sendEventToMachine(
- {
- ...context,
- collaborativeEditor: editorContentObject,
- },
- event
- );
+ sendEventToMachine(context, event);
}
scrollToTop(event);
@@ -255,7 +231,7 @@ const DynamicReviewTaskForm = ({ data_hash, personality, claim }) => {
{reviewerError && (
- {t("claimReviewTask:invalidReviewerMessage")}
+ {t("reviewTask:invalidReviewerMessage")}
)}
@@ -286,7 +262,7 @@ const DynamicReviewTaskForm = ({ data_hash, personality, claim }) => {
disabled={!hasCaptcha}
data-cy={`testClaimReview${event}`}
>
- {t(`claimReviewTask:${event}`)}
+ {t(`reviewTask:${event}`)}
);
})}
diff --git a/src/components/ClaimReview/form/fieldLists/submittedForm.ts b/src/components/ClaimReview/form/fieldLists/submittedForm.ts
index be27db3f3..0bc90ff63 100644
--- a/src/components/ClaimReview/form/fieldLists/submittedForm.ts
+++ b/src/components/ClaimReview/form/fieldLists/submittedForm.ts
@@ -2,8 +2,8 @@ import { createFormField, FormField } from "../../../Form/FormField";
const submittedForm: FormField[] = [
createFormField({
- fieldName: "collaborativeEditor",
- type: "collaborative",
+ fieldName: "visualEditor",
+ type: "visualEditor",
defaultValue: "",
}),
];
diff --git a/src/components/ClaimReview/form/fieldLists/verificationRequestForm.ts b/src/components/ClaimReview/form/fieldLists/verificationRequestForm.ts
new file mode 100644
index 000000000..7c2cb2a4c
--- /dev/null
+++ b/src/components/ClaimReview/form/fieldLists/verificationRequestForm.ts
@@ -0,0 +1,31 @@
+import verificationRequestApi from "../../../../api/verificationRequestApi";
+import { createFormField, FormField } from "../../../Form/FormField";
+
+export const fetchVerificationRequestList = async (content) => {
+ const verificationRequestSearchResults =
+ await verificationRequestApi.getVerificationRequests({
+ searchContent: content,
+ });
+ return verificationRequestSearchResults.map((verificationRequest) => ({
+ label: verificationRequest.content,
+ value: verificationRequest._id,
+ }));
+};
+
+const verificationRequestForm: FormField[] = [
+ createFormField({
+ fieldName: "group",
+ type: "inputSearch",
+ i18nKey: "group",
+ required: false,
+ extraProps: { dataLoader: fetchVerificationRequestList },
+ }),
+
+ createFormField({
+ fieldName: "isSensitive",
+ type: "textbox",
+ required: false,
+ }),
+];
+
+export default verificationRequestForm;
diff --git a/src/components/ClaimReview/form/fieldLists/assignedCollaborativeForm.ts b/src/components/ClaimReview/form/fieldLists/visualEditor.ts
similarity index 80%
rename from src/components/ClaimReview/form/fieldLists/assignedCollaborativeForm.ts
rename to src/components/ClaimReview/form/fieldLists/visualEditor.ts
index c563d8374..95dd897e4 100644
--- a/src/components/ClaimReview/form/fieldLists/assignedCollaborativeForm.ts
+++ b/src/components/ClaimReview/form/fieldLists/visualEditor.ts
@@ -5,10 +5,10 @@ import {
fieldValidation,
} from "../../../Form/FormField";
-const assignedCollaborativeForm: FormField[] = [
+const visualEditor: FormField[] = [
createFormField({
- fieldName: "collaborativeEditor",
- type: "collaborative",
+ fieldName: "visualEditor",
+ type: "visualEditor",
defaultValue: "",
}),
@@ -30,4 +30,4 @@ function isValidClassification(string) {
return Object.values(ClassificationEnum).includes(string);
}
-export default assignedCollaborativeForm;
+export default visualEditor;
diff --git a/src/components/ClaimReview/form/hooks/useAutoSaveDraft.tsx b/src/components/ClaimReview/form/hooks/useAutoSaveDraft.tsx
index 9583ad545..ad6b6df6b 100644
--- a/src/components/ClaimReview/form/hooks/useAutoSaveDraft.tsx
+++ b/src/components/ClaimReview/form/hooks/useAutoSaveDraft.tsx
@@ -1,9 +1,9 @@
import { useEffect } from "react";
import { useAppSelector } from "../../../../store/store";
-import reviewTaskApi from "../../../../api/ClaimReviewTaskApi";
+import reviewTaskApi from "../../../../api/reviewTaskApi";
import { useTranslation } from "next-i18next";
-const useAutoSaveDraft = (data_hash, personality, claim, watch) => {
+const useAutoSaveDraft = (data_hash, personality, target, watch) => {
const autoSave = useAppSelector((state) => state.autoSave);
const { t } = useTranslation();
@@ -21,9 +21,9 @@ const useAutoSaveDraft = (data_hash, personality, claim, watch) => {
machine: {
context: {
reviewData: value,
- claimReview: {
+ review: {
personality,
- claim,
+ target,
isPartialReview: true,
},
},
diff --git a/src/components/Collaborative/CollaborativeEditor.tsx b/src/components/Collaborative/CollaborativeEditor.tsx
deleted file mode 100644
index 27fb8e46e..000000000
--- a/src/components/Collaborative/CollaborativeEditor.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import {
- AnnotationExtension,
- LinkExtension,
- PlaceholderExtension,
- YjsExtension,
-} from "remirror/extensions";
-import React, { useCallback, useContext, useEffect, useMemo } from "react";
-import { Remirror, useRemirror } from "@remirror/react";
-
-import { CollaborativeEditorContext } from "./CollaborativeEditorProvider";
-import CollaborativeEditorStyle from "./CollaborativeEditor.style";
-import Editor from "./Editor";
-import FloatingLinkToolbar from "./Components/LinkToolBar/FloatingLinkToolbar";
-import SummaryExtension from "./Form/SummaryExtesion";
-import QuestionExtension from "./Form/QuestionExtension";
-import ReportExtension from "./Form/ReportExtension";
-import VerificationExtesion from "./Form/VerificationExtension";
-import { TrailingNodeExtension } from "remirror/extensions";
-import { RemirrorContentType } from "remirror";
-import EditorSourcesList from "./Components/Source/EditorSourceList";
-import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
-import { reviewingSelector } from "../../machines/reviewTask/selectors";
-import { useSelector } from "@xstate/react";
-import CommentContainer from "./Comment/CommentContainer";
-import { useAppSelector } from "../../store/store";
-
-interface CollaborativeEditorProps {
- placeholder: string;
- onContentChange: (state: any) => void;
-}
-
-const CollaborativeEditor = ({
- placeholder,
- onContentChange,
-}: CollaborativeEditorProps) => {
- const {
- websocketProvider,
- editorContentObject,
- editorSources,
- setEditorSources,
- } = useContext(CollaborativeEditorContext);
- const enableEditorAnnotations = useAppSelector(
- (state) => state?.enableEditorAnnotations
- );
- const { machineService } = useContext(ReviewTaskMachineContext);
- const readonly = useSelector(machineService, reviewingSelector);
- const users = websocketProvider?.awareness?.states?.size;
- const isCollaborative = users > 1;
-
- useEffect(() => {
- const sources = machineService.state.context.reviewData.sources;
- setEditorSources(sources);
- }, [machineService.state.context.reviewData.sources, setEditorSources]);
-
- function createExtensions() {
- const extensions: any = [
- new PlaceholderExtension({ placeholder }),
- new LinkExtension({
- extraAttributes: {
- id: () => null,
- },
- autoLink: true,
- selectTextOnClick: true,
- }),
- new SummaryExtension({ disableExtraAttributes: true }),
- new QuestionExtension({ disableExtraAttributes: true }),
- new ReportExtension({ disableExtraAttributes: true }),
- new VerificationExtesion({ disableExtraAttributes: true }),
- new TrailingNodeExtension(),
- ];
- if (websocketProvider) {
- extensions.push(
- new YjsExtension({ getProvider: () => websocketProvider })
- );
- }
-
- if (enableEditorAnnotations) {
- // TODO: Make annotations shared across documents
- extensions.push(new AnnotationExtension());
- }
-
- return extensions;
- }
-
- const { manager, state, setState } = useRemirror({
- extensions: createExtensions,
- core: { excludeExtensions: ["history"] },
- stringHandler: "html",
- content: isCollaborative
- ? undefined
- : (editorContentObject as RemirrorContentType),
- });
-
- const editorContentNode = useMemo(() => {
- if (!editorContentObject) {
- return null;
- }
-
- return manager?.schema?.nodeFromJSON(editorContentObject);
- }, [editorContentObject]);
-
- const handleChange = useCallback(
- ({ state }) => {
- onContentChange(state);
- setState(state);
- },
- [setState, onContentChange]
- );
-
- return (
-
-
-
-
-
-
-
-
- );
-};
-
-export default CollaborativeEditor;
diff --git a/src/components/Collaborative/CollaborativeEditorProvider.tsx b/src/components/Collaborative/CollaborativeEditorProvider.tsx
deleted file mode 100644
index 99ef4cc3b..000000000
--- a/src/components/Collaborative/CollaborativeEditorProvider.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { createContext, useContext, useEffect, useMemo, useState } from "react";
-import { useAppSelector } from "../../store/store";
-import { createWebsocketConnection } from "./utils/createWebsocketConnection";
-import ClaimReviewTaskApi from "../../api/ClaimReviewTaskApi";
-import { RemirrorJSON } from "remirror";
-import { SourceType } from "../../types/Source";
-import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
-
-interface ContextType {
- websocketProvider: any;
- editorContentObject?: RemirrorJSON;
- setEditorContentObject?: (data: any) => void;
- editorSources?: SourceType[];
- setEditorSources?: (data: any) => void;
- data_hash?: string;
- isFetchingEditor?: boolean;
- comments?: any[];
- setComments?: (data: any) => void;
- shouldInsertAIReport?: boolean;
- setShouldInsertAIReport?: (data: any) => void;
-}
-
-export const CollaborativeEditorContext = createContext({
- websocketProvider: null,
-});
-
-interface CollaborativeEditorProviderProps {
- data_hash: string;
- children: React.ReactNode;
-}
-
-export const CollaborativeEditorProvider = (
- props: CollaborativeEditorProviderProps
-) => {
- const { reportModel } = useContext(ReviewTaskMachineContext);
- const { enableCollaborativeEdit } = useAppSelector((state) => ({
- enableCollaborativeEdit: state?.enableCollaborativeEdit,
- }));
-
- const [editorContentObject, setEditorContentObject] = useState(null);
- const [editorSources, setEditorSources] = useState([]);
- const [isFetchingEditor, setIsFetchingEditor] = useState(false);
- const { websocketUrl } = useAppSelector((state) => state);
- const [comments, setComments] = useState(null);
- const [shouldInsertAIReport, setShouldInsertAIReport] = useState(false);
-
- useEffect(() => {
- const fetchEditorContentObject = (data_hash) => {
- setIsFetchingEditor(true);
- return ClaimReviewTaskApi.getEditorContentObject(
- data_hash,
- reportModel
- );
- };
-
- if (reportModel) {
- fetchEditorContentObject(props.data_hash).then((content) => {
- setEditorContentObject(content);
- setIsFetchingEditor(false);
- });
- }
- }, [props.data_hash, reportModel]);
-
- const websocketProvider = useMemo(() => {
- if (enableCollaborativeEdit) {
- return createWebsocketConnection(props.data_hash, websocketUrl);
- }
- return null;
- }, [enableCollaborativeEdit, props.data_hash, websocketUrl]);
-
- return (
-
- {props.children}
-
- );
-};
diff --git a/src/components/Collaborative/Comment/CommentCardActions.tsx b/src/components/Collaborative/Comment/CommentCardActions.tsx
index 47c6df06b..7c3bf51ae 100644
--- a/src/components/Collaborative/Comment/CommentCardActions.tsx
+++ b/src/components/Collaborative/Comment/CommentCardActions.tsx
@@ -4,9 +4,9 @@ import CheckIcon from "@mui/icons-material/Check";
import CommentPopoverContent from "./CommentPopoverContent";
import { Popover } from "antd";
import Button, { ButtonType } from "../../Button";
-import ClaimReviewTaskApi from "../../../api/ClaimReviewTaskApi";
+import ReviewTaskApi from "../../../api/reviewTaskApi";
import CommentApi from "../../../api/comment";
-import { CollaborativeEditorContext } from "../CollaborativeEditorProvider";
+import { VisualEditorContext } from "../VisualEditorProvider";
import { useCommands } from "@remirror/react";
import { currentUserRole } from "../../../atoms/currentUser";
import { useAtom } from "jotai";
@@ -20,7 +20,7 @@ const CommentCardActions = ({ content, setIsResolved }) => {
const enableEditorAnnotations = useAppSelector(
(state) => state?.enableEditorAnnotations
);
- const { data_hash, setComments } = useContext(CollaborativeEditorContext);
+ const { data_hash, setComments } = useContext(VisualEditorContext);
const { removeAnnotations } = useCommands();
const [role] = useAtom(currentUserRole);
const { machineService } = useContext(ReviewTaskMachineContext);
@@ -54,7 +54,7 @@ const CommentCardActions = ({ content, setIsResolved }) => {
)
);
} else {
- await ClaimReviewTaskApi.deleteComment(data_hash, content._id);
+ await ReviewTaskApi.deleteComment(data_hash, content._id);
setComments((comments) =>
comments.filter((c) => c._id !== content?._id)
);
diff --git a/src/components/Collaborative/Comment/CommentCardForm.tsx b/src/components/Collaborative/Comment/CommentCardForm.tsx
index e527bbd3c..60bef1739 100644
--- a/src/components/Collaborative/Comment/CommentCardForm.tsx
+++ b/src/components/Collaborative/Comment/CommentCardForm.tsx
@@ -1,9 +1,9 @@
import React, { useCallback, useContext, useState } from "react";
import Button, { ButtonType } from "../../Button";
import AletheiaInput from "../../AletheiaInput";
-import ClaimReviewTaskApi from "../../../api/ClaimReviewTaskApi";
+import ReviewTaskApi from "../../../api/reviewTaskApi";
import { useCommands, useCurrentSelection } from "@remirror/react";
-import { CollaborativeEditorContext } from "../CollaborativeEditorProvider";
+import { VisualEditorContext } from "../VisualEditorProvider";
import CommentApi from "../../../api/comment";
import { useTranslation } from "next-i18next";
import colors from "../../../styles/colors";
@@ -16,7 +16,7 @@ const CommentCardForm = ({ user, setIsCommentVisible, isEditing, content }) => {
const { t } = useTranslation();
const { from, to, $to } = useCurrentSelection();
const { addAnnotation } = useCommands();
- const { data_hash, setComments } = useContext(CollaborativeEditorContext);
+ const { data_hash, setComments } = useContext(VisualEditorContext);
const [isLoading, setIsLoading] = useState(false);
const [showButtons, setShowButtons] = useState(false);
const [commentValue, setCommentValue] = useState("");
@@ -70,7 +70,7 @@ const CommentCardForm = ({ user, setIsCommentVisible, isEditing, content }) => {
);
} else {
const { comment: createdComment } =
- await ClaimReviewTaskApi.addComment(data_hash, newComment);
+ await ReviewTaskApi.addComment(data_hash, newComment);
if (
enableEditorAnnotations &&
diff --git a/src/components/Collaborative/Comment/CommentContainer.tsx b/src/components/Collaborative/Comment/CommentContainer.tsx
index 799f6bd37..10095651d 100644
--- a/src/components/Collaborative/Comment/CommentContainer.tsx
+++ b/src/components/Collaborative/Comment/CommentContainer.tsx
@@ -7,7 +7,7 @@ import {
useCommands,
useHelpers,
} from "@remirror/react";
-import { CollaborativeEditorContext } from "../CollaborativeEditorProvider";
+import { VisualEditorContext } from "../VisualEditorProvider";
import { Row } from "antd";
import CommentsList from "./CommentsList";
import { ReviewTaskMachineContext } from "../../../machines/reviewTask/ReviewTaskMachineProvider";
@@ -24,7 +24,7 @@ const CommentContainer = ({ readonly, state }) => {
const enableEditorAnnotations = useAppSelector(
(state) => state?.enableEditorAnnotations
);
- const { comments, setComments } = useContext(CollaborativeEditorContext);
+ const { comments, setComments } = useContext(VisualEditorContext);
const { machineService } = useContext(ReviewTaskMachineContext);
const reviewData = useSelector(machineService, reviewDataSelector);
const [role] = useAtom(currentUserRole);
diff --git a/src/components/Collaborative/Components/ClaimReviewEditor.tsx b/src/components/Collaborative/Components/ClaimReviewEditor.tsx
new file mode 100644
index 000000000..228a0a875
--- /dev/null
+++ b/src/components/Collaborative/Components/ClaimReviewEditor.tsx
@@ -0,0 +1,55 @@
+import React, { useContext, useEffect } from "react";
+import FloatingLinkToolbar from "./LinkToolBar/FloatingLinkToolbar";
+import EditorSourcesList from "./Source/EditorSourceList";
+
+import CopilotDrawer from "../../Copilot/CopilotDrawer";
+import { useAppSelector } from "../../../store/store";
+import { useDispatch } from "react-redux";
+import actions from "../../../store/actions";
+import { ReviewTaskMachineContext } from "../../../machines/reviewTask/ReviewTaskMachineProvider";
+
+const ClaimReviewEditor = ({ manager, state, editorSources }) => {
+ const { claim, sentenceContent } = useContext(ReviewTaskMachineContext);
+ const dispatch = useDispatch();
+ const { enableCopilotChatBot, reviewDrawerCollapsed } = useAppSelector(
+ (state) => ({
+ enableCopilotChatBot: state?.enableCopilotChatBot,
+ reviewDrawerCollapsed:
+ state?.reviewDrawerCollapsed !== undefined
+ ? state?.reviewDrawerCollapsed
+ : true,
+ })
+ );
+
+ useEffect(() => {
+ if (enableCopilotChatBot && reviewDrawerCollapsed) {
+ dispatch(actions.openCopilotDrawer());
+ }
+
+ return () => {
+ dispatch(actions.closeCopilotDrawer());
+ };
+ }, []);
+
+ const showCopilot = enableCopilotChatBot && reviewDrawerCollapsed;
+
+ return (
+ <>
+
+
+ {showCopilot && (
+
+ )}
+ >
+ );
+};
+
+export default ClaimReviewEditor;
diff --git a/src/components/Collaborative/Editor.style.tsx b/src/components/Collaborative/Components/Editor.style.tsx
similarity index 97%
rename from src/components/Collaborative/Editor.style.tsx
rename to src/components/Collaborative/Components/Editor.style.tsx
index 01e29507f..6503996f7 100644
--- a/src/components/Collaborative/Editor.style.tsx
+++ b/src/components/Collaborative/Components/Editor.style.tsx
@@ -1,4 +1,4 @@
-import colors from "../../styles/colors";
+import colors from "../../../styles/colors";
import styled from "styled-components";
const EditorStyle = styled.div`
diff --git a/src/components/Collaborative/Editor.tsx b/src/components/Collaborative/Components/Editor.tsx
similarity index 54%
rename from src/components/Collaborative/Editor.tsx
rename to src/components/Collaborative/Components/Editor.tsx
index 39fc58aaa..8aa20ba34 100644
--- a/src/components/Collaborative/Editor.tsx
+++ b/src/components/Collaborative/Components/Editor.tsx
@@ -1,64 +1,38 @@
-import React, { useEffect, useCallback, useContext } from "react";
+import React, { useCallback, useContext } from "react";
-import { useHelpers } from "@remirror/react";
-import { useCommands } from "@remirror/react";
-import { getSummaryContentHtml } from "./Form/SummaryCard";
-import { getQuestionContentHtml } from "./Form/QuestionCard";
-import { getReportContentHtml } from "./Form/ReportCard";
-import { getVerificationContentHtml } from "./Form/VerificationCard";
+import { useHelpers, useCommands } from "@remirror/react";
import SummarizeIcon from "@mui/icons-material/Summarize";
import QuestionMarkIcon from "@mui/icons-material/QuestionMark";
import ReportProblemIcon from "@mui/icons-material/ReportProblem";
import FactCheckIcon from "@mui/icons-material/FactCheck";
import { Button } from "antd";
import EditorStyle from "./Editor.style";
-import { CollaborativeEditorContext } from "./CollaborativeEditorProvider";
-import useCardPresence from "./hooks/useCardPresence";
-import { Node } from "@remirror/pm/model";
-import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
-import { ReportModelEnum } from "../../machines/reviewTask/enums";
+import useCardPresence from "../hooks/useCardPresence";
+import { ReviewTaskMachineContext } from "../../../machines/reviewTask/ReviewTaskMachineProvider";
+import {
+ ReportModelEnum,
+ ReviewTaskTypeEnum,
+} from "../../../machines/reviewTask/enums";
+import { getContentHtml } from "../Form/EditorCard";
+import { EditorState } from "remirror";
/**
- * Modifies context state to JSON using useHelpers hook
- * which can only be used inside a remirror component.
- * Also returns a inputs toolbar which can be used for users
- * to add a new input.
+ * @param editable enable or disable buttons
* @param state remirror state
+ * @returns An toolbar used for users to add a new input.
*/
const Editor = ({
- node,
editable,
state,
}: {
- node: Node;
editable: boolean;
- state: any;
+ state: EditorState;
}) => {
const command = useCommands();
- const {
- setEditorContentObject,
- shouldInsertAIReport,
- setShouldInsertAIReport,
- } = useContext(CollaborativeEditorContext);
- const { reportModel } = useContext(ReviewTaskMachineContext);
- const { getJSON } = useHelpers();
-
- useEffect(
- () => setEditorContentObject(getJSON()),
- [state, getJSON, setEditorContentObject]
+ const { reportModel, reviewTaskType } = useContext(
+ ReviewTaskMachineContext
);
-
- /**
- * Deletes current report and insert automated fact-checking generated report.
- * This solution is need because when we try to update the editorContentObject react state, the remirror state overrides the changes, not applying the generated report into the document.
- */
- useEffect(() => {
- if (shouldInsertAIReport) {
- command.delete({ from: 0, to: state.doc.content.size });
- command.insertNode(node);
- }
- setShouldInsertAIReport(false);
- }, [shouldInsertAIReport]);
+ const { getJSON } = useHelpers();
const summaryDisabled = useCardPresence(getJSON, state, "summary", false);
const reportDisabled = useCardPresence(getJSON, state, "report", false);
@@ -70,9 +44,9 @@ const Editor = ({
);
const handleInsertNode = useCallback(
- (insertNodeFunction) => {
+ (insertNode) => {
const { doc } = state;
- command.insertHtml(insertNodeFunction(), {
+ command.insertHtml(insertNode, {
selection: doc.content.size,
replaceEmptyParentBlock: true,
});
@@ -82,7 +56,7 @@ const Editor = ({
const actions = [
{
- onClick: () => handleInsertNode(getSummaryContentHtml),
+ onClick: () => handleInsertNode(getContentHtml("data-summary-id")),
disabled: editable || summaryDisabled,
"data-cy": "testClaimReviewsummarizeAdd",
icon: ,
@@ -92,19 +66,22 @@ const Editor = ({
if (reportModel === ReportModelEnum.FactChecking) {
actions.push(
{
- onClick: () => handleInsertNode(getVerificationContentHtml),
+ onClick: () =>
+ handleInsertNode(getContentHtml("data-verification-id")),
disabled: editable || verificationDisabled,
"data-cy": "testClaimReviewverificationAdd",
icon: ,
},
{
- onClick: () => handleInsertNode(getReportContentHtml),
+ onClick: () =>
+ handleInsertNode(getContentHtml("data-report-id")),
disabled: editable || reportDisabled,
"data-cy": "testClaimReviewreportAdd",
icon: ,
},
{
- onClick: () => handleInsertNode(getQuestionContentHtml),
+ onClick: () =>
+ handleInsertNode(getContentHtml("data-question-id")),
disabled: editable,
"data-cy": "testClaimReviewquestionsAdd",
icon: ,
@@ -112,6 +89,10 @@ const Editor = ({
);
}
+ if (reviewTaskType !== ReviewTaskTypeEnum.Claim) {
+ return <>>;
+ }
+
return (
diff --git a/src/components/Collaborative/Components/Source/EditorAddSources.tsx b/src/components/Collaborative/Components/Source/EditorAddSources.tsx
index 4999e7c27..222747d9b 100644
--- a/src/components/Collaborative/Components/Source/EditorAddSources.tsx
+++ b/src/components/Collaborative/Components/Source/EditorAddSources.tsx
@@ -2,7 +2,7 @@ import React, { useContext, useState } from "react";
import AletheiaButton, { ButtonType } from "../../../Button";
import { uniqueId } from "remirror";
import SourceDialog from "../LinkToolBar/Dialog/SourceDialog";
-import { CollaborativeEditorContext } from "../../CollaborativeEditorProvider";
+import { VisualEditorContext } from "../../VisualEditorProvider";
import { useTranslation } from "next-i18next";
import { PlusOutlined } from "@ant-design/icons";
import { URL_PATTERN } from "../../hooks/useFloatingLinkState";
@@ -26,7 +26,7 @@ const EditorAddSources = ({
const [href, setHref] = useState("https://");
const [showDialog, setShowDialog] = useState(false);
const [error, setError] = useState(null);
- const { setEditorSources } = useContext(CollaborativeEditorContext);
+ const { setEditorSources } = useContext(VisualEditorContext);
const [isLoading, setIsLoading] = useState(false);
const validateFloatingLink = () => {
diff --git a/src/components/Collaborative/Components/Source/EditorSourcePopover.tsx b/src/components/Collaborative/Components/Source/EditorSourcePopover.tsx
index c4c1e0a6e..8e025f2ea 100644
--- a/src/components/Collaborative/Components/Source/EditorSourcePopover.tsx
+++ b/src/components/Collaborative/Components/Source/EditorSourcePopover.tsx
@@ -2,7 +2,7 @@ import React, { useContext } from "react";
import { Popover } from "antd";
import EditorSourcePopoverContent from "./EditorSourcePopoverContent";
import { useCommands } from "@remirror/react";
-import { CollaborativeEditorContext } from "../../CollaborativeEditorProvider";
+import { VisualEditorContext } from "../../VisualEditorProvider";
import { ProsemirrorNode } from "remirror";
/**
@@ -25,7 +25,7 @@ const EditorSourcePopover = ({
children: React.ReactNode;
}) => {
const command = useCommands();
- const { setEditorSources } = useContext(CollaborativeEditorContext);
+ const { setEditorSources } = useContext(VisualEditorContext);
const { props, href } = source;
function findMarkPositions(
diff --git a/src/components/Collaborative/Components/SourceEditorButton.tsx b/src/components/Collaborative/Components/SourceEditorButton.tsx
new file mode 100644
index 000000000..921cac94b
--- /dev/null
+++ b/src/components/Collaborative/Components/SourceEditorButton.tsx
@@ -0,0 +1,68 @@
+import React, { useCallback, useContext, useState } from "react";
+import AletheiaButton from "../../Button";
+import { Col } from "antd";
+import SummarizationApi from "../../../api/summarizationApi";
+import { useTranslation } from "next-i18next";
+import { VisualEditorContext } from "../VisualEditorProvider";
+
+const SourceEditorButton = ({ manager, state, readonly }) => {
+ const { t } = useTranslation();
+ const [isLoading, setIsLoading] = useState(false);
+ const { source } = useContext(VisualEditorContext);
+
+ const summarizeSource = useCallback(async () => {
+ setIsLoading(true);
+ const { output: summary } = await SummarizationApi.summarizeSource(
+ source
+ );
+ setIsLoading(false);
+ return manager.view.updateState(
+ manager.createState({
+ content: {
+ content: [
+ {
+ type: "summary",
+ content: [
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: summary,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ type: "doc",
+ },
+ })
+ );
+ }, [manager, state.doc]);
+
+ return (
+
+
+ {t("sourceForm:summarizeSouce")}
+
+
+ );
+};
+
+export default SourceEditorButton;
diff --git a/src/components/Collaborative/Components/SourceReviewEditor.tsx b/src/components/Collaborative/Components/SourceReviewEditor.tsx
new file mode 100644
index 000000000..0e10945d1
--- /dev/null
+++ b/src/components/Collaborative/Components/SourceReviewEditor.tsx
@@ -0,0 +1,8 @@
+import React from "react";
+import SourceEditorButton from "./SourceEditorButton";
+
+const SourceReviewEditor = (props) => {
+ return
;
+};
+
+export default SourceReviewEditor;
diff --git a/src/components/Collaborative/Form/BaseNodeExtesion.tsx b/src/components/Collaborative/Form/BaseNodeExtesion.tsx
new file mode 100644
index 000000000..c2c0be84c
--- /dev/null
+++ b/src/components/Collaborative/Form/BaseNodeExtesion.tsx
@@ -0,0 +1,77 @@
+import {
+ NodeExtension,
+ NodeExtensionSpec,
+ ExtensionTag,
+ DOMCompatibleAttributes,
+} from "@remirror/core";
+import React, { ComponentType } from "react";
+import { NodeViewComponentProps } from "@remirror/react";
+
+function createNodeExtension({ name, componentName, dataAttributeName }) {
+ return class extends NodeExtension {
+ get name() {
+ return name;
+ }
+
+ ReactComponent: ComponentType
= ({
+ node,
+ forwardRef,
+ getPosition,
+ }) => {
+ const Component = componentName;
+
+ return (
+
+ );
+ };
+
+ createTags() {
+ return [ExtensionTag.Block];
+ }
+
+ createNodeSpec(): NodeExtensionSpec {
+ return {
+ selectable: false,
+ /**
+ * Atom is needed to create a boundary between the card and
+ * others elements in the editor
+ */
+ atom: false,
+ /**
+ * isolating is needed to not allow cards to get deleted
+ * whend deleting lines
+ */
+ isolating: true,
+ content: "block*",
+ toDOM: (node) => {
+ const attrs: DOMCompatibleAttributes = {
+ [`data-${dataAttributeName}`]:
+ node.attrs[dataAttributeName],
+ };
+ return ["div", attrs, 0];
+ },
+ parseDOM: [
+ {
+ attrs: {
+ [dataAttributeName]: { default: "" },
+ },
+ tag: `div[data-${dataAttributeName}]`,
+ getAttrs: (dom) => {
+ const node = dom as HTMLDivElement;
+ const value = node.getAttribute(
+ `data-${dataAttributeName}`
+ );
+ return { [dataAttributeName]: value };
+ },
+ },
+ ],
+ };
+ }
+ };
+}
+
+export default createNodeExtension;
diff --git a/src/components/Collaborative/Form/CardStyle.tsx b/src/components/Collaborative/Form/CardStyle.tsx
index 17a37d5f3..0a9ccf86c 100644
--- a/src/components/Collaborative/Form/CardStyle.tsx
+++ b/src/components/Collaborative/Form/CardStyle.tsx
@@ -7,7 +7,6 @@ const CardStyle = styled(Row)`
display: flex;
justify-content: space-between;
padding: 8px 0px;
- gap: 8px;
.card-container {
width: 100%;
@@ -23,7 +22,6 @@ const CardStyle = styled(Row)`
border-radius: 4px;
border: none;
width: 100%;
- min-height: 100px;
height: auto;
padding: 10px;
}
diff --git a/src/components/Collaborative/Form/EditorCard.tsx b/src/components/Collaborative/Form/EditorCard.tsx
new file mode 100644
index 000000000..84b656972
--- /dev/null
+++ b/src/components/Collaborative/Form/EditorCard.tsx
@@ -0,0 +1,45 @@
+import React from "react";
+import { uniqueId } from "remirror";
+import { Col } from "antd";
+import CardStyle from "./CardStyle";
+
+interface EditorCardProps {
+ label?: string;
+ dataCy?: string;
+ forwardRef?: any;
+ extra?: React.ReactNode;
+ span?: number;
+ inputSize?: number;
+}
+
+const EditorCard = ({
+ label,
+ dataCy,
+ forwardRef,
+ extra,
+ span = 24,
+ inputSize = 100,
+}: EditorCardProps) => {
+ return (
+
+
+
+
+
+ {extra}
+
+ );
+};
+
+export const getContentHtml = (dataAttributeName) => `
+ `;
+
+export default EditorCard;
diff --git a/src/components/Collaborative/Form/QuestionCard.tsx b/src/components/Collaborative/Form/QuestionCard.tsx
index b9215dcfd..6662d3f7b 100644
--- a/src/components/Collaborative/Form/QuestionCard.tsx
+++ b/src/components/Collaborative/Form/QuestionCard.tsx
@@ -1,16 +1,16 @@
import React, { useCallback, useContext } from "react";
-import { uniqueId } from "remirror";
import { Col } from "antd";
import Button from "../../Button";
import { useCommands } from "@remirror/react";
import { DeleteOutlined } from "@ant-design/icons";
-import CardStyle from "./CardStyle";
import { useTranslation } from "next-i18next";
import { ReviewTaskMachineContext } from "../../../machines/reviewTask/ReviewTaskMachineProvider";
import { reviewingSelector } from "../../../machines/reviewTask/selectors";
import { useSelector } from "@xstate/react";
+import EditorCard from "./EditorCard";
+import { uniqueId } from "remirror";
-export const QuestionCard = ({ forwardRef, node, initialPosition }) => {
+const QuestionCard = ({ forwardRef, node, initialPosition }) => {
const { t } = useTranslation();
const { machineService } = useContext(ReviewTaskMachineContext);
const editable = useSelector(machineService, reviewingSelector);
@@ -26,20 +26,13 @@ export const QuestionCard = ({ forwardRef, node, initialPosition }) => {
);
return (
-
-
-
-
-
-
+
-
-
+ }
+ />
);
};
@@ -59,3 +52,5 @@ export const getQuestionContentHtml = () => `
`;
+
+export default QuestionCard;
diff --git a/src/components/Collaborative/Form/QuestionExtension.tsx b/src/components/Collaborative/Form/QuestionExtension.tsx
index f1de64ba8..7358ef246 100644
--- a/src/components/Collaborative/Form/QuestionExtension.tsx
+++ b/src/components/Collaborative/Form/QuestionExtension.tsx
@@ -1,75 +1,10 @@
-import {
- DOMCompatibleAttributes,
- ExtensionTag,
- NodeExtension,
- NodeExtensionSpec,
-} from "@remirror/core";
-import React, { ComponentType } from "react";
-import { NodeViewComponentProps } from "@remirror/react";
-import { QuestionCard } from "./QuestionCard";
-
-class QuestionExtension extends NodeExtension {
- get name() {
- return "questions" as const;
- }
-
- ReactComponent: ComponentType = ({
- node,
- forwardRef,
- getPosition,
- }) => {
- return (
-
- );
- };
-
- createTags() {
- return [ExtensionTag.Block];
- }
-
- createNodeSpec(): NodeExtensionSpec {
- return {
- selectable: false,
- /**
- * Atom is needed to create a boundary between the card and
- * others elements in the editor
- */
- atom: false,
- /**
- * isolating is needed to not allow cards to get deleted
- * whend deleting lines
- */
- isolating: true,
- content: "block*",
- toDOM: (node) => {
- const attrs: DOMCompatibleAttributes = {
- "data-question-id": node.attrs.questionId,
- };
- return ["div", attrs, 0];
- },
- parseDOM: [
- {
- attrs: {
- questionId: { default: "" },
- },
- tag: `div[data-question-id]`,
- getAttrs: (dom) => {
- const node = dom as HTMLDivElement;
- const questionId =
- node.getAttribute("data-question-id");
-
- return {
- questionId,
- };
- },
- },
- ],
- };
- }
-}
+import createNodeExtension from "./BaseNodeExtesion";
+import QuestionCard from "./QuestionCard";
+
+const QuestionExtension = createNodeExtension({
+ name: "questions",
+ componentName: QuestionCard,
+ dataAttributeName: "question-id",
+});
export default QuestionExtension;
diff --git a/src/components/Collaborative/Form/ReportCard.tsx b/src/components/Collaborative/Form/ReportCard.tsx
index 3d20901c4..d54359d86 100644
--- a/src/components/Collaborative/Form/ReportCard.tsx
+++ b/src/components/Collaborative/Form/ReportCard.tsx
@@ -1,26 +1,16 @@
import React from "react";
-import { uniqueId } from "remirror";
-import { Col } from "antd";
-import CardStyle from "./CardStyle";
import { useTranslation } from "next-i18next";
+import EditorCard from "./EditorCard";
-export const ReportCard = ({ forwardRef, node }) => {
+const ReportCard = ({ forwardRef }) => {
const { t } = useTranslation();
return (
-
-
-
-
-
-
+
);
};
-export const getReportContentHtml = () => `
- `;
+export default ReportCard;
diff --git a/src/components/Collaborative/Form/ReportExtension.tsx b/src/components/Collaborative/Form/ReportExtension.tsx
index baab16fc5..a0d351cc1 100644
--- a/src/components/Collaborative/Form/ReportExtension.tsx
+++ b/src/components/Collaborative/Form/ReportExtension.tsx
@@ -1,66 +1,10 @@
-import {
- DOMCompatibleAttributes,
- ExtensionTag,
- NodeExtension,
- NodeExtensionSpec,
-} from "@remirror/core";
-import React, { ComponentType } from "react";
-import { NodeViewComponentProps } from "@remirror/react";
-import { ReportCard } from "./ReportCard";
-
-class ReportExtension extends NodeExtension {
- get name() {
- return "report" as const;
- }
-
- ReactComponent: ComponentType = ({
- node,
- forwardRef,
- }) => {
- return ;
- };
-
- createTags() {
- return [ExtensionTag.Block];
- }
- createNodeSpec(): NodeExtensionSpec {
- return {
- selectable: false,
- /**
- * Atom is needed to create a boundary between the card and
- * others elements in the editor
- */
- atom: false,
- /**
- * isolating is needed to not allow cards to get deleted
- * whend deleting lines
- */
- isolating: true,
- content: "block*",
- toDOM: (node) => {
- const attrs: DOMCompatibleAttributes = {
- "data-report-id": node.attrs.reportId,
- };
- return ["div", attrs, 0];
- },
- parseDOM: [
- {
- attrs: {
- reportId: { default: "" },
- },
- tag: `div[data-report-id]`,
- getAttrs: (dom) => {
- const node = dom as HTMLDivElement;
- const reportId = node.getAttribute("data-report-id");
-
- return {
- reportId,
- };
- },
- },
- ],
- };
- }
-}
+import createNodeExtension from "./BaseNodeExtesion";
+import ReportCard from "./ReportCard";
+
+const ReportExtension = createNodeExtension({
+ name: "report",
+ componentName: ReportCard,
+ dataAttributeName: "report-id",
+});
export default ReportExtension;
diff --git a/src/components/Collaborative/Form/SummaryCard.tsx b/src/components/Collaborative/Form/SummaryCard.tsx
index 5f8dc2855..0842291dd 100644
--- a/src/components/Collaborative/Form/SummaryCard.tsx
+++ b/src/components/Collaborative/Form/SummaryCard.tsx
@@ -1,31 +1,24 @@
import React, { useContext } from "react";
-import { uniqueId } from "remirror";
-import { Col } from "antd";
-import CardStyle from "./CardStyle";
import { useTranslation } from "next-i18next";
import { ReviewTaskMachineContext } from "../../../machines/reviewTask/ReviewTaskMachineProvider";
import { ReportModelEnum } from "../../../machines/reviewTask/enums";
+import EditorCard from "./EditorCard";
-export const SummaryCard = ({ forwardRef, node }) => {
- const { reportModel } = useContext(ReviewTaskMachineContext);
+const SummaryCard = ({ forwardRef }) => {
const { t } = useTranslation();
+ const { reportModel } = useContext(ReviewTaskMachineContext);
+ const label =
+ reportModel === ReportModelEnum.InformativeNews
+ ? t("claimReviewForm:informativeNewsLabel")
+ : t("claimReviewForm:summaryLabel");
+
return (
-
-
-
-
-
-
+
);
};
-export const getSummaryContentHtml = () => `
- `;
+export default SummaryCard;
diff --git a/src/components/Collaborative/Form/SummaryExtesion.tsx b/src/components/Collaborative/Form/SummaryExtesion.tsx
index 218cbd49c..f3ea169dc 100644
--- a/src/components/Collaborative/Form/SummaryExtesion.tsx
+++ b/src/components/Collaborative/Form/SummaryExtesion.tsx
@@ -1,66 +1,10 @@
-import {
- DOMCompatibleAttributes,
- ExtensionTag,
- NodeExtension,
- NodeExtensionSpec,
-} from "@remirror/core";
-import React, { ComponentType } from "react";
-import { NodeViewComponentProps } from "@remirror/react";
-import { SummaryCard } from "./SummaryCard";
+import createNodeExtension from "./BaseNodeExtesion";
+import SummaryCard from "./SummaryCard";
-class SummaryExtesion extends NodeExtension {
- get name() {
- return "summary" as const;
- }
+const SummaryExtension = createNodeExtension({
+ name: "summary",
+ componentName: SummaryCard,
+ dataAttributeName: "summary-id",
+});
- ReactComponent: ComponentType = ({
- node,
- forwardRef,
- }) => {
- return ;
- };
-
- createTags() {
- return [ExtensionTag.Block];
- }
- createNodeSpec(): NodeExtensionSpec {
- return {
- selectable: false,
- /**
- * Atom is needed to create a boundary between the card and
- * others elements in the editor
- */
- atom: false,
- /**
- * isolating is needed to not allow cards to get deleted
- * whend deleting lines
- */
- isolating: true,
- content: "block*",
- toDOM: (node) => {
- const attrs: DOMCompatibleAttributes = {
- "data-summary-id": node.attrs.summaryId,
- };
- return ["div", attrs, 0];
- },
- parseDOM: [
- {
- attrs: {
- summaryId: { default: "" },
- },
- tag: `div[data-summary-id]`,
- getAttrs: (dom) => {
- const node = dom as HTMLDivElement;
- const summaryId = node.getAttribute("data-summary-id");
-
- return {
- summaryId,
- };
- },
- },
- ],
- };
- }
-}
-
-export default SummaryExtesion;
+export default SummaryExtension;
diff --git a/src/components/Collaborative/Form/VerificationCard.tsx b/src/components/Collaborative/Form/VerificationCard.tsx
index 5cae1c034..49db08900 100644
--- a/src/components/Collaborative/Form/VerificationCard.tsx
+++ b/src/components/Collaborative/Form/VerificationCard.tsx
@@ -1,27 +1,16 @@
import React from "react";
-import { uniqueId } from "remirror";
-import { Col } from "antd";
-import CardStyle from "./CardStyle";
import { useTranslation } from "next-i18next";
+import EditorCard from "./EditorCard";
-export const VerificationCard = ({ forwardRef, node }) => {
+const VerificationCard = ({ forwardRef }) => {
const { t } = useTranslation();
return (
-
-
-
-
-
-
+
);
};
-export const getVerificationContentHtml = () => `
- `;
+export default VerificationCard;
diff --git a/src/components/Collaborative/Form/VerificationExtension.tsx b/src/components/Collaborative/Form/VerificationExtension.tsx
index 3e4b0967e..937091732 100644
--- a/src/components/Collaborative/Form/VerificationExtension.tsx
+++ b/src/components/Collaborative/Form/VerificationExtension.tsx
@@ -1,68 +1,10 @@
-import {
- DOMCompatibleAttributes,
- ExtensionTag,
- NodeExtension,
- NodeExtensionSpec,
-} from "@remirror/core";
-import React, { ComponentType } from "react";
-import { NodeViewComponentProps } from "@remirror/react";
-import { VerificationCard } from "./VerificationCard";
+import createNodeExtension from "./BaseNodeExtesion";
+import VerificationCard from "./VerificationCard";
-class VerificationExtesion extends NodeExtension {
- get name() {
- return "verification" as const;
- }
+const VerificationExtension = createNodeExtension({
+ name: "verification",
+ componentName: VerificationCard,
+ dataAttributeName: "verification-id",
+});
- ReactComponent: ComponentType = ({
- node,
- forwardRef,
- }) => {
- return ;
- };
-
- createTags() {
- return [ExtensionTag.Block];
- }
- createNodeSpec(): NodeExtensionSpec {
- return {
- selectable: false,
- /**
- * Atom is needed to create a boundary between the card and
- * others elements in the editor
- */
- atom: false,
- /**
- * isolating is needed to not allow cards to get deleted
- * whend deleting lines
- */
- isolating: true,
- content: "block*",
- toDOM: (node) => {
- const attrs: DOMCompatibleAttributes = {
- "data-verification-id": node.attrs.verificationId,
- };
- return ["div", attrs, 0];
- },
- parseDOM: [
- {
- attrs: {
- verificationId: { default: "" },
- },
- tag: `div[data-verification-id]`,
- getAttrs: (dom) => {
- const node = dom as HTMLDivElement;
- const verificationId = node.getAttribute(
- "data-verification-id"
- );
-
- return {
- verificationId,
- };
- },
- },
- ],
- };
- }
-}
-
-export default VerificationExtesion;
+export default VerificationExtension;
diff --git a/src/components/Collaborative/CollaborativeEditor.style.tsx b/src/components/Collaborative/VisualEditor.style.tsx
similarity index 82%
rename from src/components/Collaborative/CollaborativeEditor.style.tsx
rename to src/components/Collaborative/VisualEditor.style.tsx
index 2fd7fd2ec..bd6b920dd 100644
--- a/src/components/Collaborative/CollaborativeEditor.style.tsx
+++ b/src/components/Collaborative/VisualEditor.style.tsx
@@ -2,11 +2,10 @@ import { AllStyledComponent } from "@remirror/styles/styled-components";
import colors from "../../styles/colors";
import styled from "styled-components";
-const CollaborativeEditorStyle = styled(AllStyledComponent)`
+const VisualEditorStyled = styled(AllStyledComponent)`
background-color: ${colors.lightGray};
border-radius: 4px;
border: none;
- min-height: 40vh;
width: 100%;
display: flex;
justify-content: space-between;
@@ -30,4 +29,4 @@ const CollaborativeEditorStyle = styled(AllStyledComponent)`
}
`;
-export default CollaborativeEditorStyle;
+export default VisualEditorStyled;
diff --git a/src/components/Collaborative/VisualEditor.tsx b/src/components/Collaborative/VisualEditor.tsx
new file mode 100644
index 000000000..f905a79b5
--- /dev/null
+++ b/src/components/Collaborative/VisualEditor.tsx
@@ -0,0 +1,51 @@
+import React, { useCallback, useContext } from "react";
+import { Remirror, useRemirror } from "@remirror/react";
+import { VisualEditorContext } from "./VisualEditorProvider";
+import VisualEditorStyled from "./VisualEditor.style";
+import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
+import { EditorConfig } from "./utils/getEditorConfig";
+import Editor from "./Components/Editor";
+import CommentContainer from "./Comment/CommentContainer";
+
+interface VisualEditorProps {
+ onContentChange: (state: any, type: string) => void;
+}
+
+const editorConfig = new EditorConfig();
+
+const VisualEditor = ({ onContentChange }: VisualEditorProps) => {
+ const { getComponents } = editorConfig;
+ const { editorConfiguration, editorSources } =
+ useContext(VisualEditorContext);
+ const { reviewTaskType } = useContext(ReviewTaskMachineContext);
+ const { manager, state, setState } = useRemirror(editorConfiguration);
+ const { readonly } = editorConfiguration;
+ const getComponentsProps = { state, manager, readonly, editorSources };
+
+ const handleChange = useCallback(
+ ({ state }) => {
+ onContentChange(state, reviewTaskType);
+ setState(state);
+ },
+ [setState, onContentChange, reviewTaskType]
+ );
+
+ return (
+
+
+ {getComponents(reviewTaskType, getComponentsProps)}
+
+
+
+
+ );
+};
+
+export default VisualEditor;
diff --git a/src/components/Collaborative/VisualEditorProvider.tsx b/src/components/Collaborative/VisualEditorProvider.tsx
new file mode 100644
index 000000000..d0dae1ff1
--- /dev/null
+++ b/src/components/Collaborative/VisualEditorProvider.tsx
@@ -0,0 +1,130 @@
+import { createContext, useContext, useEffect, useMemo, useState } from "react";
+import { useAppSelector } from "../../store/store";
+import { createWebsocketConnection } from "./utils/createWebsocketConnection";
+import ReviewTaskApi from "../../api/reviewTaskApi";
+import { RemirrorContentType } from "remirror";
+import { SourceType } from "../../types/Source";
+import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
+import { EditorConfig } from "./utils/getEditorConfig";
+import { reviewingSelector } from "../../machines/reviewTask/selectors";
+import { useSelector } from "@xstate/react";
+
+interface ContextType {
+ editorConfiguration?: any;
+ editorSources?: SourceType[];
+ setEditorSources?: (data: any) => void;
+ data_hash?: string;
+ isFetchingEditor?: boolean;
+ comments?: any[];
+ setComments?: (data: any) => void;
+ source?: string;
+}
+
+export const VisualEditorContext = createContext({});
+
+interface VisualEditorProviderProps {
+ data_hash: string;
+ children: React.ReactNode;
+ source?: string;
+}
+
+export const VisualEditorProvider = (props: VisualEditorProviderProps) => {
+ const editorConfig = new EditorConfig();
+ const { machineService, reportModel, reviewTaskType } = useContext(
+ ReviewTaskMachineContext
+ );
+ const { enableCollaborativeEdit, enableEditorAnnotations, websocketUrl } =
+ useAppSelector((state) => ({
+ enableCollaborativeEdit: state?.enableCollaborativeEdit,
+ enableEditorAnnotations: state?.enableEditorAnnotations,
+ websocketUrl: state?.websocketUrl,
+ }));
+
+ const [editorContentObject, setEditorContentObject] = useState(null);
+ const [editorSources, setEditorSources] = useState(
+ machineService.state.context.reviewData.sources
+ );
+ const [isFetchingEditor, setIsFetchingEditor] = useState(false);
+ const [comments, setComments] = useState(null);
+ const readonly = useSelector(machineService, reviewingSelector);
+
+ useEffect(() => {
+ const params = { reportModel, reviewTaskType };
+ const fetchEditorContentObject = (data_hash) => {
+ setIsFetchingEditor(true);
+ return ReviewTaskApi.getEditorContentObject(data_hash, params);
+ };
+
+ if (reportModel) {
+ fetchEditorContentObject(props.data_hash).then((content) => {
+ setEditorContentObject(content);
+ setIsFetchingEditor(false);
+ });
+ }
+ }, [props.data_hash, reportModel]);
+
+ const { websocketProvider, isCollaborative } = useMemo(() => {
+ if (!enableCollaborativeEdit) {
+ return { websocketProvider: null, isCollaborative: null };
+ }
+
+ const websocketProvider = createWebsocketConnection(
+ props.data_hash,
+ websocketUrl
+ );
+ return {
+ websocketProvider,
+ isCollaborative: websocketProvider?.awareness?.states?.size > 1,
+ };
+ }, [enableCollaborativeEdit, props.data_hash, websocketUrl]);
+
+ const extensions = useMemo(
+ () =>
+ editorConfig.getExtensions(
+ reviewTaskType,
+ websocketProvider,
+ enableEditorAnnotations
+ ),
+ [websocketProvider, reviewTaskType]
+ );
+
+ const editorConfiguration = {
+ readonly,
+ extensions,
+ isCollaborative,
+ core: { excludeExtensions: ["history"] },
+ stringHandler: "html",
+ content: isCollaborative
+ ? undefined
+ : (editorContentObject as RemirrorContentType),
+ };
+
+ const value = useMemo(
+ () => ({
+ editorConfiguration,
+ editorSources,
+ setEditorSources,
+ data_hash: props.data_hash,
+ isFetchingEditor,
+ comments,
+ setComments,
+ source: props.source,
+ }),
+ [
+ editorConfiguration,
+ editorSources,
+ setEditorSources,
+ isFetchingEditor,
+ comments,
+ setComments,
+ props.data_hash,
+ props.source,
+ ]
+ );
+
+ return (
+
+ {props.children}
+
+ );
+};
diff --git a/src/components/Collaborative/hooks/useFloatingLinkState.tsx b/src/components/Collaborative/hooks/useFloatingLinkState.tsx
index 914abb05f..47d37eaf2 100644
--- a/src/components/Collaborative/hooks/useFloatingLinkState.tsx
+++ b/src/components/Collaborative/hooks/useFloatingLinkState.tsx
@@ -15,7 +15,7 @@ import {
useUpdateReason,
} from "@remirror/react";
import { useTranslation } from "next-i18next";
-import { CollaborativeEditorContext } from "../CollaborativeEditorProvider";
+import { VisualEditorContext } from "../VisualEditorProvider";
import useLinkShortcut from "./useLinkShortcut";
import { uniqueId } from "remirror";
@@ -24,9 +24,7 @@ export const URL_PATTERN =
function useFloatingLinkState() {
const { t } = useTranslation();
- const { editorSources, setEditorSources } = useContext(
- CollaborativeEditorContext
- );
+ const { editorSources, setEditorSources } = useContext(VisualEditorContext);
const [error, setError] = useState(null);
const chain = useChainedCommands();
diff --git a/src/components/Collaborative/utils/getEditorConfig.tsx b/src/components/Collaborative/utils/getEditorConfig.tsx
new file mode 100644
index 000000000..eebbdc2bb
--- /dev/null
+++ b/src/components/Collaborative/utils/getEditorConfig.tsx
@@ -0,0 +1,68 @@
+import { ReviewTaskTypeEnum } from "../../../machines/reviewTask/enums";
+import SummaryExtension from "../Form/SummaryExtesion";
+import QuestionExtension from "../Form/QuestionExtension";
+import ReportExtension from "../Form/ReportExtension";
+import VerificationExtension from "../Form/VerificationExtension";
+import {
+ AnnotationExtension,
+ LinkExtension,
+ YjsExtension,
+ TrailingNodeExtension,
+} from "remirror/extensions";
+import SourceReviewEditor from "../Components/SourceReviewEditor";
+import ClaimReviewEditor from "../Components/ClaimReviewEditor";
+import { MarkExtension, NodeExtension, PlainExtension } from "remirror";
+
+export class EditorConfig {
+ getExtensions(
+ type: string,
+ websocketProvider: any,
+ enableEditorAnnotations: boolean
+ ): Partial[] {
+ const baseExtensions: Partial[] = [
+ new SummaryExtension({ disableExtraAttributes: true }),
+ new TrailingNodeExtension(),
+ ];
+
+ if (websocketProvider) {
+ baseExtensions.push(
+ new YjsExtension({ getProvider: () => websocketProvider })
+ );
+ }
+
+ if (enableEditorAnnotations) {
+ // TODO: Make annotations shared across documents
+ baseExtensions.push(new AnnotationExtension());
+ }
+
+ switch (type) {
+ case ReviewTaskTypeEnum.Source:
+ return baseExtensions;
+ case ReviewTaskTypeEnum.Claim:
+ return [
+ ...baseExtensions,
+ new LinkExtension({
+ extraAttributes: { id: () => null },
+ autoLink: true,
+ selectTextOnClick: true,
+ }),
+ new QuestionExtension({ disableExtraAttributes: true }),
+ new ReportExtension({ disableExtraAttributes: true }),
+ new VerificationExtension({ disableExtraAttributes: true }),
+ ];
+ default:
+ return [];
+ }
+ }
+
+ getComponents(type: string, props: any): JSX.Element {
+ switch (type) {
+ case ReviewTaskTypeEnum.Source:
+ return ;
+ case ReviewTaskTypeEnum.Claim:
+ return ;
+ default:
+ return <>>;
+ }
+ }
+}
diff --git a/src/components/Collaborative/utils/getEditorSources.ts b/src/components/Collaborative/utils/getEditorSources.ts
deleted file mode 100644
index 1c66ab7cd..000000000
--- a/src/components/Collaborative/utils/getEditorSources.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import md5 from "md5";
-
-interface MarksProps {
- type: string;
- attrs: {
- href: string;
- target?: string;
- auto: boolean;
- };
-}
-
-interface ContentTextProps {
- type: string;
- marks: MarksProps[];
- text: string;
-}
-
-interface ContentParagraphsProps {
- content: ContentTextProps[];
- type: string;
-}
-
-export default function getEditorSources(content: ContentParagraphsProps[]) {
- let sourceReference = 0;
- return content
- .flatMap((paragraph) => {
- return paragraph?.content
- ?.filter((content) => content.marks)
- .map((content) => {
- sourceReference += 1;
- return {
- targetText: content.text,
- ref: md5(`$${content.text}${sourceReference}`),
- href: content.marks[0].attrs.href,
- };
- });
- })
- .filter((source) => source);
-}
diff --git a/src/components/Collaborative/utils/replaceHrefReferenceWithHash.ts b/src/components/Collaborative/utils/replaceHrefReferenceWithHash.ts
deleted file mode 100644
index e597f37c5..000000000
--- a/src/components/Collaborative/utils/replaceHrefReferenceWithHash.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import md5 from "md5";
-
-export default function replaceHrefReferenceWithHash(html: string) {
- const regex = /]*>([^<]*)<\/a>/g;
- let sourceReference = 1;
- const replaceHrefReference = (match, text) => {
- const linkHashReference = md5(`$${text}${sourceReference}`);
- const modifiedTag = `${text}${sourceReference}`;
- sourceReference += 1;
- return modifiedTag;
- };
-
- return html.replace(regex, replaceHrefReference);
-}
diff --git a/src/components/ContentWrapper.tsx b/src/components/ContentWrapper.tsx
index 5706eff51..c6232a6c4 100644
--- a/src/components/ContentWrapper.tsx
+++ b/src/components/ContentWrapper.tsx
@@ -26,31 +26,23 @@ const ContentWrapper = ({ children }) => {
const router = useRouter();
// TODO: we can remove this when we have desktop layout for all the pages
- const desktopReadyPages = [
- "claim-review",
- "home",
- "kanban-page",
- "personality-page",
- "claim-create",
- "sources-create",
- "claim",
- "debate-editor",
- "debate-page",
- "sign-up",
- "login",
- "admin-page",
- "admin-badges",
- "admin-namespaces",
- "profile",
- "supportive-materials",
- "search-page",
+ const desktopUnReadyPages = [
+ "404-page",
+ "about-page",
+ "access-denied-page",
+ "code-of-conduct",
+ "privacy-policy",
+ "personality-list",
+ "personality-create-search",
+ "claim-sources-page",
+ "history-page",
];
- const layout = desktopReadyPages.some((page) =>
+ const layout = desktopUnReadyPages.some((page) =>
router.pathname.includes(page)
)
- ? "desktop"
- : "mobile";
+ ? "mobile"
+ : "desktop";
return (
diff --git a/src/components/Copilot/CopilotConversation.tsx b/src/components/Copilot/CopilotConversation.tsx
index bc0f758ed..a9449442e 100644
--- a/src/components/Copilot/CopilotConversation.tsx
+++ b/src/components/Copilot/CopilotConversation.tsx
@@ -1,15 +1,14 @@
-import React, { useContext, useEffect, useRef, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
import CopilotConversationCard from "./CopilotConversationCard";
import CopilotConversationLoading from "./CopilotConversationLoading";
import { SenderEnum } from "../../types/enums";
import CopilotConversationSuggestions from "./CopilotConversationSuggestions";
import AletheiaButton from "../Button";
-import { CollaborativeEditorContext } from "../Collaborative/CollaborativeEditorProvider";
-import { RemirrorContentType } from "remirror";
import { useTranslation } from "next-i18next";
import CopilotFeedback from "./CopilotFeedback";
const CopilotConversation = ({
+ manager,
handleSendMessage,
isLoading,
messages,
@@ -17,9 +16,6 @@ const CopilotConversation = ({
}) => {
const { t } = useTranslation();
const CopilotConversationRef = useRef(null);
- const { setEditorContentObject, setShouldInsertAIReport } = useContext(
- CollaborativeEditorContext
- );
const [showButtons, setShowButtons] = useState({
ADD_REPORT: true,
ADD_RATE: false,
@@ -46,8 +42,9 @@ const CopilotConversation = ({
}, [messages, showButtons]);
const handleAddReportClick = () => {
- setShouldInsertAIReport(true);
- setEditorContentObject(editorReport as RemirrorContentType);
+ manager.view.updateState(
+ manager.createState({ content: { ...editorReport } })
+ );
setShowButtons({
ADD_REPORT: false,
ADD_RATE: true,
diff --git a/src/components/Copilot/CopilotDrawer.style.tsx b/src/components/Copilot/CopilotDrawer.style.tsx
index 10c6bae7d..9b84cc001 100644
--- a/src/components/Copilot/CopilotDrawer.style.tsx
+++ b/src/components/Copilot/CopilotDrawer.style.tsx
@@ -8,6 +8,7 @@ const CopilotDrawerStyled = styled(Drawer)`
zindex: 999999;
max-height: 100vh;
overflow: hidden;
+ position: absolute;
& .MuiDrawer-paper {
width: ${(props) => props.width};
diff --git a/src/components/Copilot/CopilotDrawer.tsx b/src/components/Copilot/CopilotDrawer.tsx
index 639deff61..2dba3afd4 100644
--- a/src/components/Copilot/CopilotDrawer.tsx
+++ b/src/components/Copilot/CopilotDrawer.tsx
@@ -11,6 +11,8 @@ import { Report } from "../../types/Report";
import { ChatMessage, ChatResponse, MessageContext } from "../../types/Copilot";
import { calculatePosition } from "./utils/calculatePositions";
import Loading from "../Loading";
+import { AnyExtension, RemirrorManager } from "remirror";
+import { ReactExtensions } from "@remirror/react";
const CopilotConversation = React.lazy(() => import("./CopilotConversation"));
interface Size {
@@ -21,9 +23,10 @@ interface Size {
interface CopilotDrawerProps {
claim: Claim;
sentence: string;
+ manager: RemirrorManager>;
}
-const CopilotDrawer = ({ claim, sentence }: CopilotDrawerProps) => {
+const CopilotDrawer = ({ manager, claim, sentence }: CopilotDrawerProps) => {
const { t } = useTranslation();
const { vw, copilotDrawerCollapsed } = useAppSelector((state) => ({
vw: state?.vw,
@@ -50,10 +53,10 @@ const CopilotDrawer = ({ claim, sentence }: CopilotDrawerProps) => {
const context: MessageContext = useMemo(
() => ({
- claimDate: claim.date,
+ claimDate: claim?.date,
sentence: sentence,
personalityName: claim?.personalities[0]?.name || null,
- claimTitle: claim.title,
+ claimTitle: claim?.title,
}),
[claim, sentence]
);
@@ -117,6 +120,7 @@ const CopilotDrawer = ({ claim, sentence }: CopilotDrawerProps) => {
>
}>
{
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t("personalityCTA:header")}
+
+ {children}
+ {t("personalityCTA:footer")}
+
+ );
+};
+
+export default CreateCTAButton;
diff --git a/src/components/Dashboard/DashboardView.tsx b/src/components/Dashboard/DashboardView.tsx
index cdb3f5658..fdb7581b6 100644
--- a/src/components/Dashboard/DashboardView.tsx
+++ b/src/components/Dashboard/DashboardView.tsx
@@ -9,9 +9,8 @@ import ClaimSkeleton from "../Skeleton/ClaimSkeleton";
import claimApi from "../../api/claim";
import ClaimCard from "../Claim/ClaimCard";
import claimReviewApi from "../../api/claimReviewApi";
-import CardBase from "../CardBase";
-import ReviewCardComment from "../ClaimReview/ReviewCardComment";
import DashboardViewStyle from "./DashboardView.style";
+import ReviewCard from "../ClaimReview/ReviewCard";
const DashboardView = () => {
const { t, i18n } = useTranslation();
@@ -70,14 +69,11 @@ const DashboardView = () => {
emptyFallback={<>>}
renderItem={(review) =>
review && (
-
-
-
+
)
}
skeleton={}
diff --git a/src/components/Debate/DebateView.tsx b/src/components/Debate/DebateView.tsx
index 1c0ba3484..78e457867 100644
--- a/src/components/Debate/DebateView.tsx
+++ b/src/components/Debate/DebateView.tsx
@@ -16,7 +16,7 @@ const DebateView = ({ claim }) => {
const speeches = debate.content;
const dispatch = useDispatch();
const [nameSpace] = useAtom(currentNameSpace);
- dispatch(actions.setSelectClaim(claim));
+ dispatch(actions.setSelectTarget(claim));
// the new debate data will in the callbackResult of the state
const updateTimeline = useCallback(() => {
diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx
index 29ee5f0ad..c2e69d341 100644
--- a/src/components/Editor/Editor.tsx
+++ b/src/components/Editor/Editor.tsx
@@ -36,8 +36,8 @@ const Editor = ({ claim, sitekey }: IEditorProps) => {
});
dispatch({
- type: ActionTypes.SET_SELECTED_CLAIM,
- selectedClaim: claim,
+ type: ActionTypes.SET_SELECTED_TARGET,
+ selectedTarget: claim,
});
dispatch({
type: ActionTypes.SET_SITEKEY,
diff --git a/src/components/Form/DynamicForm.tsx b/src/components/Form/DynamicForm.tsx
index a980722b9..c35bfe8b1 100644
--- a/src/components/Form/DynamicForm.tsx
+++ b/src/components/Form/DynamicForm.tsx
@@ -7,7 +7,7 @@ import Text from "antd/lib/typography/Text";
import colors from "../../styles/colors";
import { useTranslation } from "next-i18next";
-const DynamicForm = ({ currentForm, machineValues, control, errors }) => {
+const DynamicForm = ({ currentForm, control, errors, machineValues = {} }) => {
const { t } = useTranslation();
return (
diff --git a/src/components/Form/DynamicInput.tsx b/src/components/Form/DynamicInput.tsx
index e5001c3eb..5d9df4450 100644
--- a/src/components/Form/DynamicInput.tsx
+++ b/src/components/Form/DynamicInput.tsx
@@ -4,13 +4,13 @@ import ClaimReviewSelect from "./ClaimReviewSelect";
import InputTextList from "../InputTextList";
import Loading from "../Loading";
import TextArea from "../TextArea";
-import UserInput from "./UserInput";
+import FetchInput from "./FetchInput";
import { useTranslation } from "next-i18next";
-import { CollaborativeEditorContext } from "../Collaborative/CollaborativeEditorProvider";
+import { VisualEditorContext } from "../Collaborative/VisualEditorProvider";
+import AletheiaInput from "../AletheiaInput";
+import { Checkbox } from "antd";
-const CollaborativeEditor = lazy(
- () => import("../Collaborative/CollaborativeEditor")
-);
+const VisualEditor = lazy(() => import("../Collaborative/VisualEditor"));
interface DynamicInputProps {
fieldName: string;
@@ -25,7 +25,7 @@ interface DynamicInputProps {
}
const DynamicInput = (props: DynamicInputProps) => {
- const { isFetchingEditor } = useContext(CollaborativeEditorContext);
+ const { isFetchingEditor } = useContext(VisualEditorContext);
const { t } = useTranslation();
switch (props.type) {
@@ -42,7 +42,7 @@ const DynamicInput = (props: DynamicInputProps) => {
);
case "inputSearch":
return (
-
{
preloadedOptions={props.extraProps.preloadedOptions}
/>
);
+ case "text":
+ return (
+ props.onChange(value)}
+ defaultValue={props.defaultValue}
+ data-cy={props["data-cy"]}
+ white="true"
+ />
+ );
case "textList":
return (
{
placeholder={t(props.placeholder)}
/>
);
- case "collaborative":
+ case "textbox":
+ return (
+ props.onChange(value)}
+ checked={!!props.value}
+ >
+ {t(`claimReviewForm:${props.fieldName}`)}
+
+ );
+ case "visualEditor":
if (isFetchingEditor) {
return ;
} else {
return (
}>
- props.onChange(doc)}
+ {
+ doc.attrs = { reviewTaskType };
+ props.onChange(doc);
+ }}
/>
);
diff --git a/src/components/Form/UserInput.tsx b/src/components/Form/FetchInput.tsx
similarity index 67%
rename from src/components/Form/UserInput.tsx
rename to src/components/Form/FetchInput.tsx
index 50e62ca78..31e5cf902 100644
--- a/src/components/Form/UserInput.tsx
+++ b/src/components/Form/FetchInput.tsx
@@ -1,8 +1,9 @@
import React, { useEffect, useState } from "react";
import SelectOptions from "./SelectOptions";
import userApi from "../../api/userApi";
+import verificationRequestApi from "../../api/verificationRequestApi";
-interface UserInputProps {
+interface FetchInputProps {
fieldName: string;
placeholder: string;
onChange: any;
@@ -14,7 +15,7 @@ interface UserInputProps {
preloadedOptions?: string[];
}
-const UserInput = ({
+const FetchInput = ({
fieldName,
placeholder,
onChange,
@@ -24,23 +25,27 @@ const UserInput = ({
style = {},
value = null,
preloadedOptions = [],
-}: UserInputProps) => {
+}: FetchInputProps) => {
const [treatedValue, setTreatedValue] = useState([]);
const [isLoading, setIsLoading] = useState(false);
+ const userFields = ["usersId", "crossCheckerId", "reviewerId"];
+ const apiFunction = userFields.includes(fieldName)
+ ? userApi.getById
+ : verificationRequestApi.getById;
useEffect(() => {
- const fetchUserNames = async () => {
+ const fetchSelectedContent = async () => {
if (
Array.isArray(value) ? value.length > 0 : value !== "" && value
) {
try {
setIsLoading(true);
- const userPromises = Array.isArray(value)
- ? value.map((id) => userApi.getById(id))
- : [userApi.getById(value)];
- const users = await Promise.all(userPromises);
- const treatedValues = users.map((user) => ({
- label: user?.name,
+ const Promises = Array.isArray(value)
+ ? value.map((id) => apiFunction(id))
+ : [apiFunction(value)];
+ const results = await Promise.all(Promises);
+ const treatedValues = results.map((user) => ({
+ label: user?.name || user?.content,
value: user?._id,
}));
setTreatedValue(treatedValues);
@@ -54,7 +59,7 @@ const UserInput = ({
}
};
- fetchUserNames().catch((error) => {
+ fetchSelectedContent().catch((error) => {
console.error(error);
setIsLoading(false);
});
@@ -76,4 +81,4 @@ const UserInput = ({
);
};
-export default UserInput;
+export default FetchInput;
diff --git a/src/components/Form/FormField.ts b/src/components/Form/FormField.ts
index 7a0714c90..342d6c634 100644
--- a/src/components/Form/FormField.ts
+++ b/src/components/Form/FormField.ts
@@ -1,8 +1,9 @@
import { Node } from "@remirror/pm/model";
import { RegisterOptions } from "react-hook-form";
import { EditorParser } from "../../../lib/editor-parser";
-import { ReviewTaskMachineContextReviewData } from "../../../server/claim-review-task/dto/create-claim-review-task.dto";
+import { ReviewTaskMachineContextReviewData } from "../../../server/review-task/dto/create-review-task.dto";
import { Roles } from "../../types/enums";
+import { URL_PATTERN } from "../Collaborative/hooks/useFloatingLinkState";
export type FormField = {
fieldName: string;
@@ -57,7 +58,7 @@ const createFormField = (props: CreateFormFieldProps): FormField => {
rules: {
required: required && "common:requiredFieldError",
...rules,
- validate: {
+ validate: required && {
notBlank: (v) =>
validateBlank(v) || "common:requiredFieldError",
...rules?.validate,
@@ -81,6 +82,9 @@ const validateSchema = (
if (!Array.isArray(value) && !value.trim() && key !== "paragraph") {
return `common:${key}RequiredFieldError`;
}
+ if (key === "source" && !URL_PATTERN.test(schema[key])) {
+ return `sourceForm:errorMessageValidURL`;
+ }
}
return true;
};
diff --git a/src/components/GridList.tsx b/src/components/GridList.tsx
index 351860e9a..dad061c7f 100644
--- a/src/components/GridList.tsx
+++ b/src/components/GridList.tsx
@@ -5,21 +5,19 @@ import Button, { ButtonType } from "./Button";
import { ArrowRightOutlined } from "@ant-design/icons";
import { isUserLoggedIn } from "../atoms/currentUser";
import { useAtom } from "jotai";
-import { currentNameSpace } from "../atoms/namespace";
-import { NameSpaceEnum } from "../types/Namespace";
const GridList = ({
renderItem,
loggedInMaxColumns = 3,
dataSource,
title,
+ href = "",
+ dataCy = "",
seeMoreButtonLabel = "",
disableSeeMoreButton = false,
gridLayout = {},
}) => {
const [isLoggedIn] = useAtom(isUserLoggedIn);
- const [nameSpace] = useAtom(currentNameSpace);
-
const overrideGridLayout = isLoggedIn
? {
xl: loggedInMaxColumns,
@@ -75,15 +73,7 @@ const GridList = ({
margin: "48px 0 64px 0",
}}
>
-
);
};
diff --git a/src/components/Kanban/KanbanCard.tsx b/src/components/Kanban/KanbanCard.tsx
index 92a0a74fc..2d36ecc68 100644
--- a/src/components/Kanban/KanbanCard.tsx
+++ b/src/components/Kanban/KanbanCard.tsx
@@ -13,24 +13,41 @@ import PhotoOutlinedIcon from "@mui/icons-material/PhotoOutlined";
import ArticleOutlinedIcon from "@mui/icons-material/ArticleOutlined";
import { useAtom } from "jotai";
import { currentNameSpace } from "../../atoms/namespace";
+import SourceApi from "../../api/sourceApi";
+import { ReviewTaskTypeEnum } from "../../machines/reviewTask/enums";
+import verificationRequestApi from "../../api/verificationRequestApi";
const { Text, Paragraph } = Typography;
-const KanbanCard = ({ reviewTask }) => {
+const KanbanCard = ({ reviewTask, reviewTaskType }) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const [nameSpace] = useAtom(currentNameSpace);
+ const apiCallFunctions = {
+ [ReviewTaskTypeEnum.Claim]: claimApi.getById,
+ [ReviewTaskTypeEnum.Source]: SourceApi.getById,
+ [ReviewTaskTypeEnum.VerificationRequest]:
+ verificationRequestApi.getById,
+ };
+
const goToClaimReview = () => {
- dispatch(actions.setSelectClaim(null));
+ dispatch(actions.setSelectTarget(null));
dispatch(actions.setSelectPersonality(null));
dispatch(actions.setSelectContent(null));
Promise.all([
- claimApi.getById(reviewTask.claimId, t, { nameSpace }),
+ apiCallFunctions[reviewTaskType](reviewTask.targetId, t, {
+ nameSpace,
+ }),
personalityApy.getPersonality(reviewTask.personalityId, {}, t),
- ]).then(([claim, personality]) => {
- dispatch(actions.setSelectClaim(claim));
+ ]).then(([target, personality]) => {
+ dispatch(actions.setSelectTarget(target));
dispatch(actions.setSelectPersonality(personality));
- dispatch(actions.setSelectContent(reviewTask?.content));
+ dispatch(
+ actions.setSelectContent({
+ ...reviewTask?.content,
+ reviewTaskType,
+ })
+ );
});
dispatch(actions.openReviewDrawer());
};
@@ -73,7 +90,7 @@ const KanbanCard = ({ reviewTask }) => {
fontWeight: "bold",
}}
>
- {title}
+ {title || reviewTask.content.href}
{reviewTask.personalityName}
diff --git a/src/components/Kanban/KanbanCol.tsx b/src/components/Kanban/KanbanCol.tsx
index 13aa454a8..f7526ca5f 100644
--- a/src/components/Kanban/KanbanCol.tsx
+++ b/src/components/Kanban/KanbanCol.tsx
@@ -1,7 +1,7 @@
import { useTranslation } from "next-i18next";
import React from "react";
-import ClaimReviewTaskApi from "../../api/ClaimReviewTaskApi";
+import ReviewTaskApi from "../../api/reviewTaskApi";
import { ReviewTaskStates } from "../../machines/reviewTask/enums";
import KanbanSkeleton from "../Skeleton/KanbanSkeleton";
import colors from "../../styles/colors";
@@ -11,6 +11,11 @@ import KanbanCard from "./KanbanCard";
import styled from "styled-components";
const StyledColumn = styled.div`
+ padding: 0 10px;
+ width: 400px;
+ background-color: ${colors.lightGraySecondary};
+ border-radius: 4px;
+
.ant-list-item {
padding: 6px 0;
}
@@ -19,6 +24,7 @@ const StyledColumn = styled.div`
interface KanbanColProps {
nameSpace: string;
state: ReviewTaskStates;
+ reviewTaskType: string;
filterUser: {
assigned: boolean;
crossChecked: boolean;
@@ -26,29 +32,33 @@ interface KanbanColProps {
};
}
-const KanbanCol = ({ nameSpace, state, filterUser }: KanbanColProps) => {
+const KanbanCol = ({
+ nameSpace,
+ state,
+ filterUser,
+ reviewTaskType,
+}: KanbanColProps) => {
const { t } = useTranslation();
return (
-
+
}
+ renderItem={(task) => (
+
+ )}
emptyFallback={
-
+
}
showDividers={false}
skeleton={}
diff --git a/src/components/Kanban/KanbanTabNavigator.tsx b/src/components/Kanban/KanbanTabNavigator.tsx
new file mode 100644
index 000000000..4fd476097
--- /dev/null
+++ b/src/components/Kanban/KanbanTabNavigator.tsx
@@ -0,0 +1,61 @@
+import React from "react";
+import Tab from "@mui/material/Tab";
+import { Grid } from "@mui/material";
+import ReportIcon from "@mui/icons-material/Report";
+import { useTranslation } from "next-i18next";
+import TabsNavigatorStyle from "../adminArea/TabsNavigator.style";
+import SourceIcon from "@mui/icons-material/Source";
+import { FileAddFilled } from "@ant-design/icons";
+
+const KanbanTabNavigator = ({ value, handleChange }) => {
+ const { t } = useTranslation();
+
+ function tabProps(index: number) {
+ return {
+ id: `simple-tab-${index}`,
+ "aria-controls": `simple-tabpanel-${index}`,
+ };
+ }
+
+ return (
+
+
+
+
+ {t("kanban:tabClaimTitle")}
+
+ }
+ {...tabProps(0)}
+ />
+
+
+ {t("kanban:tabSourceTitle")}
+
+ }
+ {...tabProps(1)}
+ />
+
+
+
+ {t("kanban:tabVerificationRequestTitle")}
+
+
+ }
+ {...tabProps(2)}
+ />
+
+
+ );
+};
+
+export default KanbanTabNavigator;
diff --git a/src/components/Kanban/KanbanToolbar.tsx b/src/components/Kanban/KanbanToolbar.tsx
new file mode 100644
index 000000000..d6a121796
--- /dev/null
+++ b/src/components/Kanban/KanbanToolbar.tsx
@@ -0,0 +1,42 @@
+import { Col } from "antd";
+import React from "react";
+import { useTranslation } from "next-i18next";
+
+import { FormControlLabel, Switch } from "@mui/material";
+
+const KanbanToolbar = ({ filterUserTasks, setFilterUserTasks }) => {
+ const { t } = useTranslation();
+
+ const handleChange = (selectedTask) => {
+ setFilterUserTasks((prev) => ({
+ ...Object.keys(prev).reduce((acc, key) => {
+ acc[key] = false;
+ return acc;
+ }, {}),
+ [selectedTask]: !prev[selectedTask],
+ }));
+ };
+
+ return (
+
+ {Object.keys(filterUserTasks).map((task) => (
+ handleChange(task)}
+ />
+ }
+ label={t(
+ `kanban:my${
+ task.charAt(0).toUpperCase() + task.slice(1)
+ }`
+ )}
+ />
+ ))}
+
+ );
+};
+
+export default KanbanToolbar;
diff --git a/src/components/Kanban/KanbanView.style.tsx b/src/components/Kanban/KanbanView.style.tsx
new file mode 100644
index 000000000..485867fd1
--- /dev/null
+++ b/src/components/Kanban/KanbanView.style.tsx
@@ -0,0 +1,26 @@
+import styled from "styled-components";
+
+const KanbanViewStyled = styled.div`
+ display: flex;
+ padding: 16px 0 32px 0;
+ height: 100%;
+ align-items: center;
+ flex-direction: column;
+ gap: 8px;
+
+ .kanban-toolbar {
+ flex-wrap: nowrap;
+ width: 100%;
+ padding: 1vh 5vh 0vh 5vh;
+ }
+
+ .kanban-board {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ gap: 12px;
+ }
+`;
+
+export default KanbanViewStyled;
diff --git a/src/components/Kanban/KanbanView.tsx b/src/components/Kanban/KanbanView.tsx
index a1cd90c2a..b3c2e04aa 100644
--- a/src/components/Kanban/KanbanView.tsx
+++ b/src/components/Kanban/KanbanView.tsx
@@ -1,110 +1,53 @@
-import { Col, Row } from "antd";
-import React, { useState } from "react";
-import { useTranslation } from "next-i18next";
+import { Col } from "antd";
+import React, { useMemo, useState } from "react";
-import { ReviewTaskStates } from "../../machines/reviewTask/enums";
+import {
+ KanbanClaimState,
+ KanbanSourceState,
+ KanbanVerificationRequestStates,
+ ReviewTaskStates,
+ ReviewTaskTypeEnum,
+} from "../../machines/reviewTask/enums";
import KanbanCol from "./KanbanCol";
-import { FormControlLabel, Switch } from "@mui/material";
import { useAtom } from "jotai";
import { currentNameSpace } from "../../atoms/namespace";
+import KanbanViewStyled from "./KanbanView.style";
+import KanbanToolbar from "./KanbanToolbar";
-const KanbanView = () => {
- const { t } = useTranslation();
+const KanbanView = ({ reviewTaskType }) => {
const [nameSpace] = useAtom(currentNameSpace);
- // Don't show unassigned, rejected, selectCrossChecker and selectReviewer column
- // because we don't save tasks in these states
- const states = Object.keys(ReviewTaskStates).filter(
- (state) =>
- state !== ReviewTaskStates.unassigned &&
- state !== ReviewTaskStates.selectCrossChecker &&
- state !== ReviewTaskStates.selectReviewer &&
- state !== ReviewTaskStates.addCommentCrossChecking &&
- state !== ReviewTaskStates.rejected
- );
const [filterUserTasks, setFilterUserTasks] = useState({
assigned: false,
crossChecked: false,
reviewed: false,
});
+ const kanbanStates = {
+ [ReviewTaskTypeEnum.Claim]: KanbanClaimState,
+ [ReviewTaskTypeEnum.Source]: KanbanSourceState,
+ [ReviewTaskTypeEnum.VerificationRequest]:
+ KanbanVerificationRequestStates,
+ };
+
+ const states = Object.keys(kanbanStates[reviewTaskType]);
return (
-
-
-
- setFilterUserTasks((prev) => ({
- reviewed: false,
- crossChecked: false,
- assigned: !prev.assigned,
- }))
- }
- />
- }
- label={t("kanban:myTasks")}
- />
-
- setFilterUserTasks((prev) => ({
- assigned: false,
- reviewed: false,
- crossChecked: !prev.crossChecked,
- }))
- }
- />
- }
- label={t("kanban:myCrossChecks")}
- />
-
- setFilterUserTasks((prev) => ({
- assigned: false,
- crossChecked: false,
- reviewed: !prev.reviewed,
- }))
- }
- />
- }
- label={t("kanban:myReviews")}
- />
-
-
- {states.map((state) => {
- return (
-
- );
- })}
+
+
+
+ {states.map((state) => (
+
+ ))}
-
+
);
};
diff --git a/src/components/List/BaseList.tsx b/src/components/List/BaseList.tsx
index 1e54238dd..9e65f98fc 100644
--- a/src/components/List/BaseList.tsx
+++ b/src/components/List/BaseList.tsx
@@ -57,13 +57,14 @@ const BaseList = ({
pageSize: 10,
fetchOnly: true,
order: sortByOrder,
- ...filter,
});
// TODO: use TimerCallback to refresh the list
useEffect(() => {
- apiCall(query).then((newItems) => {
+ setLoading(true);
+ setExecLoadMore(false);
+ apiCall({ ...query, ...filter }).then((newItems) => {
setInitLoading(false);
setLoading(false);
setTotalPages(newItems.totalPages);
@@ -72,17 +73,7 @@ const BaseList = ({
execLoadMore ? [...items, ...newItems.data] : newItems.data
);
});
- }, [query]);
-
- useEffect(() => {
- setLoading(true);
- setExecLoadMore(false);
- setQuery({
- ...query,
- ...filter,
- page: 1,
- });
- }, [filter]);
+ }, [query, filter]);
const loadMoreData = () => {
if (execLoadMore !== true) {
diff --git a/src/components/LoginButton.tsx b/src/components/LoginButton.tsx
new file mode 100644
index 000000000..46c4a1cb4
--- /dev/null
+++ b/src/components/LoginButton.tsx
@@ -0,0 +1,22 @@
+import { Col } from "antd";
+import React from "react";
+import { useTranslation } from "next-i18next";
+import Button from "./Button";
+
+const LoginButton = () => {
+ const { t } = useTranslation();
+
+ return (
+
+ {t("claimReviewForm:loginButton")}
+
+ );
+};
+
+export default LoginButton;
diff --git a/src/components/Metrics/MetricsOverview.tsx b/src/components/Metrics/MetricsOverview.tsx
index 75eeb4c68..9841f5ea8 100644
--- a/src/components/Metrics/MetricsOverview.tsx
+++ b/src/components/Metrics/MetricsOverview.tsx
@@ -17,7 +17,7 @@ const MetricsOverview = ({ stats }) => {
offset={2}
span={18}
>
- {stats?.reviews && stats?.reviews.lenght && (
+ {stats?.reviews && stats?.reviews.length && (
{
const { t } = useTranslation();
+ const [nameSpace] = useAtom(currentNameSpace);
+ const href =
+ nameSpace !== NameSpaceEnum.Main
+ ? `/${nameSpace}/personality`
+ : "/personality";
+
return (
(
diff --git a/src/components/Personality/PersonalityCreateCTA.tsx b/src/components/Personality/PersonalityCreateCTA.tsx
index 1dd654895..76bdfa6e4 100644
--- a/src/components/Personality/PersonalityCreateCTA.tsx
+++ b/src/components/Personality/PersonalityCreateCTA.tsx
@@ -2,25 +2,22 @@ import React from "react";
import { useTranslation } from "next-i18next";
import Button, { ButtonType } from "../Button";
import { PlusOutlined } from "@ant-design/icons";
+import CreateCTAButton from "../CreateCTAButton";
const PersonalityCreateCTA = ({ href }) => {
const { t } = useTranslation();
return (
- <>
-
- {t("personalityCTA:header")}
-
-
-
-
- {t("personalityCTA:button")}
-
-
- {t("personalityCTA:footer")}
- >
+
+
+ {t("personalityCTA:button")}
+
+
);
-
-}
+};
export default PersonalityCreateCTA;
diff --git a/src/components/Personality/PersonalityMinimalCard.style.tsx b/src/components/Personality/PersonalityMinimalCard.style.tsx
index d4c5260b2..f9e2e2cb0 100644
--- a/src/components/Personality/PersonalityMinimalCard.style.tsx
+++ b/src/components/Personality/PersonalityMinimalCard.style.tsx
@@ -4,26 +4,26 @@ import colors from "../../styles/colors";
import { queries } from "../../styles/mediaQueries";
const PersonalityMinimalCardStyle = styled(Row)`
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+
.personality {
text-align: center;
width: 150px;
}
.personality-name {
font-size: 16px;
- margin-top: 16px;
+ margin-top: 8px;
color: ${colors.bluePrimary};
font-weight: 400;
}
- .personality-card {
- text-align: center;
- max-width: 167px;
- justify-content: center;
- }
-
.personality-description-content {
- font-size: 10px;
+ font-size: 12px;
color: ${colors.blackSecondary};
+ margin: 0;
}
.personality-description {
@@ -37,17 +37,14 @@ const PersonalityMinimalCardStyle = styled(Row)`
}
@media ${queries.sm} {
- .personality-card {
- max-width: 100%;
- justify-content: start;
- align-items: start;
- padding-bottom: 10px;
- }
+ max-width: 100%;
+ justify-content: start;
+ align-items: center;
+ padding-bottom: 10px;
.personality {
- text-align: left;
- padding-left: 15px;
- width: calc(100% - 130px);
+ gap: 32px;
+ justify-content: flex-start;
}
.personality .ant-col {
@@ -55,7 +52,6 @@ const PersonalityMinimalCardStyle = styled(Row)`
}
.personality-name {
- font-size: 26px;
margin-top: 0;
}
@@ -63,10 +59,9 @@ const PersonalityMinimalCardStyle = styled(Row)`
text-align: center;
flex-wrap: wrap;
width: 100%;
- margin-top: 16px;
- font-size: 14px;
+ margin-top: 8px;
display: flex;
- justify-content: space-between;
+ justify-content: center;
}
.personality-description {
@@ -75,14 +70,12 @@ const PersonalityMinimalCardStyle = styled(Row)`
}
@media ${queries.xs} {
- .personality-card {
- justify-content: space-around;
- align-items: center;
- }
+ justify-content: space-around;
+ align-items: center;
.personality-description-content {
flex-direction: column;
- align-items: flex-start;
+ align-items: center;
}
}
`;
diff --git a/src/components/Personality/PersonalityMinimalCard.tsx b/src/components/Personality/PersonalityMinimalCard.tsx
index d5b157e11..f8c5b21aa 100644
--- a/src/components/Personality/PersonalityMinimalCard.tsx
+++ b/src/components/Personality/PersonalityMinimalCard.tsx
@@ -8,21 +8,18 @@ import { useAtom } from "jotai";
import { currentNameSpace } from "../../atoms/namespace";
import { NameSpaceEnum } from "../../types/Namespace";
-const PersonalityMinimalCard = ({ personality }) => {
+const PersonalityMinimalCard = ({ personality, avatarSize = 117 }) => {
const { t } = useTranslation();
const [nameSpace] = useAtom(currentNameSpace);
return (
-
+
{
{sentence?.props?.classification && (
<>
{", "}
- {t("claimReview:claimReview")}
-
-
-
+
>
)}
diff --git a/src/components/Sentence/SentenceCard.tsx b/src/components/Sentence/SentenceCard.tsx
deleted file mode 100644
index afa0e9919..000000000
--- a/src/components/Sentence/SentenceCard.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from "react";
-import ClaimCardHeader from "../Claim/ClaimCardHeader";
-import ClaimReviewCardContent from "../ClaimReview/ReviewCardCommentContent";
-import TagsList from "../ClaimReview/TagsList";
-import ClaimReviewCardActions from "../ClaimReview/ReviewCardActions";
-import { ContentModelEnum } from "../../types/enums";
-import styled from "styled-components";
-import { Comment } from "antd";
-import CardBase from "../CardBase";
-import { useAtom } from "jotai";
-import { currentNameSpace } from "../../atoms/namespace";
-import { generateSentenceContentPath } from "../../utils/GetSentenceContentHref";
-
-const StyledComment = styled(Comment)`
- .ant-comment-actions > li {
- width: 100%;
- }
-`;
-
-const SentenceCard = ({ sentence }) => {
- const { content, claim, personality } = sentence;
- const claimItem =
- Array.isArray(claim) && claim.length > 0 ? claim[0] : claim;
- const personalityItem =
- Array.isArray(personality) && personality.length > 0
- ? personality[0]
- : personality;
- const contentModel = sentence?.claim[0]?.contentModel;
- const [nameSpace] = useAtom(currentNameSpace);
- const isImage = contentModel === ContentModelEnum.Image;
-
- return (
-
-
- }
- content={
-
- }
- actions={[
- ,
- ,
- ]}
- />
-
- );
-};
-
-export default SentenceCard;
diff --git a/src/components/SentenceReport/ClaimSummaryDisplay.tsx b/src/components/SentenceReport/ClaimSummaryDisplay.tsx
new file mode 100644
index 000000000..838c3b83b
--- /dev/null
+++ b/src/components/SentenceReport/ClaimSummaryDisplay.tsx
@@ -0,0 +1,97 @@
+import React from "react";
+import { ContentModelEnum } from "../../types/enums";
+import SentenceReportSummary from "./SentenceReportSummary";
+import { generateSentenceContentPath } from "../../utils/GetSentenceContentHref";
+import { Typography } from "antd";
+import ClaimInfo from "../Claim/ClaimInfo";
+import { useAtom } from "jotai";
+import { currentNameSpace } from "../../atoms/namespace";
+import { useTranslation } from "next-i18next";
+import ReviewContent from "../ClaimReview/ReviewContent";
+
+const { Paragraph } = Typography;
+
+const ClaimSummaryDisplay = ({
+ claim,
+ personality,
+ content,
+}: {
+ personality?: any;
+ claim: any;
+ content: any;
+}) => {
+ const { t } = useTranslation();
+ const isImage = claim?.contentModel === ContentModelEnum.Image;
+ const [nameSpace] = useAtom(currentNameSpace);
+
+ const contentProps = {
+ [ContentModelEnum.Speech]: {
+ linkText: "claim:cardLinkToFullText",
+ contentPath: generateSentenceContentPath(
+ nameSpace,
+ personality,
+ claim,
+ claim?.contentModel
+ ),
+ title: `"(...) ${content}"`,
+ speechTypeTranslation: "claim:typeSpeech",
+ },
+ [ContentModelEnum.Image]: {
+ linkText: "claim:cardLinkToImage",
+ contentPath: generateSentenceContentPath(
+ nameSpace,
+ personality,
+ claim,
+ claim?.contentModel
+ ),
+ title: claim?.title,
+ speechTypeTranslation: "",
+ },
+ [ContentModelEnum.Debate]: {
+ linkText: "claim:cardLinkToDebate",
+ contentPath: generateSentenceContentPath(
+ nameSpace,
+ personality,
+ claim,
+ claim?.contentModel
+ ),
+ title: `"(...) ${content}"`,
+ speechTypeTranslation: "claim:typeDebate",
+ },
+ [ContentModelEnum.Unattributed]: {
+ linkText: "claim:cardLinkToFullText",
+ contentPath: generateSentenceContentPath(
+ nameSpace,
+ personality,
+ claim,
+ claim?.contentModel
+ ),
+ title: `"(...) ${content}"`,
+ speechTypeTranslation: "claim:typeSpeech",
+ },
+ };
+
+ const { linkText, contentPath, title, speechTypeTranslation } =
+ contentProps[claim?.contentModel];
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default ClaimSummaryDisplay;
diff --git a/src/components/SentenceReport/SentenceReportCard.style.tsx b/src/components/SentenceReport/SentenceReportCard.style.tsx
index 586966a33..faee3111a 100644
--- a/src/components/SentenceReport/SentenceReportCard.style.tsx
+++ b/src/components/SentenceReport/SentenceReportCard.style.tsx
@@ -1,11 +1,10 @@
import styled from "styled-components";
import colors from "../../styles/colors";
import { queries } from "../../styles/mediaQueries";
+import { Row } from "antd";
-const SentenceReportCardStyle = styled.div`
- .main-content {
- padding-top: 32px;
- }
+const SentenceReportCardStyle = styled(Row)`
+ padding-top: 32px;
.sentence-card {
padding-left: 10px;
@@ -25,9 +24,7 @@ const SentenceReportCardStyle = styled.div`
}
@media ${queries.md} {
- .main-content {
- padding-top: 16px;
- }
+ padding-top: 16px;
}
@media ${queries.sm} {
diff --git a/src/components/SentenceReport/SentenceReportCard.tsx b/src/components/SentenceReport/SentenceReportCard.tsx
index 8a7cefa84..1affe5275 100644
--- a/src/components/SentenceReport/SentenceReportCard.tsx
+++ b/src/components/SentenceReport/SentenceReportCard.tsx
@@ -1,150 +1,89 @@
-import { Col, Row, Typography } from "antd";
+import { Col, Typography } from "antd";
import { useTranslation } from "next-i18next";
-import React from "react";
-import { ContentModelEnum } from "../../types/enums";
+import React, { useContext } from "react";
-import ClassificationText from "../ClassificationText";
-import ImageClaim from "../ImageClaim";
-import LocalizedDate from "../LocalizedDate";
+import ReviewClassification from "../ClaimReview/ReviewClassification";
import PersonalityMinimalCard from "../Personality/PersonalityMinimalCard";
import SentenceReportCardStyle from "./SentenceReportCard.style";
-import SentenceReportSummary from "./SentenceReportSummary";
import AletheiaAlert from "../AletheiaAlert";
-import { useAtom } from "jotai";
-import { currentNameSpace } from "../../atoms/namespace";
import { useAppSelector } from "../../store/store";
-import { generateSentenceContentPath } from "../../utils/GetSentenceContentHref";
+import { ReviewTaskTypeEnum } from "../../machines/reviewTask/enums";
+import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
+import ClaimSummaryDisplay from "./ClaimSummaryDisplay";
+import SourceSummaryDisplay from "./SourceSummaryDisplay";
+import VerificationRequestDisplay from "./VerificationRequestDisplay";
-const { Title, Paragraph } = Typography;
+const { Title } = Typography;
const SentenceReportCard = ({
- claim,
+ target,
personality,
classification,
content,
hideDescription,
}: {
personality?: any;
- claim: any;
+ target: any;
content: any;
classification?: any;
hideDescription?: string;
}) => {
const { t } = useTranslation();
- const isImage = claim?.contentModel === ContentModelEnum.Image;
- const [nameSpace] = useAtom(currentNameSpace);
- const { vw } = useAppSelector((state) => state);
-
- const contentProps = {
- [ContentModelEnum.Speech]: {
- linkText: "claim:cardLinkToFullText",
- contentPath: generateSentenceContentPath(
- nameSpace,
- personality,
- claim,
- claim?.contentModel
- ),
- title: `"(...) ${content.content}"`,
- speechTypeTranslation: "claim:typeSpeech",
- },
- [ContentModelEnum.Image]: {
- linkText: "claim:cardLinkToImage",
- contentPath: generateSentenceContentPath(
- nameSpace,
- personality,
- claim,
- claim?.contentModel
- ),
- title: claim.title,
- speechTypeTranslation: "",
- },
- [ContentModelEnum.Debate]: {
- linkText: "claim:cardLinkToDebate",
- contentPath: generateSentenceContentPath(
- nameSpace,
- personality,
- claim,
- claim?.contentModel
- ),
- title: `"(...) ${content.content}"`,
- speechTypeTranslation: "claim:typeDebate",
- },
- [ContentModelEnum.Unattributed]: {
- linkText: "claim:cardLinkToFullText",
- contentPath: generateSentenceContentPath(
- nameSpace,
- personality,
- claim,
- claim?.contentModel
- ),
- title: `"(...) ${content.content}"`,
- speechTypeTranslation: "claim:typeSpeech",
- },
- };
-
- const { linkText, contentPath, title, speechTypeTranslation } =
- contentProps[claim?.contentModel];
+ const { reviewTaskType } = useContext(ReviewTaskMachineContext);
+ const isClaim = reviewTaskType === ReviewTaskTypeEnum.Claim;
+ const {
+ vw: { sm, md },
+ } = useAppSelector((state) => state);
+ const isSource = reviewTaskType === ReviewTaskTypeEnum.Source;
+ const isVerificationRequest =
+ reviewTaskType === ReviewTaskTypeEnum.VerificationRequest;
return (
-
- {personality && (
-
-
-
- )}
-
- {classification && (
-
- {
- // TODO: Create a more meaningful h1 for this page
- t("claimReview:claimReview")
- }
-
-
- )}
-
-
- {title}
- {isImage && (
-
+ {personality && (
+
+
+
+ )}
+
+ {classification && (
+
+ {t(linkText)}
-
-
-
- {isImage
- ? t("claim:cardHeader3")
- : t("claim:cardHeader1")}
-
-
-
- {!isImage && t("claim:cardHeader2")}
- {t(speechTypeTranslation)}
-
- {hideDescription && (
-
- )}
-
-
+
+ )}
+ {isClaim && (
+
+ )}
+ {isSource && }
+ {isVerificationRequest && (
+
+ )}
+ {hideDescription && (
+
+ )}
+
);
};
diff --git a/src/components/SentenceReport/SentenceReportContent.tsx b/src/components/SentenceReport/SentenceReportContent.tsx
index 0ec7fa818..b2a2cf343 100644
--- a/src/components/SentenceReport/SentenceReportContent.tsx
+++ b/src/components/SentenceReport/SentenceReportContent.tsx
@@ -18,12 +18,12 @@ const SentenceReportContent = ({
href,
}) => {
const { t } = useTranslation();
- const { machineService, publishedReview } = useContext(
+ const { machineService, publishedReview, reviewTaskType } = useContext(
ReviewTaskMachineContext
);
const { summary, questions, report, verification, sources } = context;
const sanitizer = dompurify.sanitize;
- const sortedSources = sources.sort((a, b) => a.props.sup - b.props.sup);
+ const sortedSources = sources?.sort((a, b) => a.props.sup - b.props.sup);
const showAllSources = !(
useSelector(machineService, publishedSelector) ||
publishedReview?.review
@@ -34,7 +34,7 @@ const SentenceReportContent = ({
{showClassification && classification && (
- {t("claimReview:claimReview")}
+ {t(`claimReview:title${reviewTaskType}Review`)}
@@ -103,7 +103,7 @@ const SentenceReportContent = ({
)}
- {sources && (
+ {sources && sources?.length > 0 && (
<>
{t("claim:sourceSectionTitle")}
diff --git a/src/components/SentenceReport/SentenceReportSummary.tsx b/src/components/SentenceReport/SentenceReportSummary.tsx
index 81d5e456d..3af8babc5 100644
--- a/src/components/SentenceReport/SentenceReportSummary.tsx
+++ b/src/components/SentenceReport/SentenceReportSummary.tsx
@@ -8,6 +8,9 @@ const SentenceReportSummary = styled(Row)`
margin: 8px 0 16px 4px;
padding: 16px 24px;
border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
background-color: ${colors.lightYellow};
// small triangle pointing to the avatar on the side
diff --git a/src/components/SentenceReport/SentenceReportView.tsx b/src/components/SentenceReport/SentenceReportView.tsx
index 4493712bc..85095be1e 100644
--- a/src/components/SentenceReport/SentenceReportView.tsx
+++ b/src/components/SentenceReport/SentenceReportView.tsx
@@ -16,6 +16,7 @@ import SentenceReportContent from "./SentenceReportContent";
import { useAtom } from "jotai";
import { currentUserRole, isUserLoggedIn } from "../../atoms/currentUser";
import SentenceReportComments from "./SentenceReportComments";
+import { ReviewTaskTypeEnum } from "../../../server/types/enums";
const SentenceReportView = ({
context,
@@ -29,7 +30,7 @@ const SentenceReportView = ({
}) => {
const [isLoggedIn] = useAtom(isUserLoggedIn);
const [role] = useAtom(currentUserRole);
- const { machineService, publishedReview } = useContext(
+ const { machineService, publishedReview, reviewTaskType } = useContext(
ReviewTaskMachineContext
);
const isReport = useSelector(machineService, reportSelector);
@@ -50,7 +51,8 @@ const SentenceReportView = ({
canShowClassificationAndCrossChecking;
return (
- canShowReport && (
+ canShowReport &&
+ reviewTaskType !== ReviewTaskTypeEnum.VerificationRequest && (
{
+ const { t } = useTranslation();
+
+ return (
+
+
+ {href}
+
+
+
+ {t("sources:sourceCardButton")}
+
+
+ );
+};
+
+export default SourceSummaryDisplay;
diff --git a/src/components/SentenceReport/VerificationRequestDisplay.style.tsx b/src/components/SentenceReport/VerificationRequestDisplay.style.tsx
new file mode 100644
index 000000000..9cbd2f07a
--- /dev/null
+++ b/src/components/SentenceReport/VerificationRequestDisplay.style.tsx
@@ -0,0 +1,21 @@
+import styled from "styled-components";
+import { queries } from "../../styles/mediaQueries";
+import { Row } from "antd";
+
+const VerificationRequestCardDisplayStyle = styled(Row)`
+ .cta-create-claim {
+ display: flex;
+ gap: 32px;
+ align-items: center;
+ margin-bottom: 32px;
+ }
+
+ @media ${queries.sm} {
+ .cta-create-claim {
+ gap: 16px;
+ flex-direction: column;
+ }
+ }
+`;
+
+export default VerificationRequestCardDisplayStyle;
diff --git a/src/components/SentenceReport/VerificationRequestDisplay.tsx b/src/components/SentenceReport/VerificationRequestDisplay.tsx
new file mode 100644
index 000000000..1ba6cf5ed
--- /dev/null
+++ b/src/components/SentenceReport/VerificationRequestDisplay.tsx
@@ -0,0 +1,61 @@
+import { Col, Typography } from "antd";
+import React from "react";
+import VerificationRequestCard from "../VerificationRequest/VerificationRequestCard";
+import { useTranslation } from "next-i18next";
+import VerificationRequestCardDisplayStyle from "./VerificationRequestDisplay.style";
+import { useAppSelector } from "../../store/store";
+import VerificationRequestAlert from "../VerificationRequest/VerificationRequestAlert";
+
+const VerificationRequestDisplay = ({ content }) => {
+ const { t } = useTranslation();
+ const { vw } = useAppSelector((state) => state);
+ const { content: contentText, group } = content;
+ const verificationRequestGroup = group?.content?.filter(
+ (c) => c._id !== content._id
+ );
+
+ return (
+
+
+
+
+
+ {t("verificationRequest:verificationRequestTitle")}
+
+
+
+ {!vw.xs && (
+
+
+ {t("verificationRequest:agroupVerificationRequest")}
+
+
+ {verificationRequestGroup?.map(({ content }) => (
+
+
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+export default VerificationRequestDisplay;
diff --git a/src/components/Source/CreateSource/CreateSourceView.tsx b/src/components/Source/CreateSource/CreateSourceView.tsx
new file mode 100644
index 000000000..91000dfdf
--- /dev/null
+++ b/src/components/Source/CreateSource/CreateSourceView.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import { Col, Row } from "antd";
+import colors from "../../../styles/colors";
+import DynamicSourceForm from "./DynamicSourceForm";
+
+const CreateSourceView = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default CreateSourceView;
diff --git a/src/components/Source/CreateSource/DynamicSourceForm.tsx b/src/components/Source/CreateSource/DynamicSourceForm.tsx
new file mode 100644
index 000000000..510ca0b0b
--- /dev/null
+++ b/src/components/Source/CreateSource/DynamicSourceForm.tsx
@@ -0,0 +1,82 @@
+import AletheiaButton, { ButtonType } from "../../Button";
+import React, { useRef, useState } from "react";
+import AletheiaCaptcha from "../../AletheiaCaptcha";
+import DynamicForm from "../../Form/DynamicForm";
+import { Row } from "antd";
+import { useForm } from "react-hook-form";
+import { useTranslation } from "next-i18next";
+import { useRouter } from "next/router";
+import { useAtom } from "jotai";
+import { currentNameSpace } from "../../../atoms/namespace";
+import { currentUserId } from "../../../atoms/currentUser";
+import SourceApi from "../../../api/sourceApi";
+import createSourceForm from "./fieldLists/createSourceForm";
+
+const DynamicSourceForm = () => {
+ const {
+ handleSubmit,
+ control,
+ formState: { errors },
+ } = useForm();
+ const router = useRouter();
+ const { t } = useTranslation();
+ const [nameSpace] = useAtom(currentNameSpace);
+ const [userId] = useAtom(currentUserId);
+ const [isLoading, setIsLoading] = useState(false);
+ const [recaptchaString, setRecaptchaString] = useState("");
+ const hasCaptcha = !!recaptchaString;
+ const recaptchaRef = useRef(null);
+
+ const onSubmit = ({ source }) => {
+ const newSource = {
+ nameSpace,
+ href: source,
+ user: userId,
+ recaptcha: recaptchaString,
+ };
+
+ SourceApi.createSource(t, router, newSource).then((s) => {
+ router.push(`/source/${s.data_hash}`);
+ setIsLoading(false);
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default DynamicSourceForm;
diff --git a/src/components/Source/CreateSource/fieldLists/createSourceForm.ts b/src/components/Source/CreateSource/fieldLists/createSourceForm.ts
new file mode 100644
index 000000000..ae6cf7a8d
--- /dev/null
+++ b/src/components/Source/CreateSource/fieldLists/createSourceForm.ts
@@ -0,0 +1,11 @@
+import { createFormField, FormField } from "../../../Form/FormField";
+
+const createSourceForm: FormField[] = [
+ createFormField({
+ fieldName: "source",
+ type: "text",
+ defaultValue: "",
+ }),
+];
+
+export default createSourceForm;
diff --git a/src/components/Source/SourceCreateCTA.tsx b/src/components/Source/SourceCreateCTA.tsx
new file mode 100644
index 000000000..8dfaba359
--- /dev/null
+++ b/src/components/Source/SourceCreateCTA.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import AletheiaButton from "../Button";
+import { PlusOutlined } from "@ant-design/icons";
+import { useTranslation } from "next-i18next";
+import CreateCTAButton from "../CreateCTAButton";
+
+const SourceCreateCTA = () => {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t("sources:sourceCreateCTAButton")}
+
+
+ );
+};
+
+export default SourceCreateCTA;
diff --git a/src/components/Source/SourceList.tsx b/src/components/Source/SourceList.tsx
index ed1c47b73..dea1c17a8 100644
--- a/src/components/Source/SourceList.tsx
+++ b/src/components/Source/SourceList.tsx
@@ -3,61 +3,39 @@ import SourceApi from "../../api/sourceApi";
import BaseList from "../List/BaseList";
import SourceSkeleton from "../Skeleton/SourceSkeleton";
import SourceListItem from "./SourceListItem";
-import AletheiaButton from "../Button";
-import { Row } from "antd";
-import { PlusOutlined } from "@ant-design/icons";
+import { Col, Row } from "antd";
import { useTranslation } from "next-i18next";
import { useAtom } from "jotai";
import { currentNameSpace } from "../../atoms/namespace";
+import SourceCreateCTA from "./SourceCreateCTA";
const SourceList = ({ footer = false }) => {
const { t } = useTranslation();
const [nameSpace] = useAtom(currentNameSpace);
- const SourceCreateCTAButton = (
-
-
- {t("personalityCTA:header")}
-
-
-
-
- {t("sources:sourceCreateCTAButton")}
-
-
- {t("personalityCTA:footer")}
-
- );
-
return (
-
- source &&
- }
- grid={{
- gutter: 20,
- md: 2,
- lg: 2,
- xl: 2,
- xxl: 2,
- }}
- skeleton={}
- emptyFallback={SourceCreateCTAButton}
- footer={footer && SourceCreateCTAButton}
- />
+
+
+
+ source &&
+ }
+ grid={{
+ gutter: 20,
+ md: 2,
+ lg: 2,
+ xl: 2,
+ xxl: 2,
+ }}
+ skeleton={}
+ emptyFallback={}
+ footer={footer && }
+ />
+
+
);
};
export default SourceList;
diff --git a/src/components/Source/SourceListItem.tsx b/src/components/Source/SourceListItem.tsx
index 14b4f690f..98bc11b9c 100644
--- a/src/components/Source/SourceListItem.tsx
+++ b/src/components/Source/SourceListItem.tsx
@@ -1,10 +1,10 @@
import React, { useMemo } from "react";
import { Col, Typography } from "antd";
import CardBase from "../CardBase";
-import ClassificationText from "../ClassificationText";
import AletheiaButton from "../Button";
import { useTranslation } from "next-i18next";
import SourceListItemStyled from "./SourceListItem.style";
+import ReviewClassification from "../ClaimReview/ReviewClassification";
const { Paragraph } = Typography;
const DOMAIN_PROTOCOL_REGEX = /^(https?:\/\/)?(www\.)?/;
@@ -41,12 +41,10 @@ const SourceListItem = ({ source }) => {
-
- {t("sources:sourceReview")}
-
-
+
{
+const TabPanel = (props: TabPanelProps) => {
const { children, value, index, ...other } = props;
return (
@@ -18,9 +17,9 @@ const AdminScreens = (props: TabPanelProps) => {
aria-labelledby={`aletheia-tab-${index}`}
{...other}
>
- {value === index && {children}}
+ {value === index && {children}
}
);
};
-export default AdminScreens;
+export default TabPanel;
diff --git a/src/components/TextAreaAutoSize.tsx b/src/components/TextAreaAutoSize.tsx
index c4ec3b447..cbf5f2a5a 100644
--- a/src/components/TextAreaAutoSize.tsx
+++ b/src/components/TextAreaAutoSize.tsx
@@ -1,6 +1,6 @@
import styled from "styled-components";
import colors from "../styles/colors";
-import { TextareaAutosize } from "@mui/base";
+import { TextareaAutosize } from "@mui/material";
const AletheiaTextAreaAutoSize = styled(TextareaAutosize)`
background: ${(props) => (props.white ? colors.white : colors.lightGray)};
diff --git a/src/components/VerificationRequest/VerificationRequestAlert.tsx b/src/components/VerificationRequest/VerificationRequestAlert.tsx
new file mode 100644
index 000000000..36eb6c670
--- /dev/null
+++ b/src/components/VerificationRequest/VerificationRequestAlert.tsx
@@ -0,0 +1,56 @@
+import React, { useContext } from "react";
+import { Col } from "antd";
+import AletheiaButton from "../Button";
+import { useTranslation } from "next-i18next";
+import AletheiaAlert from "../AletheiaAlert";
+import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider";
+import { publishedSelector } from "../../machines/reviewTask/selectors";
+import { useSelector } from "@xstate/react";
+
+const VerificationRequestAlert = ({ targetId, verificationRequestId }) => {
+ const { t } = useTranslation();
+ const { machineService, publishedReview } = useContext(
+ ReviewTaskMachineContext
+ );
+ const isPublished =
+ useSelector(machineService, publishedSelector) ||
+ publishedReview?.review;
+
+ const alertProps = {
+ type: "warning",
+ showIcon: !targetId,
+ message: !targetId ? (
+ t("verificationRequest:createClaimFromVerificationRequest")
+ ) : (
+
+
+ {t("verificationRequest:openVerificationRequestClaimLabel")}
+
+
+ {t(
+ "verificationRequest:openVerificationRequestClaimButton"
+ )}
+
+
+ ),
+ description: !targetId ? (
+
+ {t("seo:claimCreateTitle")}
+
+ ) : null,
+ };
+
+ return (
+ {isPublished && }
+ );
+};
+
+export default VerificationRequestAlert;
diff --git a/src/components/VerificationRequest/VerificationRequestCard.tsx b/src/components/VerificationRequest/VerificationRequestCard.tsx
new file mode 100644
index 000000000..519cddac4
--- /dev/null
+++ b/src/components/VerificationRequest/VerificationRequestCard.tsx
@@ -0,0 +1,39 @@
+import { Col, Typography } from "antd";
+import React from "react";
+import colors from "../../styles/colors";
+import CardBase from "../CardBase";
+
+const VerificationRequestCard = ({
+ content,
+ actions = [],
+ expandable = true,
+}) => {
+ return (
+
+
+ {content}
+
+
+
+ {actions ? actions.map((action) => action) : <>>}
+
+
+ );
+};
+
+export default VerificationRequestCard;
diff --git a/src/components/VerificationRequest/VerificationRequestList.tsx b/src/components/VerificationRequest/VerificationRequestList.tsx
new file mode 100644
index 000000000..26f70da6f
--- /dev/null
+++ b/src/components/VerificationRequest/VerificationRequestList.tsx
@@ -0,0 +1,52 @@
+import React from "react";
+import BaseList from "../List/BaseList";
+import { Col, Row } from "antd";
+import { useTranslation } from "next-i18next";
+import { useAtom } from "jotai";
+import { currentNameSpace } from "../../atoms/namespace";
+import verificationRequestApi from "../../api/verificationRequestApi";
+import VerificationRequestCard from "./VerificationRequestCard";
+import AletheiaButton from "../Button";
+
+const VerificationRequestList = () => {
+ const { t } = useTranslation();
+ const [nameSpace] = useAtom(currentNameSpace);
+
+ return (
+
+
+ (
+
+ {t(
+ "verificationRequest:openVerificationRequest"
+ )}
+ ,
+ ]}
+ />
+ )}
+ grid={{
+ gutter: 20,
+ md: 2,
+ lg: 2,
+ xl: 2,
+ xxl: 2,
+ }}
+ />
+
+
+ );
+};
+export default VerificationRequestList;
diff --git a/src/machines/createClaim/actions.ts b/src/machines/createClaim/actions.ts
index 36bc8e127..3f18cffc4 100644
--- a/src/machines/createClaim/actions.ts
+++ b/src/machines/createClaim/actions.ts
@@ -82,6 +82,7 @@ const persistClaim = assign(
const sendData = {
...claimData,
personalities: claimData.personalities.map((p) => p._id),
+ group: claimData.group._id,
};
const saveFunctions = {
diff --git a/src/machines/createClaim/context.ts b/src/machines/createClaim/context.ts
index 733398043..596e154fb 100644
--- a/src/machines/createClaim/context.ts
+++ b/src/machines/createClaim/context.ts
@@ -9,5 +9,6 @@ export const initialContext: CreateClaimContext = {
title: "",
date: new Date().toLocaleDateString(),
sources: [],
+ group: null,
},
};
diff --git a/src/machines/createClaim/createClaimMachine.ts b/src/machines/createClaim/createClaimMachine.ts
index be4713ce9..fbcf84a37 100644
--- a/src/machines/createClaim/createClaimMachine.ts
+++ b/src/machines/createClaim/createClaimMachine.ts
@@ -16,6 +16,12 @@ import {
} from "./actions";
import { CreateClaimContext } from "./context";
+const updateGroupEvent = {
+ [Events.updateGroup]: {
+ actions: [saveClaimContext],
+ },
+};
+
export const newCreateClaimMachine = ({ value, context }) => {
return createMachine<
CreateClaimContext,
@@ -43,6 +49,7 @@ export const newCreateClaimMachine = ({ value, context }) => {
target: States.personalityAdded,
actions: [startUnattributed],
},
+ ...updateGroupEvent,
},
},
[States.setupSpeech]: {
@@ -58,6 +65,7 @@ export const newCreateClaimMachine = ({ value, context }) => {
target: States.setupSpeech,
actions: [removePersonality],
},
+ ...updateGroupEvent,
},
},
[States.setupImage]: {
@@ -73,6 +81,7 @@ export const newCreateClaimMachine = ({ value, context }) => {
[Events.savePersonality]: {
target: States.personalityAdded,
},
+ ...updateGroupEvent,
},
},
[States.setupDebate]: {
@@ -88,6 +97,7 @@ export const newCreateClaimMachine = ({ value, context }) => {
target: States.setupDebate,
actions: [removePersonality],
},
+ ...updateGroupEvent,
},
},
[States.personalityAdded]: {
@@ -96,6 +106,7 @@ export const newCreateClaimMachine = ({ value, context }) => {
target: States.persisted,
actions: [persistClaim],
},
+ ...updateGroupEvent,
},
},
[States.persisted]: {
diff --git a/src/machines/createClaim/provider.ts b/src/machines/createClaim/provider.ts
index 6c50826b5..7cdcad754 100644
--- a/src/machines/createClaim/provider.ts
+++ b/src/machines/createClaim/provider.ts
@@ -10,14 +10,18 @@ const claimPersonalities = atom([]) as PrimitiveAtom<
Personality[]
>;
+const claimVerificationRequestsGroup = atom(null) as PrimitiveAtom; // type
+
const machineConfig = atom((get) => {
const value = CreateClaimStates.notStarted;
const personalities = get(claimPersonalities);
+ const group = get(claimVerificationRequestsGroup);
const nameSpace = get(currentNameSpace);
const context: CreateClaimContext = {
...initialContext,
claimData: {
personalities,
+ group,
contentModel: null,
nameSpace,
},
@@ -30,4 +34,8 @@ const createClaimMachineAtom = atomWithMachine((get) =>
newCreateClaimMachine(get(machineConfig))
);
-export { claimPersonalities, createClaimMachineAtom };
+export {
+ claimPersonalities,
+ createClaimMachineAtom,
+ claimVerificationRequestsGroup as claimVerificationRequests,
+};
diff --git a/src/machines/createClaim/types.ts b/src/machines/createClaim/types.ts
index 4931677ad..470371fdf 100644
--- a/src/machines/createClaim/types.ts
+++ b/src/machines/createClaim/types.ts
@@ -8,6 +8,7 @@ enum CreateClaimEvents {
savePersonality = "SAVE_PERSONALITY",
noPersonality = "NO_PERSONALITY",
persist = "PERSIST",
+ updateGroup = "UPDATE_GROUP",
}
enum CreateClaimStates {
diff --git a/src/machines/reviewTask/ReviewTaskMachineProvider.tsx b/src/machines/reviewTask/ReviewTaskMachineProvider.tsx
index 9452fc67e..63757512c 100644
--- a/src/machines/reviewTask/ReviewTaskMachineProvider.tsx
+++ b/src/machines/reviewTask/ReviewTaskMachineProvider.tsx
@@ -2,7 +2,7 @@ import { useTranslation } from "next-i18next";
import { createContext, useEffect, useState } from "react";
import ClaimReviewApi from "../../api/claimReviewApi";
-import ClaimReviewTaskApi from "../../api/ClaimReviewTaskApi";
+import ReviewTaskApi from "../../api/reviewTaskApi";
import Loading from "../../components/Loading";
import { getInitialContext } from "./context";
import { ReportModelEnum, ReviewTaskEvents, ReviewTaskStates } from "./enums";
@@ -15,35 +15,49 @@ import { currentUserId } from "../../atoms/currentUser";
interface ContextType {
machineService: any;
+ reviewTaskType: string;
publishedReview?: { review: any };
setFormAndEvents?: (param: string, isSameLabel?: boolean) => void;
form?: FormField[];
events?: ReviewTaskEvents[];
reportModel?: string;
recreateMachine?: (reportModel: string) => void;
+ claim?: any;
+ sentenceContent?: any;
}
export const ReviewTaskMachineContext = createContext({
machineService: null,
+ reviewTaskType: null,
});
interface ReviewTaskMachineProviderProps {
+ reviewTaskType: string;
data_hash: string;
children: React.ReactNode;
baseMachine?: any;
baseReportModel?: any;
publishedReview?: { review: any };
+ claim?: any;
+ sentenceContent?: any;
}
-const getMachineInitialState = (userId: string = ""): any => ({
+const getMachineInitialState = (
+ reviewTaskType: string,
+ userId: string = ""
+): any => ({
[ReportModelEnum.FactChecking]: {
- context: getInitialContext({}),
+ context: getInitialContext(reviewTaskType),
value: ReviewTaskStates.unassigned,
},
[ReportModelEnum.InformativeNews]: {
- context: getInitialContext({ usersId: [userId] }),
+ context: getInitialContext(reviewTaskType, { usersId: [userId] }),
value: ReviewTaskStates.assigned,
},
+ [ReportModelEnum.Request]: {
+ context: getInitialContext(reviewTaskType),
+ value: ReviewTaskStates.unassigned,
+ },
});
/* We chose not to use Jotai as the provider for this machine because we need
@@ -77,7 +91,7 @@ export const ReviewTaskMachineProvider = (
machine: props.baseMachine,
reportModel: props.baseReportModel,
})
- : ClaimReviewTaskApi.getMachineByDataHash(data_hash);
+ : ReviewTaskApi.getMachineByDataHash(data_hash);
};
setLoading(true);
fetchReviewTask(props.data_hash).then(({ machine, reportModel }) => {
@@ -93,7 +107,9 @@ export const ReviewTaskMachineProvider = (
}
const newMachine =
machine ||
- getMachineInitialState()[ReportModelEnum.FactChecking];
+ getMachineInitialState(props.reviewTaskType)[
+ ReportModelEnum.FactChecking
+ ];
setReportModel(reportModel);
setGlobalMachineService(
@@ -146,7 +162,9 @@ export const ReviewTaskMachineProvider = (
const createMachineBasedOnReportModel = (reportModel: string) => {
setLoading(true);
setReportModel(reportModel as ReportModelEnum);
- const newMachine = getMachineInitialState(userId)[reportModel];
+ const newMachine = getMachineInitialState(props.reviewTaskType, userId)[
+ reportModel
+ ];
setGlobalMachineService(
createNewMachineService(newMachine, reportModel)
);
@@ -158,11 +176,14 @@ export const ReviewTaskMachineProvider = (
value={{
machineService: globalMachineService,
publishedReview: publishedClaimReview,
+ reviewTaskType: props.reviewTaskType,
setFormAndEvents,
reportModel,
form,
events,
recreateMachine: createMachineBasedOnReportModel,
+ claim: props.claim,
+ sentenceContent: props.sentenceContent,
}}
>
{loading && }
diff --git a/src/machines/reviewTask/actions.ts b/src/machines/reviewTask/actions.ts
index 4d06ba7d5..c15d61f4e 100644
--- a/src/machines/reviewTask/actions.ts
+++ b/src/machines/reviewTask/actions.ts
@@ -8,11 +8,12 @@ const saveContext = assign(
(context, event) => {
const editorParser = new EditorParser();
if (
- event.type === ReviewTaskEvents.finishReport ||
- event.type === ReviewTaskEvents.draft
+ (event.type === ReviewTaskEvents.finishReport ||
+ event.type === ReviewTaskEvents.draft) &&
+ "visualEditor" in event.reviewData
) {
const schema = editorParser.editor2schema(
- event.reviewData.collaborativeEditor
+ event.reviewData.visualEditor.toJSON()
);
const reviewDataHtml = editorParser.schema2html(schema);
event.reviewData = {
@@ -26,38 +27,26 @@ const saveContext = assign(
...context.reviewData,
...event.reviewData,
},
- claimReview: {
- ...context.claimReview,
- ...event.claimReview,
+ review: {
+ ...context.review,
+ ...event.review,
isPartialReview: false,
},
};
}
);
-const savePartialReviewContext = assign<
+const rejectVerificationRequest = assign<
ReviewTaskMachineContextType,
SaveEvent
->((context, event) => {
- const editorParser = new EditorParser();
- const reviewData = editorParser.editor2schema(
- event.reviewData.collaborativeEditor
- );
- event.reviewData = {
- ...event.reviewData,
- ...reviewData,
- };
+>((context) => {
return {
reviewData: {
...context.reviewData,
- ...event.reviewData,
- },
- claimReview: {
- ...context.claimReview,
- ...event.claimReview,
- isPartialReview: true,
+ rejected: true,
},
+ review: context.review,
};
});
-export { saveContext, savePartialReviewContext };
+export { saveContext, rejectVerificationRequest };
diff --git a/src/machines/reviewTask/context.ts b/src/machines/reviewTask/context.ts
index 958636ff9..f1e8dc024 100644
--- a/src/machines/reviewTask/context.ts
+++ b/src/machines/reviewTask/context.ts
@@ -1,37 +1,74 @@
-import { ClaimReview, ReviewData } from "./events";
+import { ReviewTaskTypeEnum } from "./enums";
+import { Review, ReviewData } from "./events";
export type ReviewTaskMachineContextType = {
reviewData: ReviewData;
- claimReview: ClaimReview;
+ review: Review;
};
-const buildState = (reviewData): ReviewTaskMachineContextType => {
+const buildState = (
+ reviewTaskType,
+ reviewData
+): ReviewTaskMachineContextType => {
+ const baseReviewData = {
+ usersId: [],
+ summary: "",
+ classification: "",
+ // initial value must be null to be able to use populate before selecting reviewer
+ reviewerId: null,
+ crossCheckerId: null,
+ crossCheckingComments: [],
+ crossCheckingComment: "",
+ crossCheckingClassification: "",
+ };
+
+ const claimReviewData = {
+ questions: [],
+ report: "",
+ verification: "",
+ sources: [],
+ };
+
+ const review = {
+ personality: "",
+ usersId: "",
+ targetId: "",
+ isPartialReview: false,
+ };
+
+ if (reviewTaskType === ReviewTaskTypeEnum.VerificationRequest) {
+ return {
+ reviewData: {
+ usersId: [],
+ isSensitive: false,
+ group: [],
+ rejected: false,
+ },
+ review,
+ };
+ }
+
+ if (reviewTaskType === ReviewTaskTypeEnum.Source) {
+ return {
+ reviewData: {
+ ...baseReviewData,
+ ...reviewData,
+ },
+ review,
+ };
+ }
+
return {
reviewData: {
- usersId: [],
- summary: "",
- questions: [],
- report: "",
- verification: "",
- sources: [],
- classification: "",
- // initial value must be null to be able to use populate before selecting reviewer
- reviewerId: null,
- crossCheckerId: null,
- crossCheckingComments: [],
- crossCheckingComment: "",
- crossCheckingClassification: "",
+ ...baseReviewData,
+ ...claimReviewData,
...reviewData,
},
- claimReview: {
- personality: "",
- claim: "",
- usersId: "",
- isPartialReview: false,
- },
+ review,
};
};
export const getInitialContext = (
- reviewData = {}
-): ReviewTaskMachineContextType => buildState(reviewData);
+ reviewTaskType: string,
+ reviewData: Partial = {}
+): ReviewTaskMachineContextType => buildState(reviewTaskType, reviewData);
diff --git a/src/machines/reviewTask/enums.ts b/src/machines/reviewTask/enums.ts
index 3e118b7fd..11c2a3efe 100644
--- a/src/machines/reviewTask/enums.ts
+++ b/src/machines/reviewTask/enums.ts
@@ -15,6 +15,31 @@ enum ReviewTaskEvents {
selectedReview = "SELECTED_REVIEW",
selectedCrossChecking = "SELECTED_CROSS_CHECKING",
reAssignUser = "RE_ASSIGN_USER",
+ reset = "RESET",
+ rejectRequest = "REJECT_REQUEST",
+ assignRequest = "ASSIGN_REQUEST",
+}
+
+enum KanbanVerificationRequestStates {
+ assignedRequest = "assignedRequest",
+ rejectedRequest = "rejectedRequest",
+ published = "published",
+}
+
+enum KanbanSourceState {
+ assigned = "assigned",
+ reported = "reported",
+ crossChecking = "cross-checking",
+ submitted = "submitted",
+ published = "published",
+}
+
+enum KanbanClaimState {
+ assigned = "assigned",
+ reported = "reported",
+ crossChecking = "cross-checking",
+ submitted = "submitted",
+ published = "published",
}
enum ReviewTaskStates {
@@ -28,6 +53,8 @@ enum ReviewTaskStates {
rejected = "rejected",
addCommentCrossChecking = "addCommentCrossChecking",
published = "published",
+ rejectedRequest = "rejectedRequest",
+ assignedRequest = "assignedRequest",
}
enum CompoundStates {
@@ -38,6 +65,22 @@ enum CompoundStates {
enum ReportModelEnum {
FactChecking = "Fact-checking",
InformativeNews = "Informative News",
+ Request = "Request",
+}
+
+enum ReviewTaskTypeEnum {
+ Claim = "Claim",
+ Source = "Source",
+ VerificationRequest = "VerificationRequest",
}
-export { ReviewTaskEvents, ReviewTaskStates, CompoundStates, ReportModelEnum };
+export {
+ ReviewTaskEvents,
+ ReviewTaskStates,
+ CompoundStates,
+ ReportModelEnum,
+ ReviewTaskTypeEnum,
+ KanbanVerificationRequestStates,
+ KanbanSourceState,
+ KanbanClaimState,
+};
diff --git a/src/machines/reviewTask/events.ts b/src/machines/reviewTask/events.ts
index 9285ce499..1b738ba9a 100644
--- a/src/machines/reviewTask/events.ts
+++ b/src/machines/reviewTask/events.ts
@@ -1,4 +1,4 @@
-import { RemirrorJSON } from "remirror";
+import { ProsemirrorNode } from "remirror";
export enum ClassificationEnum {
"not-fact" = 0,
@@ -12,7 +12,7 @@ export enum ClassificationEnum {
"trustworthy",
}
-export type ReviewData = {
+type FactCheckingReviewData = {
usersId: string[];
summary: string;
questions: string[];
@@ -21,16 +21,36 @@ export type ReviewData = {
sources: string[] | object[];
classification: string | ClassificationEnum;
reviewerId: string;
- collaborativeEditor?: RemirrorJSON;
+ visualEditor?: ProsemirrorNode;
reviewDataHtml?: any;
crossCheckingComments: any[];
crossCheckingComment: string;
crossCheckingClassification: string;
};
-export type ClaimReview = {
+type InformativeNewsReviewData = {
+ usersId: string[];
+ summary: string;
+ sources: string[] | object[];
+ classification: string | ClassificationEnum;
+ visualEditor?: ProsemirrorNode;
+ reviewDataHtml?: any;
+};
+
+type RequestReviewData = {
+ usersId: string[];
+ isSensitive: boolean;
+ group: any[];
+ rejected: boolean;
+};
+
+export type ReviewData =
+ | FactCheckingReviewData
+ | InformativeNewsReviewData
+ | RequestReviewData;
+
+export type Review = {
personality: string;
- claim: string;
usersId: string;
isPartialReview: boolean;
};
@@ -38,7 +58,7 @@ export type ClaimReview = {
export type SaveEvent = {
type: string;
reviewData: ReviewData;
- claimReview: ClaimReview;
+ review: Review;
};
export type ReviewTaskMachineEvents = SaveEvent;
diff --git a/src/machines/reviewTask/getNextEvent.ts b/src/machines/reviewTask/getNextEvent.ts
index 07560d7eb..a6d4adcb5 100644
--- a/src/machines/reviewTask/getNextEvent.ts
+++ b/src/machines/reviewTask/getNextEvent.ts
@@ -11,13 +11,20 @@ const getNextEvents = (
) => {
const defaultEvents = [Events.goback, Events.draft];
const eventsMap = {
- [States.unassigned]: [Events.assignUser],
+ [States.unassigned]: [
+ reportModel === ReportModelEnum.Request
+ ? Events.assignRequest
+ : Events.assignUser,
+ ],
[Events.assignUser]: [...defaultEvents, Events.finishReport],
[States.assigned]:
reportModel === ReportModelEnum.FactChecking
? [...defaultEvents, Events.finishReport]
: [Events.draft, Events.finishReport],
+ [Events.assignRequest]: [Events.rejectRequest, Events.publish],
+ [States.assignedRequest]: [Events.rejectRequest, Events.publish],
+
[Events.finishReport]: [
Events.goback,
reportModel === ReportModelEnum.FactChecking
@@ -78,8 +85,14 @@ const getNextEvents = (
? [...defaultEvents, Events.finishReport]
: [Events.draft, Events.finishReport],
- [States.published]: [],
- [Events.publish]: [],
+ [States.published]:
+ reportModel === ReportModelEnum.Request ? [Events.reset] : [],
+ [Events.publish]:
+ reportModel === ReportModelEnum.Request ? [Events.reset] : [],
+
+ [Events.reset]: [Events.rejectRequest, Events.publish],
+ [States.rejectedRequest]: [],
+ [Events.rejectRequest]: [],
};
return eventsMap[param];
diff --git a/src/machines/reviewTask/getNextForm.ts b/src/machines/reviewTask/getNextForm.ts
index f506dce8f..83cce84fd 100644
--- a/src/machines/reviewTask/getNextForm.ts
+++ b/src/machines/reviewTask/getNextForm.ts
@@ -1,11 +1,12 @@
import { ReviewTaskEvents, ReviewTaskStates } from "./enums";
-import assignedCollaborativeForm from "../../components/ClaimReview/form/fieldLists/assignedCollaborativeForm";
+import visualEditor from "../../components/ClaimReview/form/fieldLists/visualEditor";
import selectReviewer from "../../components/ClaimReview/form/fieldLists/selectReviewerForm";
import unassignedForm from "../../components/ClaimReview/form/fieldLists/unassignedForm";
import submittedForm from "../../components/ClaimReview/form/fieldLists/submittedForm";
import crossCheckingForm from "../../components/ClaimReview/form/fieldLists/crossCheckingForm";
import selectCrossCheckerForm from "../../components/ClaimReview/form/fieldLists/selectCrossCheckerForm";
+import verificationRequestForm from "../../components/ClaimReview/form/fieldLists/verificationRequestForm";
const getNextForm = (
param: ReviewTaskEvents | ReviewTaskStates,
@@ -13,8 +14,10 @@ const getNextForm = (
) => {
const formMap = {
[ReviewTaskStates.unassigned]: unassignedForm,
- [ReviewTaskEvents.assignUser]: assignedCollaborativeForm,
- [ReviewTaskStates.assigned]: assignedCollaborativeForm,
+ [ReviewTaskEvents.assignUser]: visualEditor,
+ [ReviewTaskStates.assigned]: visualEditor,
+ [ReviewTaskEvents.assignRequest]: verificationRequestForm,
+ [ReviewTaskStates.assignedRequest]: verificationRequestForm,
[ReviewTaskEvents.finishReport]: [],
[ReviewTaskStates.reported]: [],
@@ -32,16 +35,18 @@ const getNextForm = (
[ReviewTaskEvents.addComment]: crossCheckingForm,
[ReviewTaskStates.addCommentCrossChecking]: crossCheckingForm,
- [ReviewTaskEvents.submitComment]: isSameLabel
- ? []
- : assignedCollaborativeForm,
+ [ReviewTaskEvents.submitComment]: isSameLabel ? [] : visualEditor,
[ReviewTaskEvents.sendToReview]: [],
[ReviewTaskStates.submitted]: [],
[ReviewTaskStates.rejected]: submittedForm,
- [ReviewTaskEvents.addRejectionComment]: assignedCollaborativeForm,
+ [ReviewTaskEvents.addRejectionComment]: visualEditor,
[ReviewTaskStates.published]: [],
[ReviewTaskEvents.publish]: [],
+
+ [ReviewTaskEvents.reset]: verificationRequestForm,
+ [ReviewTaskStates.rejectedRequest]: [],
+ [ReviewTaskEvents.rejectRequest]: [],
};
return formMap[param];
diff --git a/src/machines/reviewTask/machineWorkflow.ts b/src/machines/reviewTask/machineWorkflow.ts
index 1ac80c538..864e25c9f 100644
--- a/src/machines/reviewTask/machineWorkflow.ts
+++ b/src/machines/reviewTask/machineWorkflow.ts
@@ -1,5 +1,5 @@
import { BaseActionObject, StatesConfig } from "xstate/lib/types";
-import { saveContext } from "./actions";
+import { rejectVerificationRequest, saveContext } from "./actions";
import {
CompoundStates,
ReviewTaskEvents as Events,
@@ -212,7 +212,52 @@ const informativeNewsWorkflow: StatesConfig<
},
};
+const RequestWorkflow: StatesConfig<
+ ReviewTaskMachineContextType,
+ any,
+ SaveEvent,
+ BaseActionObject
+> = {
+ [States.unassigned]: {
+ on: {
+ [Events.assignRequest]: {
+ target: States.assignedRequest,
+ actions: [saveContext],
+ },
+ },
+ },
+ [States.assignedRequest]: {
+ on: {
+ [Events.rejectRequest]: {
+ target: States.rejectedRequest,
+ actions: [rejectVerificationRequest],
+ },
+ [Events.reAssignUser]: {
+ target: States.unassigned,
+ },
+ [Events.publish]: {
+ target: States.published,
+ actions: [saveContext],
+ },
+ },
+ },
+ [States.published]: {
+ on: {
+ [Events.reAssignUser]: {
+ target: States.unassigned,
+ },
+ [Events.reset]: {
+ target: States.assignedRequest,
+ },
+ },
+ },
+ [States.rejectedRequest]: {
+ type: "final",
+ },
+};
+
export const machineWorkflow = {
[ReportModelEnum.FactChecking]: factCheckingWorkflow,
[ReportModelEnum.InformativeNews]: informativeNewsWorkflow,
+ [ReportModelEnum.Request]: RequestWorkflow,
};
diff --git a/src/machines/reviewTask/reviewTaskMachine.ts b/src/machines/reviewTask/reviewTaskMachine.ts
index 297f2da93..9c5e38311 100644
--- a/src/machines/reviewTask/reviewTaskMachine.ts
+++ b/src/machines/reviewTask/reviewTaskMachine.ts
@@ -1,4 +1,4 @@
-import api from "../../api/ClaimReviewTaskApi";
+import api from "../../api/reviewTaskApi";
import { createMachine, interpret } from "xstate";
import { ReviewTaskMachineContextType } from "./context";
import { ReviewTaskMachineEvents } from "./events";
@@ -6,6 +6,7 @@ import { ReviewTaskMachineState } from "./states";
import { ReviewTaskEvents as Events, ReportModelEnum } from "./enums";
import sendReviewNotifications from "../../notifications/sendReviewNotifications";
import { isSameLabel, machineWorkflow } from "./machineWorkflow";
+import verificationRequestApi from "../../api/verificationRequestApi";
export const createNewMachine = ({ value, context }, reportModel) => {
return createMachine<
@@ -33,60 +34,74 @@ export const transitionHandler = (state) => {
resetIsLoading,
currentUserId,
nameSpace,
+ reviewTaskType,
+ target,
} = state.event;
const event = state.event.type;
+ const { value } = state;
+ const { reviewData, review } = state.context;
+ const nextState = typeof value !== "string" ? Object.keys(value)[0] : value;
- const nextState =
- typeof state.value !== "string"
- ? Object.keys(state.value)[0]
- : state.value;
+ const shouldUpdateVerificationRequest =
+ (value === "published" || value === "rejectedRequest") &&
+ reportModel === ReportModelEnum.Request;
- if (
+ const shouldNotUpdateReviewTask =
event === Events.reject ||
event === Events.selectedCrossChecking ||
event === Events.selectedReview ||
- event === Events.reAssignUser
- ) {
- setFormAndEvents(nextState);
- } else if (event !== Events.init) {
- api.createClaimReviewTask(
- {
- data_hash,
- reportModel,
- machine: {
- context: {
- reviewData: state.context.reviewData,
- claimReview: state.context.claimReview,
- },
- value: state.value,
- },
- recaptcha: recaptchaString,
- nameSpace,
- },
- t,
- event
- )
- .then(() => {
- return event === Events.goback
- ? setFormAndEvents(nextState)
- : setFormAndEvents(
- event,
- isSameLabel(state.context, state.event)
- );
- })
- .catch((e) => {
- // TODO: Track errors with Sentry
- })
- .finally(() => resetIsLoading());
+ event === Events.reAssignUser;
+
+ if (event === Events.init) {
+ return;
+ }
+
+ if (shouldNotUpdateReviewTask) {
+ return setFormAndEvents(nextState);
}
- sendReviewNotifications(
+ const reviewTask = {
data_hash,
- event,
- state.context.reviewData,
- currentUserId,
- t
- );
+ reportModel,
+ machine: {
+ context: {
+ reviewData,
+ review,
+ },
+ value: value,
+ },
+ recaptcha: recaptchaString,
+ nameSpace,
+ reviewTaskType,
+ target,
+ };
+
+ api.createReviewTask(reviewTask, t, event)
+ .then(async () => {
+ if (shouldUpdateVerificationRequest) {
+ const redirectUrl = `/claim/create?verificationRequest=${target}`;
+ await verificationRequestApi.updateVerificationRequest(target, {
+ ...reviewData,
+ });
+
+ if (value === "published") {
+ window.location.href = redirectUrl;
+ }
+ }
+
+ return event === Events.goback
+ ? setFormAndEvents(nextState)
+ : setFormAndEvents(
+ event,
+ isSameLabel(state.context, state.event)
+ );
+ })
+ .catch((e) => {
+ // TODO: Track errors with Sentry
+ })
+ .finally(() => resetIsLoading());
+
+ sendReviewNotifications(data_hash, event, reviewData, currentUserId, t);
};
export const createNewMachineService = (
diff --git a/src/machines/reviewTask/selectors.ts b/src/machines/reviewTask/selectors.ts
index b225d3384..937b5622f 100644
--- a/src/machines/reviewTask/selectors.ts
+++ b/src/machines/reviewTask/selectors.ts
@@ -27,10 +27,7 @@ const reviewingSelector = (state) => {
};
const reviewNotStartedSelector = (state) => {
- return (
- state.matches(ReviewTaskStates.unassigned) &&
- state.context.claimReview.claim === ""
- );
+ return state.matches(ReviewTaskStates.unassigned);
};
const reviewDataSelector = (state) => {
diff --git a/src/pages/admin-page.tsx b/src/pages/admin-page.tsx
index c9eb0f94a..af3c302fc 100644
--- a/src/pages/admin-page.tsx
+++ b/src/pages/admin-page.tsx
@@ -11,7 +11,7 @@ import BadgesApi from "../api/badgesApi";
import { Grid } from "@mui/material";
import DashboardView from "../components/Dashboard/DashboardView";
import AdminTabNavigator from "../components/adminArea/AdminTabNavigator";
-import AdminScreens from "../components/adminArea/AdminScreens";
+import TabPanel from "../components/TabPanel";
import { currentNameSpace } from "../atoms/namespace";
import { NameSpaceEnum } from "../types/Namespace";
@@ -43,13 +43,13 @@ const Admin: NextPage<{ users: string; nameSpace: string }> = ({
-
+
-
+
-
+
-
+
diff --git a/src/pages/claim-create.tsx b/src/pages/claim-create.tsx
index fe7d584c7..78fac2c51 100644
--- a/src/pages/claim-create.tsx
+++ b/src/pages/claim-create.tsx
@@ -7,7 +7,10 @@ import { useDispatch } from "react-redux";
import AffixButton from "../components/AffixButton/AffixButton";
import CreateClaimView from "../components/Claim/CreateClaim/CreateClaimView";
import Seo from "../components/Seo";
-import { claimPersonalities } from "../machines/createClaim/provider";
+import {
+ claimPersonalities,
+ claimVerificationRequests,
+} from "../machines/createClaim/provider";
import actions from "../store/actions";
import { GetLocale } from "../utils/GetLocale";
import { NameSpaceEnum } from "../types/Namespace";
@@ -17,6 +20,7 @@ const ClaimCreatePage: NextPage = ({
sitekey,
personality,
nameSpace,
+ verificationRequestGroup,
}) => {
const { t } = useTranslation();
const setCurrentNameSpace = useSetAtom(currentNameSpace);
@@ -35,6 +39,7 @@ const ClaimCreatePage: NextPage = ({
//@ts-ignore
initialValues={[
[claimPersonalities, personality ? [personality] : []],
+ [claimVerificationRequests, verificationRequestGroup],
[currentNameSpace, nameSpace],
]}
>
@@ -56,6 +61,9 @@ export async function getServerSideProps({ query, locale, locales, req }) {
personality: query?.personality
? JSON.parse(JSON.stringify(query?.personality))
: "",
+ verificationRequestGroup: query?.verificationRequestGroup
+ ? JSON.parse(JSON.stringify(query?.verificationRequestGroup))
+ : "",
nameSpace: query.nameSpace ? query.nameSpace : NameSpaceEnum.Main,
},
};
diff --git a/src/pages/claim-review.tsx b/src/pages/claim-review.tsx
index 07ec37427..6bbebd3b5 100644
--- a/src/pages/claim-review.tsx
+++ b/src/pages/claim-review.tsx
@@ -13,17 +13,18 @@ import actions from "../store/actions";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { useDispatch } from "react-redux";
import { useTranslation } from "next-i18next";
-import { CollaborativeEditorProvider } from "../components/Collaborative/CollaborativeEditorProvider";
+import { VisualEditorProvider } from "../components/Collaborative/VisualEditorProvider";
import { NameSpaceEnum } from "../types/Namespace";
import { useSetAtom } from "jotai";
import { currentNameSpace } from "../atoms/namespace";
+import { ReviewTaskTypeEnum } from "../machines/reviewTask/enums";
export interface ClaimReviewPageProps {
personality?: any;
claim: any;
content: any;
sitekey: string;
- claimReviewTask: any;
+ reviewTask: any;
claimReview: any;
hideDescriptions: object;
enableCollaborativeEditor: boolean;
@@ -127,18 +128,21 @@ const ClaimReviewPage: NextPage = (props) => {
-
+
-
+
>
@@ -155,7 +159,7 @@ export async function getServerSideProps({ query, locale, locales, req }) {
personality: JSON.parse(JSON.stringify(query.personality)),
claim: JSON.parse(JSON.stringify(query.claim)),
content: JSON.parse(JSON.stringify(query.content)),
- claimReviewTask: JSON.parse(JSON.stringify(query.claimReviewTask)),
+ reviewTask: JSON.parse(JSON.stringify(query.reviewTask)),
claimReview: JSON.parse(JSON.stringify(query.claimReview)),
sitekey: query.sitekey,
hideDescriptions: JSON.parse(
diff --git a/src/pages/home-page.tsx b/src/pages/home-page.tsx
index 10f6033f3..804fef118 100644
--- a/src/pages/home-page.tsx
+++ b/src/pages/home-page.tsx
@@ -16,6 +16,7 @@ const HomePage: NextPage<{
href;
claims;
nameSpace;
+ reviews;
}> = (props) => {
const { t } = useTranslation();
const setCurrentNameSpace = useSetAtom(currentNameSpace);
@@ -36,6 +37,7 @@ export async function getServerSideProps({ query, locale, locales, req }) {
// Nextjs have problems with client re-hydration for some serialized objects
// This is a hack until a better solution https://github.com/vercel/next.js/issues/11993
personalities: JSON.parse(JSON.stringify(query.personalities)),
+ reviews: JSON.parse(JSON.stringify(query.reviews)),
claims: JSON.parse(JSON.stringify(query.claims)),
stats: JSON.parse(JSON.stringify(query.stats)),
nameSpace: query.nameSpace ? query.nameSpace : NameSpaceEnum.Main,
diff --git a/src/pages/kanban-page.tsx b/src/pages/kanban-page.tsx
index b746ad600..50af94356 100644
--- a/src/pages/kanban-page.tsx
+++ b/src/pages/kanban-page.tsx
@@ -3,7 +3,7 @@ import AffixButton from "../components/AffixButton/AffixButton";
import { GetLocale } from "../utils/GetLocale";
import KanbanView from "../components/Kanban/KanbanView";
import { NextPage } from "next";
-import React from "react";
+import React, { useEffect } from "react";
import Seo from "../components/Seo";
import actions from "../store/actions";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
@@ -11,6 +11,11 @@ import { useDispatch } from "react-redux";
import { useSetAtom } from "jotai";
import { currentNameSpace } from "../atoms/namespace";
import { NameSpaceEnum } from "../types/Namespace";
+import { Grid } from "@mui/material";
+import KanbanTabNavigator from "../components/Kanban/KanbanTabNavigator";
+import TabPanel from "../components/TabPanel";
+import { ReviewTaskTypeEnum } from "../machines/reviewTask/enums";
+import Cookies from "js-cookie";
const KanbanPage: NextPage<{
sitekey;
@@ -21,6 +26,7 @@ const KanbanPage: NextPage<{
websocketUrl: string;
nameSpace: NameSpaceEnum;
}> = (props) => {
+ const kanban_tab = Number(Cookies.get("kanban_tab")) || 0;
const dispatch = useDispatch();
const setCurrentNameSpace = useSetAtom(currentNameSpace);
setCurrentNameSpace(props.nameSpace);
@@ -48,10 +54,28 @@ const KanbanPage: NextPage<{
type: ActionTypes.SET_AUTO_SAVE,
autoSave: false,
});
+
+ const [value, setValue] = React.useState(null);
+ const handleChange = (_event: React.SyntheticEvent, newValue: number) => {
+ document.cookie = `kanban_tab=${newValue}`;
+ setValue(newValue);
+ };
+
+ useEffect(() => setValue(kanban_tab), [kanban_tab]);
+
return (
<>
-
+
+
+
+ {value !== null &&
+ Object.keys(ReviewTaskTypeEnum).map((key, index) => (
+
+
+
+ ))}
+
>
);
diff --git a/src/pages/source-review.tsx b/src/pages/source-review.tsx
new file mode 100644
index 000000000..55cc16bbb
--- /dev/null
+++ b/src/pages/source-review.tsx
@@ -0,0 +1,155 @@
+import { ActionTypes } from "../store/types";
+import AffixButton from "../components/AffixButton/AffixButton";
+import { GetLocale } from "../utils/GetLocale";
+import { NextPage } from "next";
+import React from "react";
+import { ReviewTaskMachineProvider } from "../machines/reviewTask/ReviewTaskMachineProvider";
+import actions from "../store/actions";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+import { useDispatch } from "react-redux";
+import { useTranslation } from "next-i18next";
+import { VisualEditorProvider } from "../components/Collaborative/VisualEditorProvider";
+import { NameSpaceEnum } from "../types/Namespace";
+import { useSetAtom } from "jotai";
+import { currentNameSpace } from "../atoms/namespace";
+import { ReviewTaskTypeEnum } from "../machines/reviewTask/enums";
+import ClaimReviewView from "../components/ClaimReview/ClaimReviewView";
+import { ClassificationEnum } from "../types/enums";
+import JsonLd from "../components/JsonLd";
+
+export interface SourceReviewPageProps {
+ source: any;
+ sitekey: string;
+ reviewTask: any;
+ sourceReview: any;
+ hideDescriptions: object;
+ enableCollaborativeEditor: boolean;
+ enableCopilotChatBot: boolean;
+ enableEditorAnnotations: boolean;
+ enableAddEditorSourcesWithoutSelecting: boolean;
+ websocketUrl: string;
+ nameSpace: string;
+}
+
+const SourceReviewPage: NextPage = (props) => {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ const setCurrentNameSpace = useSetAtom(currentNameSpace);
+ setCurrentNameSpace(props.nameSpace as NameSpaceEnum);
+ const {
+ source,
+ sourceReview: claimReview,
+ sitekey,
+ enableCollaborativeEditor,
+ enableCopilotChatBot,
+ enableEditorAnnotations,
+ hideDescriptions,
+ } = props;
+
+ dispatch(actions.setWebsocketUrl(props.websocketUrl));
+ dispatch(actions.setSitekey(sitekey));
+ dispatch(actions.closeCopilotDrawer());
+ dispatch({
+ type: ActionTypes.SET_AUTO_SAVE,
+ autoSave: false,
+ });
+ dispatch({
+ type: ActionTypes.SET_COLLABORATIVE_EDIT,
+ enableCollaborativeEdit: enableCollaborativeEditor,
+ });
+ dispatch({
+ type: ActionTypes.SET_COPILOT_CHAT_BOT,
+ enableCopilotChatBot: enableCopilotChatBot,
+ });
+ dispatch({
+ type: ActionTypes.SET_EDITOR_ANNOTATION,
+ enableEditorAnnotations: enableEditorAnnotations,
+ });
+ dispatch({
+ type: ActionTypes.SET_ADD_EDITOR_SOURCES_WITHOUT_SELECTING,
+ enableAddEditorSourcesWithoutSelecting:
+ props.enableAddEditorSourcesWithoutSelecting,
+ });
+
+ const review = source?.props?.classification;
+
+ //TODO: Improve source review schema
+ const jsonld = {
+ "@context": "https://schema.org",
+ "@type": "MediaReview",
+ url: "https://aletheiafact.org",
+ author: {
+ "@type": "Organization",
+ url: "https://aletheiafact.org",
+ sameAs: [
+ "https://www.facebook.com/AletheiaFactorg-107521791638412",
+ "https://www.instagram.com/aletheiafact",
+ ],
+ },
+ originalMediaLink: source.href,
+ reviewRating: {
+ "@type": "Rating",
+ ratingValue: claimReview?.isHidden ? 0 : ClassificationEnum[review],
+ bestRating: 8,
+ worstRating: 1,
+ alternateName: claimReview?.isHidden
+ ? t("claimReviewForm:notReviewed")
+ : t(`claimReviewForm:${review}`),
+ },
+ itemReviewed: {
+ "@type": "CreativeWork",
+ isBasedOnUrl: source.href,
+ },
+ datePublished: claimReview?.date,
+ };
+
+ return (
+ <>
+ {review && }
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export async function getServerSideProps({ query, locale, locales, req }) {
+ locale = GetLocale(req, locale, locales);
+ return {
+ props: {
+ ...(await serverSideTranslations(locale)),
+ source: JSON.parse(JSON.stringify(query.source)),
+ reviewTask: JSON.parse(JSON.stringify(query.reviewTask)),
+ sourceReview: JSON.parse(JSON.stringify(query.claimReview)),
+ sitekey: query.sitekey,
+ hideDescriptions: JSON.parse(
+ JSON.stringify(query.hideDescriptions)
+ ),
+ enableCollaborativeEditor: query?.enableCollaborativeEditor,
+ enableCopilotChatBot: query?.enableCopilotChatBot,
+ enableEditorAnnotations: query?.enableEditorAnnotations,
+ enableAddEditorSourcesWithoutSelecting:
+ query?.enableAddEditorSourcesWithoutSelecting,
+ websocketUrl: query?.websocketUrl,
+ nameSpace: query.nameSpace ? query.nameSpace : NameSpaceEnum.Main,
+ },
+ };
+}
+export default SourceReviewPage;
diff --git a/src/pages/sources-create.tsx b/src/pages/sources-create.tsx
index e31511584..75527e115 100644
--- a/src/pages/sources-create.tsx
+++ b/src/pages/sources-create.tsx
@@ -8,7 +8,7 @@ import actions from "../store/actions";
import { GetLocale } from "../utils/GetLocale";
import { NameSpaceEnum } from "../types/Namespace";
import { currentNameSpace } from "../atoms/namespace";
-import CreateSourceView from "../components/Claim/CreateSource/CreateSourceView";
+import CreateSourceView from "../components/Source/CreateSource/CreateSourceView";
const CreateSourcesPage: NextPage = ({ sitekey, nameSpace }) => {
const { t } = useTranslation();
diff --git a/src/pages/verification-request-page.tsx b/src/pages/verification-request-page.tsx
new file mode 100644
index 000000000..b5cd7f44e
--- /dev/null
+++ b/src/pages/verification-request-page.tsx
@@ -0,0 +1,41 @@
+import { NextPage } from "next";
+import { useTranslation } from "next-i18next";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+
+import Seo from "../components/Seo";
+import { GetLocale } from "../utils/GetLocale";
+import { NameSpaceEnum } from "../types/Namespace";
+import { currentNameSpace } from "../atoms/namespace";
+import { useSetAtom } from "jotai";
+import AffixButton from "../components/AffixButton/AffixButton";
+import VerificationRequestList from "../components/VerificationRequest/VerificationRequestList";
+
+const VerificationRequestPage: NextPage<{ nameSpace }> = ({ nameSpace }) => {
+ const { t } = useTranslation();
+ const setCurrentNameSpace = useSetAtom(currentNameSpace);
+ setCurrentNameSpace(nameSpace);
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export async function getServerSideProps({ query, locale, locales, req }) {
+ locale = GetLocale(req, locale, locales);
+ return {
+ props: {
+ ...(await serverSideTranslations(locale)),
+ // Nextjs have problems with client re-hydration for some serialized objects
+ // This is a hack until a better solution https://github.com/vercel/next.js/issues/11993
+ href: req.protocol + "://" + req.get("host") + req.originalUrl,
+ nameSpace: query.nameSpace ? query.nameSpace : NameSpaceEnum.Main,
+ },
+ };
+}
+export default VerificationRequestPage;
diff --git a/src/pages/verification-request-review-page.tsx b/src/pages/verification-request-review-page.tsx
new file mode 100644
index 000000000..4b97f513d
--- /dev/null
+++ b/src/pages/verification-request-review-page.tsx
@@ -0,0 +1,69 @@
+import AffixButton from "../components/AffixButton/AffixButton";
+import { GetLocale } from "../utils/GetLocale";
+import { NextPage } from "next";
+import React from "react";
+import { ReviewTaskMachineProvider } from "../machines/reviewTask/ReviewTaskMachineProvider";
+import actions from "../store/actions";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+import { useDispatch } from "react-redux";
+import { NameSpaceEnum } from "../types/Namespace";
+import { useSetAtom } from "jotai";
+import { currentNameSpace } from "../atoms/namespace";
+import ClaimReviewView from "../components/ClaimReview/ClaimReviewView";
+import { ReviewTaskTypeEnum } from "../machines/reviewTask/enums";
+
+export interface SourceReviewPageProps {
+ verificationRequest: any;
+ sitekey: string;
+ reviewTask: any;
+ hideDescriptions: object;
+ websocketUrl: string;
+ nameSpace: string;
+}
+
+const SourceReviewPage: NextPage = (props) => {
+ const { verificationRequest, sitekey, hideDescriptions } = props;
+ const dispatch = useDispatch();
+ const setCurrentNameSpace = useSetAtom(currentNameSpace);
+ setCurrentNameSpace(props.nameSpace as NameSpaceEnum);
+ dispatch(actions.setWebsocketUrl(props.websocketUrl));
+ dispatch(actions.setSitekey(sitekey));
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export async function getServerSideProps({ query, locale, locales, req }) {
+ locale = GetLocale(req, locale, locales);
+ return {
+ props: {
+ ...(await serverSideTranslations(locale)),
+ verificationRequest: JSON.parse(
+ JSON.stringify(query.verificationRequest)
+ ),
+ reviewTask: JSON.parse(JSON.stringify(query.reviewTask)),
+ sitekey: query.sitekey,
+ hideDescriptions: JSON.parse(
+ JSON.stringify(query.hideDescriptions)
+ ),
+ websocketUrl: query?.websocketUrl,
+ nameSpace: query.nameSpace ? query.nameSpace : NameSpaceEnum.Main,
+ },
+ };
+}
+export default SourceReviewPage;
diff --git a/src/store/actions.ts b/src/store/actions.ts
index 199e59b51..e4081f5a8 100644
--- a/src/store/actions.ts
+++ b/src/store/actions.ts
@@ -38,14 +38,15 @@ const actions = {
type: ActionTypes.SET_SELECTED_PERSONALITY,
selectedPersonality: personality,
}),
- setSelectClaim: (claim) => ({
- type: ActionTypes.SET_SELECTED_CLAIM,
- selectedClaim: claim,
+ setSelectTarget: (target) => ({
+ type: ActionTypes.SET_SELECTED_TARGET,
+ selectedTarget: target,
}),
setSelectContent: (content: Content) => ({
type: ActionTypes.SET_SELECTED_CONTENT,
selectedContent: content,
selectedDataHash: content?.data_hash || "",
+ selectedReviewTaskType: content?.reviewTaskType,
}),
setSitekey: (sitekey) => ({
type: ActionTypes.SET_SITEKEY,
diff --git a/src/store/store.ts b/src/store/store.ts
index 95e190639..d450fa201 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -180,10 +180,10 @@ const reducer = (state, action) => {
...state,
selectedPersonality: action.selectedPersonality,
};
- case ActionTypes.SET_SELECTED_CLAIM:
+ case ActionTypes.SET_SELECTED_TARGET:
return {
...state,
- selectedClaim: action.selectedClaim,
+ selectedTarget: action.selectedTarget,
};
case ActionTypes.SET_SELECTED_CONTENT:
return {
diff --git a/src/store/types.ts b/src/store/types.ts
index 8c19f6650..4699ca6f1 100644
--- a/src/store/types.ts
+++ b/src/store/types.ts
@@ -20,7 +20,7 @@ export enum ActionTypes {
SET_CLAIM_CREATE_TYPE,
SET_CLAIM_CREATE_PERSONALITY,
SET_SELECTED_PERSONALITY,
- SET_SELECTED_CLAIM,
+ SET_SELECTED_TARGET,
SET_SELECTED_CONTENT,
SET_USER_ID,
SET_SITEKEY,
@@ -71,7 +71,7 @@ export interface RootState {
vw: WidthBreakpoints;
selectedDataHash: string;
selectedPersonality: any;
- selectedClaim: any;
+ selectedTarget: any;
selectedContent: Content;
sitekey: string;
}
diff --git a/src/types/Claim.ts b/src/types/Claim.ts
index 3ccbcbc4f..4d2d205ba 100644
--- a/src/types/Claim.ts
+++ b/src/types/Claim.ts
@@ -1,4 +1,5 @@
import { ContentModelEnum } from "./enums";
+import { Group } from "./Group";
import { Image } from "./Image";
import { NameSpaceEnum } from "./Namespace";
import { Personality } from "./Personality";
@@ -12,4 +13,5 @@ export type Claim = {
personalities?: Personality[];
recaptcha?: string;
nameSpace?: NameSpaceEnum;
+ group: Group;
};
diff --git a/src/types/Content.ts b/src/types/Content.ts
index 9be1d7826..92b97e68a 100644
--- a/src/types/Content.ts
+++ b/src/types/Content.ts
@@ -5,4 +5,5 @@ export interface Content {
data_hash: string;
props: any;
content: string;
+ reviewTaskType: string;
}
diff --git a/src/types/Group.ts b/src/types/Group.ts
new file mode 100644
index 000000000..30159047e
--- /dev/null
+++ b/src/types/Group.ts
@@ -0,0 +1,6 @@
+import { VerificationnRequest } from "./VerificationRequest";
+
+export type Group = {
+ _id: string;
+ content: VerificationnRequest[];
+};
diff --git a/src/types/Source.ts b/src/types/Source.ts
index e9a8c7de8..70889c5c0 100644
--- a/src/types/Source.ts
+++ b/src/types/Source.ts
@@ -1,5 +1,8 @@
export type SourceType = {
+ _id: string;
href: string;
+ nameSpace: string;
+ data_hash?: string;
props: {
field: string;
id: string;
diff --git a/src/types/VerificationRequest.ts b/src/types/VerificationRequest.ts
new file mode 100644
index 000000000..ef1de9e15
--- /dev/null
+++ b/src/types/VerificationRequest.ts
@@ -0,0 +1,9 @@
+export type VerificationRequest = {
+ data_hash: string;
+ content: string;
+ isSensitive: boolean;
+ rejected: boolean;
+ group: string[];
+ date: Date;
+ sources: string[];
+};
diff --git a/src/types/enums.ts b/src/types/enums.ts
index 5f76fd05d..e1fb5471f 100644
--- a/src/types/enums.ts
+++ b/src/types/enums.ts
@@ -35,7 +35,7 @@ export enum TargetModel {
Debate = "Debate",
Personality = "Personality",
ClaimReview = "ClaimReview",
- ClaimReviewTask = "ClaimReviewTask",
+ ReviewTask = "ReviewTask",
Image = "Image",
}
diff --git a/src/utils/GetClaimContentHref.ts b/src/utils/GetReviewContentHref.ts
similarity index 70%
rename from src/utils/GetClaimContentHref.ts
rename to src/utils/GetReviewContentHref.ts
index 9751bcb90..c443e545d 100644
--- a/src/utils/GetClaimContentHref.ts
+++ b/src/utils/GetReviewContentHref.ts
@@ -1,15 +1,25 @@
+import { ReviewTaskTypeEnum } from "../machines/reviewTask/enums";
import { NameSpaceEnum } from "../types/Namespace";
import { ContentModelEnum } from "../types/enums";
-export const generateClaimContentPath = (
+export const generateReviewContentPath = (
nameSpace,
personality,
claim,
contentModel,
- data_hash
+ data_hash,
+ reviewTaskType
) => {
const basePath = nameSpace !== NameSpaceEnum.Main ? `/${nameSpace}` : "";
+ if (reviewTaskType === ReviewTaskTypeEnum.Source) {
+ return `${basePath}/source/${data_hash}`;
+ }
+
+ if (reviewTaskType === ReviewTaskTypeEnum.VerificationRequest) {
+ return `${basePath}/verification-request/${data_hash}`;
+ }
+
switch (contentModel) {
case ContentModelEnum.Speech:
return `${basePath}/personality/${personality?.slug}/claim/${claim?.slug}/sentence/${data_hash}`;
diff --git a/src/utils/GetSentenceContentHref.ts b/src/utils/GetSentenceContentHref.ts
index c501f79f7..0779c064e 100644
--- a/src/utils/GetSentenceContentHref.ts
+++ b/src/utils/GetSentenceContentHref.ts
@@ -22,7 +22,7 @@ export const generateSentenceContentPath = (
}
if (isDebate) {
- path += "/debate";
+ path = `${basePath}/personality/${personality?.slug}/claim/${claim?.slug}`;
}
if (data_hash) {
diff --git a/yarn.lock b/yarn.lock
index 0b9aa8049..84c4d3814 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3486,17 +3486,31 @@ __metadata:
languageName: node
linkType: hard
-"@grpc/grpc-js@npm:^1.9.4":
- version: 1.9.9
- resolution: "@grpc/grpc-js@npm:1.9.9"
+"@grpc/grpc-js@npm:1.10.9":
+ version: 1.10.9
+ resolution: "@grpc/grpc-js@npm:1.10.9"
dependencies:
- "@grpc/proto-loader": ^0.7.8
- "@types/node": ">=12.12.47"
- checksum: 71183a483b4a302f6c09b81db282c2d58f2a10624f22f7891b8039f0cd18a65d0c55b729e2ec76beba6daccc9bcf905cf63e9d0959dfe62da456c6b7b731424c
+ "@grpc/proto-loader": ^0.7.13
+ "@js-sdsl/ordered-map": ^4.4.2
+ checksum: 88d91c227175275d8cc178c807d09510a83d947911c9bfe8ccd132cb27144c54508bcd114d52ab00b6e4f37eecf74aeee3ef3971900bdb90735d55a0b0dba761
languageName: node
linkType: hard
-"@grpc/proto-loader@npm:^0.7.5, @grpc/proto-loader@npm:^0.7.8":
+"@grpc/proto-loader@npm:^0.7.13":
+ version: 0.7.13
+ resolution: "@grpc/proto-loader@npm:0.7.13"
+ dependencies:
+ lodash.camelcase: ^4.3.0
+ long: ^5.0.0
+ protobufjs: ^7.2.5
+ yargs: ^17.7.2
+ bin:
+ proto-loader-gen-types: build/bin/proto-loader-gen-types.js
+ checksum: 399c1b8a4627f93dc31660d9636ea6bf58be5675cc7581e3df56a249369e5be02c6cd0d642c5332b0d5673bc8621619bc06fb045aa3e8f57383737b5d35930dc
+ languageName: node
+ linkType: hard
+
+"@grpc/proto-loader@npm:^0.7.5":
version: 0.7.10
resolution: "@grpc/proto-loader@npm:0.7.10"
dependencies:
@@ -3874,6 +3888,13 @@ __metadata:
languageName: node
linkType: hard
+"@js-sdsl/ordered-map@npm:^4.4.2":
+ version: 4.4.2
+ resolution: "@js-sdsl/ordered-map@npm:4.4.2"
+ checksum: a927ae4ff8565ecb75355cc6886a4f8fadbf2af1268143c96c0cce3ba01261d241c3f4ba77f21f3f017a00f91dfe9e0673e95f830255945c80a0e96c6d30508a
+ languageName: node
+ linkType: hard
+
"@juggle/resize-observer@npm:^3.3.1":
version: 3.4.0
resolution: "@juggle/resize-observer@npm:3.4.0"
@@ -10240,7 +10261,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0":
+"@types/node@npm:>=13.7.0":
version: 20.9.0
resolution: "@types/node@npm:20.9.0"
dependencies:
@@ -11593,7 +11614,7 @@ __metadata:
ai: ^3.1.1
antd: ^4.18.5
aws-sdk: ^2.1154.0
- axios: ^1.5.0
+ axios: ^1.6.0
babel-eslint: ^10.1.0
babel-jest: ^29.7.0
babel-loader: ^8.2.5
@@ -12185,13 +12206,6 @@ __metadata:
languageName: node
linkType: hard
-"async-limiter@npm:~1.0.0":
- version: 1.0.1
- resolution: "async-limiter@npm:1.0.1"
- checksum: 2b849695b465d93ad44c116220dee29a5aeb63adac16c1088983c339b0de57d76e82533e8e364a93a9f997f28bbfc6a92948cefc120652bd07f3b59f8d75cf2b
- languageName: node
- linkType: hard
-
"async-mutex@npm:^0.3.2":
version: 0.3.2
resolution: "async-mutex@npm:0.3.2"
@@ -12304,7 +12318,7 @@ __metadata:
languageName: node
linkType: hard
-"axios@npm:^1.4.0, axios@npm:^1.5.0":
+"axios@npm:^1.4.0":
version: 1.5.1
resolution: "axios@npm:1.5.1"
dependencies:
@@ -12315,6 +12329,17 @@ __metadata:
languageName: node
linkType: hard
+"axios@npm:^1.6.0":
+ version: 1.7.2
+ resolution: "axios@npm:1.7.2"
+ dependencies:
+ follow-redirects: ^1.15.6
+ form-data: ^4.0.0
+ proxy-from-env: ^1.1.0
+ checksum: e457e2b0ab748504621f6fa6609074ac08c824bf0881592209dfa15098ece7e88495300e02cd22ba50b3468fd712fe687e629dcb03d6a3f6a51989727405aedf
+ languageName: node
+ linkType: hard
+
"axios@npm:^1.6.1":
version: 1.6.8
resolution: "axios@npm:1.6.8"
@@ -12888,12 +12913,12 @@ __metadata:
languageName: node
linkType: hard
-"braces@npm:^3.0.2, braces@npm:~3.0.2":
- version: 3.0.2
- resolution: "braces@npm:3.0.2"
+"braces@npm:3.0.3":
+ version: 3.0.3
+ resolution: "braces@npm:3.0.3"
dependencies:
- fill-range: ^7.0.1
- checksum: e2a8e769a863f3d4ee887b5fe21f63193a891c68b612ddb4b68d82d1b5f3ff9073af066c343e9867a393fe4c2555dcb33e89b937195feb9c1613d259edfcd459
+ fill-range: ^7.1.1
+ checksum: b95aa0b3bd909f6cd1720ffcf031aeaf46154dd88b4da01f9a1d3f7ea866a79eba76a6d01cbc3c422b2ee5cdc39a4f02491058d5df0d7bf6e6a162a832df1f69
languageName: node
linkType: hard
@@ -14757,6 +14782,15 @@ __metadata:
languageName: node
linkType: hard
+"decompress-response@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "decompress-response@npm:6.0.0"
+ dependencies:
+ mimic-response: ^3.1.0
+ checksum: d377cf47e02d805e283866c3f50d3d21578b779731e8c5072d6ce8c13cc31493db1c2f6784da9d1d5250822120cefa44f1deab112d5981015f2e17444b763812
+ languageName: node
+ linkType: hard
+
"dedent@npm:^0.7.0":
version: 0.7.0
resolution: "dedent@npm:0.7.0"
@@ -14816,6 +14850,13 @@ __metadata:
languageName: node
linkType: hard
+"deep-extend@npm:^0.6.0":
+ version: 0.6.0
+ resolution: "deep-extend@npm:0.6.0"
+ checksum: 7be7e5a8d468d6b10e6a67c3de828f55001b6eb515d014f7aeb9066ce36bd5717161eb47d6a0f7bed8a9083935b465bc163ee2581c8b128d29bf61092fdf57a7
+ languageName: node
+ linkType: hard
+
"deep-is@npm:^0.1.3, deep-is@npm:~0.1.3":
version: 0.1.4
resolution: "deep-is@npm:0.1.4"
@@ -15356,14 +15397,14 @@ __metadata:
languageName: node
linkType: hard
-"ejs@npm:^3.1.8":
- version: 3.1.9
- resolution: "ejs@npm:3.1.9"
+"ejs@npm:3.1.10":
+ version: 3.1.10
+ resolution: "ejs@npm:3.1.10"
dependencies:
jake: ^10.8.5
bin:
ejs: bin/cli.js
- checksum: af6f10eb815885ff8a8cfacc42c6b6cf87daf97a4884f87a30e0c3271fedd85d76a3a297d9c33a70e735b97ee632887f85e32854b9cdd3a2d97edf931519a35f
+ checksum: ce90637e9c7538663ae023b8a7a380b2ef7cc4096de70be85abf5a3b9641912dde65353211d05e24d56b1f242d71185c6d00e02cb8860701d571786d92c71f05
languageName: node
linkType: hard
@@ -17005,12 +17046,12 @@ __metadata:
languageName: node
linkType: hard
-"fill-range@npm:^7.0.1":
- version: 7.0.1
- resolution: "fill-range@npm:7.0.1"
+"fill-range@npm:^7.1.1":
+ version: 7.1.1
+ resolution: "fill-range@npm:7.1.1"
dependencies:
to-regex-range: ^5.0.1
- checksum: cc283f4e65b504259e64fd969bcf4def4eb08d85565e906b7d36516e87819db52029a76b6363d0f02d0d532f0033c9603b9e2d943d56ee3b0d4f7ad3328ff917
+ checksum: b4abfbca3839a3d55e4ae5ec62e131e2e356bf4859ce8480c64c4876100f4df292a63e5bb1618e1d7460282ca2b305653064f01654474aa35c68000980f17798
languageName: node
linkType: hard
@@ -18589,6 +18630,13 @@ __metadata:
languageName: node
linkType: hard
+"ini@npm:~1.3.0":
+ version: 1.3.8
+ resolution: "ini@npm:1.3.8"
+ checksum: dfd98b0ca3a4fc1e323e38a6c8eb8936e31a97a918d3b377649ea15bdb15d481207a0dda1021efbd86b464cae29a0d33c1d7dcaf6c5672bee17fa849bc50a1b3
+ languageName: node
+ linkType: hard
+
"inline-style-parser@npm:0.1.1":
version: 0.1.1
resolution: "inline-style-parser@npm:0.1.1"
@@ -21794,6 +21842,13 @@ __metadata:
languageName: node
linkType: hard
+"mimic-response@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "mimic-response@npm:3.1.0"
+ checksum: 25739fee32c17f433626bf19f016df9036b75b3d84a3046c7d156e72ec963dd29d7fc8a302f55a3d6c5a4ff24259676b15d915aad6480815a969ff2ec0836867
+ languageName: node
+ linkType: hard
+
"min-document@npm:^2.19.0":
version: 2.19.0
resolution: "min-document@npm:2.19.0"
@@ -23456,6 +23511,17 @@ __metadata:
languageName: node
linkType: hard
+"packument@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "packument@npm:2.0.0"
+ dependencies:
+ registry-auth-token: ^4.2.1
+ registry-url: ^5.1.0
+ simple-get: ^4.0.1
+ checksum: ac6882ce3b8397797c23363bcb96dc98b5cfb6fe5b7ceaabf31824fca97a9540e90a102294dcb98c867d9713806357d1b76db8d05b8dceef941e67c9a1c39011
+ languageName: node
+ linkType: hard
+
"pako@npm:^0.2.5, pako@npm:~0.2.0":
version: 0.2.9
resolution: "pako@npm:0.2.9"
@@ -24374,6 +24440,27 @@ __metadata:
languageName: node
linkType: hard
+"protobufjs@npm:^7.2.5":
+ version: 7.3.1
+ resolution: "protobufjs@npm:7.3.1"
+ dependencies:
+ "@protobufjs/aspromise": ^1.1.2
+ "@protobufjs/base64": ^1.1.2
+ "@protobufjs/codegen": ^2.0.4
+ "@protobufjs/eventemitter": ^1.1.0
+ "@protobufjs/fetch": ^1.1.0
+ "@protobufjs/float": ^1.0.2
+ "@protobufjs/inquire": ^1.1.0
+ "@protobufjs/path": ^1.1.2
+ "@protobufjs/pool": ^1.1.0
+ "@protobufjs/utf8": ^1.1.0
+ "@types/node": ">=13.7.0"
+ long: ^5.0.0
+ packument: ^2.0.0
+ checksum: 81f5543a596a88a23b6966726df7be713df925d8a7990b23a7db2c91fb95397ed9a0c0e8422f484603c9e818e3ef098ad62fe81a7800656fb1f882dd33f973d0
+ languageName: node
+ linkType: hard
+
"proxy-addr@npm:~2.0.7":
version: 2.0.7
resolution: "proxy-addr@npm:2.0.7"
@@ -25217,6 +25304,20 @@ __metadata:
languageName: node
linkType: hard
+"rc@npm:1.2.8, rc@npm:^1.2.8":
+ version: 1.2.8
+ resolution: "rc@npm:1.2.8"
+ dependencies:
+ deep-extend: ^0.6.0
+ ini: ~1.3.0
+ minimist: ^1.2.0
+ strip-json-comments: ~2.0.1
+ bin:
+ rc: ./cli.js
+ checksum: 2e26e052f8be2abd64e6d1dabfbd7be03f80ec18ccbc49562d31f617d0015fbdbcf0f9eed30346ea6ab789e0fdfe4337f033f8016efdbee0df5354751842080e
+ languageName: node
+ linkType: hard
+
"react-async-script@npm:^1.1.1":
version: 1.2.0
resolution: "react-async-script@npm:1.2.0"
@@ -25860,6 +25961,24 @@ __metadata:
languageName: node
linkType: hard
+"registry-auth-token@npm:^4.2.1":
+ version: 4.2.2
+ resolution: "registry-auth-token@npm:4.2.2"
+ dependencies:
+ rc: 1.2.8
+ checksum: c5030198546ecfdcbcb0722cbc3e260c4f5f174d8d07bdfedd4620e79bfdf17a2db735aa230d600bd388fce6edd26c0a9ed2eb7e9b4641ec15213a28a806688b
+ languageName: node
+ linkType: hard
+
+"registry-url@npm:^5.1.0":
+ version: 5.1.0
+ resolution: "registry-url@npm:5.1.0"
+ dependencies:
+ rc: ^1.2.8
+ checksum: bcea86c84a0dbb66467b53187fadebfea79017cddfb4a45cf27530d7275e49082fe9f44301976eb0164c438e395684bcf3dae4819b36ff9d1640d8cc60c73df9
+ languageName: node
+ linkType: hard
+
"regjsparser@npm:^0.9.1":
version: 0.9.1
resolution: "regjsparser@npm:0.9.1"
@@ -26909,6 +27028,24 @@ __metadata:
languageName: node
linkType: hard
+"simple-concat@npm:^1.0.0":
+ version: 1.0.1
+ resolution: "simple-concat@npm:1.0.1"
+ checksum: 4d211042cc3d73a718c21ac6c4e7d7a0363e184be6a5ad25c8a1502e49df6d0a0253979e3d50dbdd3f60ef6c6c58d756b5d66ac1e05cda9cacd2e9fc59e3876a
+ languageName: node
+ linkType: hard
+
+"simple-get@npm:^4.0.1":
+ version: 4.0.1
+ resolution: "simple-get@npm:4.0.1"
+ dependencies:
+ decompress-response: ^6.0.0
+ once: ^1.3.1
+ simple-concat: ^1.0.0
+ checksum: e4132fd27cf7af230d853fa45c1b8ce900cb430dd0a3c6d3829649fe4f2b26574c803698076c4006450efb0fad2ba8c5455fbb5755d4b0a5ec42d4f12b31d27e
+ languageName: node
+ linkType: hard
+
"simple-peer@npm:^9.11.0":
version: 9.11.1
resolution: "simple-peer@npm:9.11.1"
@@ -27695,6 +27832,13 @@ __metadata:
languageName: node
linkType: hard
+"strip-json-comments@npm:~2.0.1":
+ version: 2.0.1
+ resolution: "strip-json-comments@npm:2.0.1"
+ checksum: 1074ccb63270d32ca28edfb0a281c96b94dc679077828135141f27d52a5a398ef5e78bcf22809d23cadc2b81dfbe345eb5fd8699b385c8b1128907dec4a7d1e1
+ languageName: node
+ linkType: hard
+
"strnum@npm:^1.0.5":
version: 1.0.5
resolution: "strnum@npm:1.0.5"
@@ -29954,9 +30098,9 @@ __metadata:
languageName: node
linkType: hard
-"ws@npm:8.13.0":
- version: 8.13.0
- resolution: "ws@npm:8.13.0"
+"ws@npm:8.17.1":
+ version: 8.17.1
+ resolution: "ws@npm:8.17.1"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ">=5.0.2"
@@ -29965,61 +30109,7 @@ __metadata:
optional: true
utf-8-validate:
optional: true
- checksum: 53e991bbf928faf5dc6efac9b8eb9ab6497c69feeb94f963d648b7a3530a720b19ec2e0ec037344257e05a4f35bd9ad04d9de6f289615ffb133282031b18c61c
- languageName: node
- linkType: hard
-
-"ws@npm:^6.1.0, ws@npm:^6.2.1":
- version: 6.2.2
- resolution: "ws@npm:6.2.2"
- dependencies:
- async-limiter: ~1.0.0
- checksum: aec3154ec51477c094ac2cb5946a156e17561a581fa27005cbf22c53ac57f8d4e5f791dd4bbba6a488602cb28778c8ab7df06251d590507c3c550fd8ebeee949
- languageName: node
- linkType: hard
-
-"ws@npm:^7.2.0, ws@npm:^7.5.9":
- version: 7.5.9
- resolution: "ws@npm:7.5.9"
- peerDependencies:
- bufferutil: ^4.0.1
- utf-8-validate: ^5.0.2
- peerDependenciesMeta:
- bufferutil:
- optional: true
- utf-8-validate:
- optional: true
- checksum: c3c100a181b731f40b7f2fddf004aa023f79d64f489706a28bc23ff88e87f6a64b3c6651fbec3a84a53960b75159574d7a7385709847a62ddb7ad6af76f49138
- languageName: node
- linkType: hard
-
-"ws@npm:^8.13.0, ws@npm:^8.2.3":
- version: 8.14.2
- resolution: "ws@npm:8.14.2"
- peerDependencies:
- bufferutil: ^4.0.1
- utf-8-validate: ">=5.0.2"
- peerDependenciesMeta:
- bufferutil:
- optional: true
- utf-8-validate:
- optional: true
- checksum: 3ca0dad26e8cc6515ff392b622a1467430814c463b3368b0258e33696b1d4bed7510bc7030f7b72838b9fdeb8dbd8839cbf808367d6aae2e1d668ce741d4308b
- languageName: node
- linkType: hard
-
-"ws@npm:~8.11.0":
- version: 8.11.0
- resolution: "ws@npm:8.11.0"
- peerDependencies:
- bufferutil: ^4.0.1
- utf-8-validate: ^5.0.2
- peerDependenciesMeta:
- bufferutil:
- optional: true
- utf-8-validate:
- optional: true
- checksum: 316b33aba32f317cd217df66dbfc5b281a2f09ff36815de222bc859e3424d83766d9eb2bd4d667de658b6ab7be151f258318fb1da812416b30be13103e5b5c67
+ checksum: 442badcce1f1178ec87a0b5372ae2e9771e07c4929a3180321901f226127f252441e8689d765aa5cfba5f50ac60dd830954afc5aeae81609aefa11d3ddf5cecf
languageName: node
linkType: hard