Compare commits

...

143 Commits

Author SHA1 Message Date
boojack dd6e2337e6 chore: update version to 0.8.2 (#722) 2022-12-10 13:23:38 +08:00
boojack 66418d4210 feat: get image only when cors error (#721) 2022-12-10 13:20:48 +08:00
boojack ab8c7b9d8a fix: auto complete in memo editor (#720) 2022-12-10 12:44:45 +08:00
M. Gschwandtner 387799b31c fix: added dark theme bg color to buttons (#719) 2022-12-10 12:14:02 +08:00
boojack 4a64a4dea8 fix: html lang attr (#718) 2022-12-10 10:42:10 +08:00
M. Gschwandtner 964c58ac01 feat: responsive layout for create shortcut dialog (#717) 2022-12-10 10:17:47 +08:00
boojack 56716cdad4 fix: break word (#708)
* fix: break word

* chore: update
2022-12-09 08:31:45 +08:00
Shruti Chaturvedi a2ee750d1e fix: bump Version of reusable.yaml (#707)
Bump version

Bump up the version for reusable.yaml to v2
2022-12-09 00:31:00 +08:00
Shruti Chaturvedi 3f0601f651 feat: add Uffizzi Integration (#655)
* Integrate Uffizzi

* Update docker-compose.uffizzi.yml

Start memos in dev mode

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-12-08 23:45:23 +08:00
Zeng1998 6f8e3432e9 fix: correct priority of keys in editor (#703) 2022-12-08 18:46:43 +08:00
Stephen Zhou b7ab6f8e7e fix: code highlight in dark mode (#702) 2022-12-08 18:30:46 +08:00
Zeng1998 36b92ad884 feat: auto continuation list in editor (#689)
* feat: auto continuation list in editor

* update

* update
2022-12-08 10:01:01 +08:00
apixandru 4d9857ce18 fix: update UsageHeatMap.tsx to account for daylight savings (#696) 2022-12-08 08:24:36 +08:00
boojack 43b22ce55f chore: fix typo (#695) 2022-12-07 22:49:05 +08:00
Zeng1998 147185309c feat: vacuum database in setting (#694)
* feat: vacuum database in setting

* update

* update

* update

* update
2022-12-07 22:45:47 +08:00
Maurice Bauer f48226d4f2 chore: update German translation (#691)
Switched to plural to make the difference between TAG(S) and TAG(E) visible...
Refers to #545
2022-12-07 20:18:35 +08:00
Zeng1998 e92407d9ec feat: image preview enhancement (#682) 2022-12-06 09:38:01 +08:00
Jasper Platenburg 79bf365d78 Dutch locale (#687) 2022-12-06 08:11:21 +08:00
Maurice Bauer 492a1370ab feat: add German i18n item (#686) 2022-12-06 07:42:17 +08:00
boojack e3ddf93c4d chore: update demo image (#672) 2022-12-04 20:43:46 +08:00
boojack 4a9314c476 chore: rename enableFoldMemo (#671)
* chore: rename `enableFoldMemo`

* chore: update
2022-12-04 15:34:03 +08:00
boojack 4767ee3293 feat: support follow system appearance (#670) 2022-12-04 12:23:29 +08:00
boojack 1ea74dfd0d chore: remove table syntax (#669) 2022-12-04 10:52:11 +08:00
Andreas Backström 53cf6ebb79 feat: add swedish/svenska translation (#668)
Add swedish / svenska translation
2022-12-03 21:38:25 +08:00
boojack d1007950e0 chore: remove emoji picker (#667) 2022-12-03 15:28:37 +08:00
Zeng1998 331226ec68 chore: fix some typos of README (#666) 2022-12-03 15:01:27 +08:00
boojack a7374cf998 fix: generate sharing memo image (#663) 2022-12-03 09:41:52 +08:00
boojack e3d76193b9 chore: update global css (#658) 2022-12-02 22:00:03 +08:00
boojack 07f0c3f052 chore: update global css (#657) 2022-12-02 21:34:43 +08:00
boojack a467a7c173 feat: upgrade dev version to 0.8.1 (#656)
* feat: upgrade version to `0.8.1`

* chore: update
2022-12-02 21:09:11 +08:00
boojack 14f9f29348 chore: update user setting appearance (#654) 2022-12-02 20:00:34 +08:00
EINDEX 5451fd2d2c feat: add a product of logseq plugin (#652) 2022-12-02 19:46:39 +08:00
hoi-lau f092771ea1 fix: resource-container overflow (#649) 2022-12-02 19:45:22 +08:00
boojack 7c6d7226f5 feat: update appearance selector (#645) 2022-12-01 20:57:19 +08:00
Stephen Zhou eaebc6dcef fix: apperance can not auto switch (#644) 2022-12-01 19:39:58 +08:00
boojack c5200ca31b feat: dark mode for dialogs (#643) 2022-11-30 20:34:16 +08:00
Tiefseemonster 1078132b12 fix: member menu dropdown position (#639)
* fix: member menu dropdown position

* chore: cleanup

* chore: cleanup
2022-11-30 20:18:39 +08:00
Stephen Zhou 6b058cd299 feat: save folding option with localstorage (#641)
* fix: change folding option need reload

* fix: floding option undefied
2022-11-30 19:13:55 +08:00
Wence b8f24af5ae feat: dynamic lazy loading route with simple loading page (#632)
* feat: dynamic loading route with simple loading page

* fix: lint fix

* Update web/src/less/loading.less

Co-authored-by: boojack <stevenlgtm@gmail.com>

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-29 22:13:22 +00:00
boojack 6384f5af74 feat: dark mode for main pages (#637)
* feat: update dark mode styles for auth and explore page

* feat: dark mode for home page
2022-11-29 21:44:52 +08:00
Zeng1998 52038d26d2 chore: update i18n for validator message (#636) 2022-11-29 21:35:40 +08:00
boojack 55f37664ef chore: add theme file for joyui (#635) 2022-11-29 20:15:16 +08:00
Zeng1998 ab8e3473a1 feat: support resources reuse (#620)
* feat: support resource reuse

* update

* update

* update

* update
2022-11-29 19:07:20 +08:00
Zeng1998 eba23c4f6e fix: add validation for user information update (#633) 2022-11-29 19:03:29 +08:00
Zeng1998 00fe6d3862 chore: add joyui tooltip for resources dialog (#630) 2022-11-29 09:36:48 +08:00
Charles Chin 3646b8f5dd docs: update readme (#628) 2022-11-29 06:34:44 +08:00
boojack d40639bf8e chore: update readme me with contributors graph (#626) 2022-11-28 22:48:02 +08:00
Zeng1998 12b81781b9 feat: share memo dialog (#618)
* feat: new share dialog

* update

* update

* update

* update
2022-11-28 21:20:35 +08:00
Zeng1998 b67a37453d feat: member management enhancement (#617)
* feat: member management enhancement

* update

* update

* update

* update
2022-11-28 19:59:11 +08:00
boojack b04e001db1 fix: image url host missing (#623) 2022-11-28 19:52:03 +08:00
Stephen Zhou fbe7b604ef feat: dark mode support for memo detail (#604)
* feat: dark mode support for memo detail

* chore: update

* chore: update

* chore: update
2022-11-28 19:40:08 +08:00
Zeng1998 0402cb7b27 fix: no user settings returns when patch user (#622) 2022-11-28 19:38:44 +08:00
Tiefseemonster b72bfc9c24 fix: selector dropdown position in fullscreen mod (#619) 2022-11-28 19:36:42 +08:00
Zeng1998 40e92f9463 fix: change password max length validation (#616) 2022-11-28 19:33:14 +08:00
Zeng1998 f883dd9c1d feat: create user repeat password (#614)
* feat: create user repeat password

* update
2022-11-28 19:32:53 +08:00
Wujiao233 d8bf55efb2 fix: shoutcut tag filter handle mutiple tags (#608)
* fix: shoutcut tag filter handle mutiple tags

* not edit parser
2022-11-28 19:32:01 +08:00
Wujiao233 f982e83d0a fix: clear shortcut filter when delete this shortcut (#611) 2022-11-28 06:14:25 +08:00
Jasper Platenburg 3472a6db26 fix: password field visible (#609) 2022-11-27 21:53:10 +08:00
boojack c79e51a91b chore: update readme (#606) 2022-11-27 16:04:35 +08:00
boojack ce795a2a7d chore: show content image (#602) 2022-11-27 09:01:19 +08:00
boojack 045819c312 fix: initial database schema (#601) 2022-11-27 08:52:43 +08:00
Tiefseemonster 2fa01886da fix: tooltip overlaps a window border (#599) 2022-11-27 08:48:21 +08:00
Tiefseemonster dd7d322c47 chore: add .vscode to gitignore (#596) 2022-11-27 07:56:19 +08:00
Tiefseemonster dfe71f33c2 fix: search bar dropdown disappearing (#593) 2022-11-26 23:13:02 +08:00
boojack db1d223448 fix: apperance select (#585) 2022-11-26 17:45:16 +08:00
Zeng1998 54271c1598 chore: fix some typos (#587) 2022-11-26 06:23:29 +00:00
Zeng1998 1ee8ebc9e1 fix: collapse btn cursor style (#586) 2022-11-26 05:12:37 +00:00
Stephen Zhou 6e5537d131 feat: dark mode support for explore page (#584)
* feat: dark mode support for auth page

* chore: update

* feat: dark mode support for explore page (#583)

* fix: avoid white text

* fix: import order
2022-11-26 12:19:00 +08:00
Stephen Zhou 90c85103c3 feat: dark mode support for auth page (#569)
* feat: dark mode support for auth page

* chore: update
2022-11-26 11:20:22 +08:00
Zeng1998 2d5d734da4 chore: update i18n for account settings (#582) 2022-11-26 02:23:57 +00:00
Zeng1998 e1e5121dd7 fix: get markdown image from backend (#581) 2022-11-26 10:20:49 +08:00
boojack 85db6721de chore: disable metrics collector (#580) 2022-11-26 09:23:38 +08:00
boojack b511a7b634 fix: user patch with empty email (#578) 2022-11-25 22:34:24 +08:00
boojack 88c3b1ad0f feat: update prod version (#577) 2022-11-25 22:17:24 +08:00
boojack 9fe15a03b3 chore(revert): update test image platforms (#576)
Revert "chore: update test image platforms (#575)"

This reverts commit 500afffbcf.
2022-11-25 22:08:43 +08:00
boojack 500afffbcf chore: update test image platforms (#575)
chore: update test image plantforms
2022-11-25 22:06:40 +08:00
Zeng1998 5f3cade810 feat: highlight the searched text in memo content (#514)
* feat: highlight the searched text in memo content

* update

* update

* update

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-25 21:59:21 +08:00
boojack 072851e3ba chore: update dialog style (#574)
* chore: update tests

* chore: update dialog styles
2022-11-25 21:55:45 +08:00
boojack e2cbea2022 chore: update action name (#573) 2022-11-25 21:53:36 +08:00
boojack 29880be283 chore: update tests (#572) 2022-11-25 21:51:53 +08:00
Tiefseemonster f4e2b7319a fix: make tooltip text no-selecting (#567)
* no selecting tooltip text

With a double click on button, you can get selection on tooltip text. That may be distracting a little bit.

* Update web/src/less/memo.less

Co-authored-by: Stephen Zhou <hi@hyoban.cc>

Co-authored-by: Stephen Zhou <hi@hyoban.cc>
Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-25 20:32:59 +08:00
Zeng1998 ff0db82dc3 chore: empty query text (#566) 2022-11-25 20:09:42 +08:00
Stephen Zhou 2daf085ce4 chore: fix vscode settings name (#565) 2022-11-25 01:51:43 +00:00
Tiefseemonster 495f1f2041 chore: fix name field in paragraph parser obj (#564) 2022-11-25 01:47:17 +00:00
boojack 89179f78c2 chore: add SECURITY.md (#562) 2022-11-25 09:05:52 +08:00
boojack 7ad7461a92 chore: add test docker image (#559) 2022-11-24 20:54:22 +08:00
boojack 8cfcd201b0 feat: support display video (#558) 2022-11-24 20:22:54 +08:00
boojack abcd3cfafb feat: add Strikethrough syntax (#557)
feat: add `Strikethrough` rule
2022-11-24 20:05:51 +08:00
boojack 50d41c456b chore: change memo created time (#556) 2022-11-24 19:55:52 +08:00
Zeng1998 1d41d53723 fix: icon style (#555) 2022-11-24 18:44:47 +08:00
boojack 0dc003854f chore: fix typo (#551) 2022-11-24 10:03:00 +08:00
Zeng1998 a4b7a77016 fix: cursor style (#549) 2022-11-24 09:30:17 +08:00
boojack 939e836d1b chore: update readme (#548) 2022-11-24 08:45:36 +08:00
boojack a0b35f7aa9 feat: add French i18n item (#547)
feat: add French i18n
2022-11-24 08:42:38 +08:00
baptiste313 574e160a11 chore: addition of the French translation and small correction for the English (#546) 2022-11-24 08:03:16 +08:00
boojack 2042737004 feat: add username field (#544)
* feat: add username field

* chore: update
2022-11-23 22:27:21 +08:00
boojack a0667abec8 chore: update data initial requests (#538) 2022-11-22 23:45:11 +08:00
boojack 362306a9cb chore: update i18n for visibility (#537) 2022-11-22 22:59:54 +08:00
Zeng1998 5e069f79a2 feat: esc to cancel editing (#532) 2022-11-22 19:54:25 +08:00
Zeng1998 7c78fb064f fix: empty filter value (#529) 2022-11-22 11:31:37 +00:00
Zeng1998 11a7e897dc fix: update issue template (#530) 2022-11-22 19:30:04 +08:00
Yarden Shoham d8adc0afe9 feat: extend validation config max length (#523) 2022-11-22 18:21:12 +08:00
boojack 17c1b97d23 fix: tag regexp (#518)
* fix: tag regexp

* chore: update
2022-11-21 23:49:52 +08:00
boojack 5aee2f46ab chore: update issue template (#517) 2022-11-21 23:38:21 +08:00
boojack 013ded1e04 chore: code clean (#516) 2022-11-21 23:23:05 +08:00
Zeng1998 0d0f893b93 chore: update i18 for memo sharing (#513) 2022-11-21 19:59:19 +08:00
Zeng1998 d2a87a4ddb chore: bgcolor of memo share image (#508) 2022-11-21 18:38:32 +08:00
boojack b05a176490 chore: add license to README (#506) 2022-11-21 07:20:36 +08:00
boojack d149926a39 chore: update seed data (#507) 2022-11-21 07:17:28 +08:00
boojack 2d49e96a8a feat: get image blob in backend (#495)
* feat: get image blob in backend

* chore: update
2022-11-19 18:43:56 +08:00
boojack 9036bd478b fix: image scrollbar (#494) 2022-11-19 17:36:25 +08:00
boojack 477130aa85 chore: update db filesize access control (#493) 2022-11-19 17:07:40 +08:00
boojack 878e0eabc8 feat: add crawler plugin (#492)
* feat: add crawler plugin

* chore: update

* chore: go mod tidy

* chore: update
2022-11-19 16:58:55 +08:00
boojack 62f63d4af7 chore: update dependencies version (#491) 2022-11-19 14:51:28 +08:00
Zeng1998 1acf2f8b13 chore: update i18 for settings (#490) 2022-11-19 10:33:52 +08:00
boojack a4a5e539ed chore: update dev version (#489) 2022-11-19 09:57:54 +08:00
boojack a2831b37c4 feat: add database filesize in UI (#488) 2022-11-18 21:17:52 +08:00
boojack 706b1b428f chore: add toast to system settings (#486) 2022-11-17 21:37:57 +08:00
boojack 1690566413 chore: update emoji picker (#483) 2022-11-17 21:11:58 +08:00
boojack a24b885236 chore: update share memo image (#482) 2022-11-17 21:01:26 +08:00
Stephen Zhou c89a6665b6 feat: fold memos according to horizontal rule (#478)
* feat: fold memos according to horizontal rule

* fix: button and content

* chore: update
2022-11-15 14:49:08 +00:00
Stephen Zhou 797accbc2c feat: parser for horizontal rule (#477)
* feat: parser for horizontal rule

* chore: revert
2022-11-15 21:56:44 +08:00
boojack 66f9bc48bb chore: remove mark memo (#476) 2022-11-15 21:22:08 +08:00
boojack de2eff474c chore: remove memo card dialog (#475) 2022-11-15 21:16:53 +08:00
Mahoo Huang 67195859dc fix: abnormal link regex (#474) 2022-11-15 21:15:10 +08:00
boojack 61abc6dd11 feat: update sharing memo image (#473) 2022-11-15 21:02:55 +08:00
boojack 9fe9245727 chore: update cancel edit button style (#472) 2022-11-15 20:35:56 +08:00
Zeng1998 906ec7994e fix: clear query text (#469)
* fix: clear query text

* update

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-15 19:52:16 +08:00
Zeng1998 5258b0a5b4 chore: change memo card's bg color (#471) 2022-11-15 18:55:11 +08:00
boojack ceac53b6d0 feat: additional script system setting (#467) 2022-11-14 22:21:19 +08:00
Stephen Zhou 3775d5c9c2 feat: folding options (#459)
* feat: folding options

* chore: update

* chore: update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-14 22:06:05 +08:00
Zeng1998 da80d4ef62 fix: duration query string (#465) 2022-11-14 18:57:43 +08:00
Stephen Zhou 205ad0fd6d chore: change rss item title (#464) 2022-11-14 13:28:50 +08:00
Zeng1998 d208731f5f feat: visibility click filter (#463)
* feat: visibility click filter

* update

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-13 21:23:40 +08:00
Stephen Zhou a90acdabb3 fix: route confusion entering from non-home page (#460)
Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-13 19:40:16 +08:00
Zeng1998 407d1cdcaa feat: add visibility filter (#461)
* feat: add visibility filter

* update
2022-11-13 19:34:22 +08:00
boojack cb50bbc3cb refactor: remove mixin colors (#458) 2022-11-13 14:25:02 +08:00
boojack 2743268fd7 chore: remove unused visibility selector (#457) 2022-11-13 10:47:21 +08:00
Zeng1998 8cc0977a01 fix: image url extraction (#453)
fix: image-url-extraction

Co-authored-by: boojack <stevenlgtm@gmail.com>
2022-11-12 21:25:50 +08:00
boojack 0809ec8c72 chore: update editor style (#456) 2022-11-12 21:22:51 +08:00
boojack db93710f85 chore: remove mobile editor style user setting (#455) 2022-11-12 21:01:34 +08:00
boojack 241c93c6b7 feat: update editor style (#454)
* feat: update editor style

* chore: update bg
2022-11-12 20:57:08 +08:00
boojack bf07ab9e2f fix: remove duplicate tag (#450)
fix: remove dumplicate tag
2022-11-12 11:31:16 +08:00
203 changed files with 4512 additions and 3374 deletions
+3 -10
View File
@@ -20,19 +20,12 @@ body:
Provide the steps to reproduce the issue.
placeholder: |
1. Go to '...'
3. Click on '....'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: |
Describe what you expected to happen.
2. Click on '....'
3. See error
validations:
required: true
- type: textarea
attributes:
label: Screenshots or additional context
description: |
Add screenshots or any other context about the problem here.
Add screenshots or any other context about the problem.
+1 -1
View File
@@ -25,4 +25,4 @@ body:
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
description: Add any other context or screenshots about the feature request.
@@ -0,0 +1,36 @@
name: build-and-push-test-image
on:
push:
branches: [main]
jobs:
build-and-push-test-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: neosmemo
password: ${{ secrets.DOCKER_NEOSMEMO_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
with:
install: true
- name: Build and Push
id: docker_build
uses: docker/build-push-action@v3
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: neosmemo/memos:test
-2
View File
@@ -17,8 +17,6 @@ on:
pull_request:
# The branches below must be a subset of the branches above
branches: [main]
schedule:
- cron: "27 12 * * 0"
jobs:
analyze:
+23 -23
View File
@@ -9,32 +9,13 @@ on:
branches: [main]
jobs:
go-static-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.18
check-latest: true
cache: true
- name: Verify go.mod is tidy
run: |
go mod tidy
git diff --exit-code
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
args: -v
skip-cache: true
eslint-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
@@ -49,7 +30,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
@@ -64,7 +45,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: "18"
cache: yarn
cache-dependency-path: "web/yarn.lock"
- run: yarn
@@ -73,13 +54,32 @@ jobs:
run: yarn build
working-directory: web
go-static-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19
check-latest: true
cache: true
- name: Verify go.mod is tidy
run: |
go mod tidy -go=1.19
git diff --exit-code
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
args: -v
skip-cache: true
go-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.19
check-latest: true
cache: true
- name: Run all tests
+97
View File
@@ -0,0 +1,97 @@
name: Build PR Image
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
build-memos:
name: Build and push `Memos`
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.meta.outputs.tags }}
if: ${{ github.event.action != 'closed' }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_WORKER=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: registry.uffizzi.com/${{ env.UUID_WORKER }}
tags: |
type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com - Uffizzi's ephemeral Registry
uses: docker/build-push-action@v3
with:
context: ./
file: Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: true
cache-from: type=gha
cache-to: type=gha, mode=max
render-compose-file:
name: Render Docker Compose File
# Pass output of this workflow to another triggered by `workflow_run` event.
runs-on: ubuntu-latest
needs:
- build-memos
outputs:
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Render Compose File
run: |
MEMOS_IMAGE=${{ needs.build-memos.outputs.tags }}
export MEMOS_IMAGE
# Render simple template from environment variables.
envsubst < docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2
delete-preview:
name: Call for Preview Deletion
runs-on: ubuntu-latest
if: ${{ github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2
+86
View File
@@ -0,0 +1,86 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "Build PR Image"
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
outputs:
compose-file-cache-key: ${{ env.HASH }}
pr-number: ${{ env.PR_NUMBER }}
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
with:
path: docker-compose.rendered.yml
key: ${{ env.HASH }}
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Compose file hash: ${{ env.HASH }}"
cat event.json
deploy-uffizzi-preview:
name: Use Remote Workflow to Preview on Uffizzi
needs:
- cache-compose-file
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:
contents: read
pull-requests: write
id-token: write
+3
View File
@@ -15,4 +15,7 @@ build
# Jetbrains
.idea
# vscode
.vscode
bin/air
+3 -3
View File
@@ -1,5 +1,5 @@
# Build frontend dist.
FROM node:16.15.0-alpine AS frontend
FROM node:18.12.1-alpine3.16 AS frontend
WORKDIR /frontend-build
COPY ./web/ .
@@ -8,7 +8,7 @@ RUN yarn
RUN yarn build
# Build backend exec file.
FROM golang:1.18.3-alpine3.16 AS backend
FROM golang:1.19.3-alpine3.16 AS backend
WORKDIR /backend-build
RUN apk update
@@ -20,7 +20,7 @@ COPY --from=frontend /frontend-build/dist ./server/dist
RUN go build -o memos ./bin/server/main.go
# Make workspace with above generated files.
FROM alpine:3.16.0 AS monolithic
FROM alpine:3.16 AS monolithic
WORKDIR /usr/local/memos
COPY --from=backend /backend-build/memos /usr/local/memos/
+20 -8
View File
@@ -1,6 +1,6 @@
<p align="center"><a href="https://usememos.com"><img height="64px" src="https://raw.githubusercontent.com/usememos/memos/main/resources/logo-full.webp" alt="✍️ memos" /></a></p>
<p align="center">An open-source, self-hosted memo hub for knowledge management and collaboration.</p>
<p align="center">An open-source, self-hosted memo hub with knowledge management and socialization.</p>
<p align="center">
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" /></a>
@@ -10,7 +10,7 @@
<p align="center">
<a href="https://demo.usememos.com/">Live Demo</a> •
<a href="https://t.me/+-_tNF1k70UU4ZTc9">Discuss in Telegram 👾</a>
Discuss in <a href="https://t.me/+-_tNF1k70UU4ZTc9">Telegram</a> / <b><a href="https://discord.gg/tfPJa4UmAv">Discord 🏂</a></b>
</p>
![demo](https://raw.githubusercontent.com/usememos/memos/main/resources/demo.webp)
@@ -19,8 +19,8 @@
- 🦄 Open source and free forever;
- 🚀 Support for self-hosting with `Docker` in seconds;
- 📜 Plain textarea first and support some useful markdown syntax;
- 👥 Collaborate and share with your teammates;
- 📜 Plain textarea first and support some useful Markdown syntax;
- 👥 Set memo private or public to others;
- 🧑‍💻 RESTful API for self-service.
## Deploy with Docker in seconds
@@ -31,7 +31,7 @@
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos neosmemo/memos:latest
```
Memos should be running at [http://localhost:5230](http://localhost:5230). If the `~/.memos/` does not have a `memos_prod.db` file, then memos will auto generate it.
If the `~/.memos/` does not have a `memos_prod.db` file, then memos will auto generate it. Memos will be running at [http://localhost:5230](http://localhost:5230).
### Docker Compose
@@ -45,16 +45,28 @@ docker-compose down && docker image rm neosmemo/memos:latest && docker-compose u
## Contribute
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. 🥰
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. 🥰
See more in [development guide](https://github.com/usememos/memos/tree/main/docs/development.md).
## Community Products
## Products made by Community
- [Moe Memos](https://memos.moe/) - Third party client for iOS and Android
- [lmm214/memos-bber](https://github.com/lmm214/memos-bber) - Chrome extension
- [Rabithua/memos_wmp](https://github.com/Rabithua/memos_wmp) - Wechat miniprogram
- [Rabithua/memos_wmp](https://github.com/Rabithua/memos_wmp) - WeChat MiniProgram
- [qazxcdswe123/telegramMemoBot](https://github.com/qazxcdswe123/telegramMemoBot) - Telegram bot
- [eallion/memos.top](https://github.com/eallion/memos.top) - A static page rendered with the Memos API
- [eindex/logseq-memos-sync](https://github.com/EINDEX/logseq-memos-sync) - A Logseq plugin
### Join the community to build memos together!
<a href="https://github.com/usememos/memos/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usememos/memos" />
</a>
## License
This project is open source and available under the [MIT License](https://github.com/usememos/memos/blob/main/LICENSE).
## Star history
+7
View File
@@ -0,0 +1,7 @@
# Security Policy
## Reporting a bug
Report security bugs via GitHub [issues](https://github.com/usememos/memos/issues).
For more information, please contact [stevenlgtm@gmail.com](stevenlgtm@gmail.com).
+3 -4
View File
@@ -1,13 +1,12 @@
package api
type Signin struct {
Email string `json:"email"`
Username string `json:"username"`
Password string `json:"password"`
}
type Signup struct {
Email string `json:"email"`
Role Role `json:"role"`
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
Role Role `json:"role"`
}
+3 -3
View File
@@ -8,8 +8,8 @@ const (
Public Visibility = "PUBLIC"
// Protected is the PROTECTED visibility.
Protected Visibility = "PROTECTED"
// Privite is the PRIVATE visibility.
Privite Visibility = "PRIVATE"
// Private is the PRIVATE visibility.
Private Visibility = "PRIVATE"
)
func (e Visibility) String() string {
@@ -18,7 +18,7 @@ func (e Visibility) String() string {
return "PUBLIC"
case Protected:
return "PROTECTED"
case Privite:
case Private:
return "PRIVATE"
}
return "PRIVATE"
+6 -2
View File
@@ -3,11 +3,15 @@ package api
import "github.com/usememos/memos/server/profile"
type SystemStatus struct {
Host *User `json:"host"`
Profile *profile.Profile `json:"profile"`
Host *User `json:"host"`
Profile profile.Profile `json:"profile"`
DBSize int64 `json:"dbSize"`
// System settings
// Allow sign up.
AllowSignUp bool `json:"allowSignUp"`
// Additional style.
AdditionalStyle string `json:"additionalStyle"`
// Additional script.
AdditionalScript string `json:"additionalScript"`
}
+10
View File
@@ -12,6 +12,8 @@ const (
SystemSettingAllowSignUpName SystemSettingName = "allowSignUp"
// SystemSettingAdditionalStyleName is the key type of additional style.
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
// SystemSettingAdditionalScriptName is the key type of additional script.
SystemSettingAdditionalScriptName SystemSettingName = "additionalScript"
)
func (key SystemSettingName) String() string {
@@ -20,6 +22,8 @@ func (key SystemSettingName) String() string {
return "allowSignUp"
case SystemSettingAdditionalStyleName:
return "additionalStyle"
case SystemSettingAdditionalScriptName:
return "additionalScript"
}
return ""
}
@@ -65,6 +69,12 @@ func (upsert SystemSettingUpsert) Validate() error {
if err != nil {
return fmt.Errorf("failed to unmarshal system setting additional style value")
}
} else if upsert.Name == SystemSettingAdditionalScriptName {
value := ""
err := json.Unmarshal([]byte(upsert.Value), &value)
if err != nil {
return fmt.Errorf("failed to unmarshal system setting additional script value")
}
} else {
return fmt.Errorf("invalid system setting name")
}
+21 -18
View File
@@ -2,8 +2,6 @@ package api
import (
"fmt"
"github.com/usememos/memos/common"
)
// Role is the type of a role.
@@ -12,6 +10,8 @@ type Role string
const (
// Host is the HOST role.
Host Role = "HOST"
// Admin is the ADMIN role.
Admin Role = "ADMIN"
// NormalUser is the USER role.
NormalUser Role = "USER"
)
@@ -20,6 +20,8 @@ func (e Role) String() string {
switch e {
case Host:
return "HOST"
case Admin:
return "ADMIN"
case NormalUser:
return "USER"
}
@@ -35,9 +37,10 @@ type User struct {
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Email string `json:"email"`
Username string `json:"username"`
Role Role `json:"role"`
Name string `json:"name"`
Email string `json:"email"`
Nickname string `json:"nickname"`
PasswordHash string `json:"-"`
OpenID string `json:"openId"`
UserSettingList []*UserSetting `json:"userSettingList"`
@@ -45,23 +48,21 @@ type User struct {
type UserCreate struct {
// Domain specific fields
Email string `json:"email"`
Username string `json:"username"`
Role Role `json:"role"`
Name string `json:"name"`
Email string `json:"email"`
Nickname string `json:"nickname"`
Password string `json:"password"`
PasswordHash string
OpenID string
}
func (create UserCreate) Validate() error {
if !common.ValidateEmail(create.Email) {
return fmt.Errorf("invalid email format")
if len(create.Username) < 4 {
return fmt.Errorf("username is too short, minimum length is 4")
}
if len(create.Email) < 6 {
return fmt.Errorf("email is too short, minimum length is 6")
}
if len(create.Password) < 6 {
return fmt.Errorf("password is too short, minimum length is 6")
if len(create.Password) < 4 {
return fmt.Errorf("password is too short, minimum length is 4")
}
return nil
@@ -75,8 +76,9 @@ type UserPatch struct {
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Username *string `json:"username"`
Email *string `json:"email"`
Name *string `json:"name"`
Nickname *string `json:"nickname"`
Password *string `json:"password"`
ResetOpenID *bool `json:"resetOpenId"`
PasswordHash *string
@@ -90,10 +92,11 @@ type UserFind struct {
RowStatus *RowStatus `json:"rowStatus"`
// Domain specific fields
Email *string `json:"email"`
Role *Role
Name *string `json:"name"`
OpenID *string
Username *string `json:"username"`
Role *Role
Email *string `json:"email"`
Nickname *string `json:"nickname"`
OpenID *string
}
type UserDelete struct {
+25 -26
View File
@@ -10,10 +10,10 @@ type UserSettingKey string
const (
// UserSettingLocaleKey is the key type for user locale.
UserSettingLocaleKey UserSettingKey = "locale"
// UserSettingAppearanceKey is the key type for user appearance.
UserSettingAppearanceKey UserSettingKey = "appearance"
// UserSettingMemoVisibilityKey is the key type for user preference memo default visibility.
UserSettingMemoVisibilityKey UserSettingKey = "memoVisibility"
// UserSettingMobileEditorStyleKey is the key type for mobile editor style.
UserSettingMobileEditorStyleKey UserSettingKey = "mobileEditorStyle"
// UserSettingMemoDisplayTsOptionKey is the key type for memo display ts option.
UserSettingMemoDisplayTsOptionKey UserSettingKey = "memoDisplayTsOption"
)
@@ -23,10 +23,10 @@ func (key UserSettingKey) String() string {
switch key {
case UserSettingLocaleKey:
return "locale"
case UserSettingAppearanceKey:
return "appearance"
case UserSettingMemoVisibilityKey:
return "memoVisibility"
case UserSettingMobileEditorStyleKey:
return "mobileEditorStyle"
case UserSettingMemoDisplayTsOptionKey:
return "memoDisplayTsOption"
}
@@ -34,10 +34,9 @@ func (key UserSettingKey) String() string {
}
var (
UserSettingLocaleValue = []string{"en", "zh", "vi"}
UserSettingMemoVisibilityValue = []Visibility{Privite, Protected, Public}
UserSettingEditorFontStyleValue = []string{"normal", "mono"}
UserSettingMobileEditorStyleValue = []string{"normal", "float"}
UserSettingLocaleValue = []string{"en", "zh", "vi", "fr", "nl", "sv", "de"}
UserSettingAppearanceValue = []string{"system", "light", "dark"}
UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public}
UserSettingMemoDisplayTsOptionKeyValue = []string{"created_ts", "updated_ts"}
)
@@ -72,8 +71,25 @@ func (upsert UserSettingUpsert) Validate() error {
if invalid {
return fmt.Errorf("invalid user setting locale value")
}
} else if upsert.Key == UserSettingAppearanceKey {
appearanceValue := "light"
err := json.Unmarshal([]byte(upsert.Value), &appearanceValue)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting appearance value")
}
invalid := true
for _, value := range UserSettingAppearanceValue {
if appearanceValue == value {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid user setting appearance value")
}
} else if upsert.Key == UserSettingMemoVisibilityKey {
memoVisibilityValue := Privite
memoVisibilityValue := Private
err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting memo visibility value")
@@ -89,23 +105,6 @@ func (upsert UserSettingUpsert) Validate() error {
if invalid {
return fmt.Errorf("invalid user setting memo visibility value")
}
} else if upsert.Key == UserSettingMobileEditorStyleKey {
mobileEditorStyleValue := "normal"
err := json.Unmarshal([]byte(upsert.Value), &mobileEditorStyleValue)
if err != nil {
return fmt.Errorf("failed to unmarshal user setting mobile editor style")
}
invalid := true
for _, value := range UserSettingMobileEditorStyleValue {
if mobileEditorStyleValue == value {
invalid = false
break
}
}
if invalid {
return fmt.Errorf("invalid user setting mobile editor style value")
}
} else if upsert.Key == UserSettingMemoDisplayTsOptionKey {
memoDisplayTsOption := "created_ts"
err := json.Unmarshal([]byte(upsert.Value), &memoDisplayTsOption)
+2
View File
@@ -40,6 +40,8 @@ func run(profile *profile.Profile) error {
serverInstance.Store = storeInstance
metricCollector := server.NewMetricCollector(profile, storeInstance)
// Disable metrics collector.
metricCollector.Enabled = false
serverInstance.Collector = &metricCollector
println(greetingBanner)
+18
View File
@@ -0,0 +1,18 @@
version: "3.0"
# uffizzi integration
x-uffizzi:
ingress:
service: memos
port: 5230
services:
memos:
image: "${MEMOS_IMAGE}"
volumes:
- memos_volume:/var/opt/memos
command: ["--mode", "dev"]
volumes:
memos_volume:
+21 -20
View File
@@ -1,29 +1,17 @@
module github.com/usememos/memos
go 1.17
go 1.19
require github.com/mattn/go-sqlite3 v1.14.9
require github.com/google/uuid v1.3.0
require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/net v0.0.0-20220728030405-41545e8bf201 // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
golang.org/x/net v0.0.0-20220728030405-41545e8bf201
)
require (
github.com/gorilla/context v1.1.1 // indirect
github.com/labstack/echo/v4 v4.9.0
github.com/labstack/gommon v0.3.1 // indirect
)
require github.com/labstack/echo/v4 v4.9.0
require (
github.com/VictoriaMetrics/fastcache v1.10.0
@@ -31,17 +19,30 @@ require (
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/labstack/echo-contrib v0.13.0
github.com/stretchr/testify v1.8.1
)
require (
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/labstack/gommon v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/segmentio/analytics-go v3.1.0+incompatible
)
require github.com/segmentio/analytics-go v3.1.0+incompatible
+9 -577
View File
@@ -1,208 +1,29 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Shopify/sarama v1.30.0/go.mod h1:zujlQQx1kzHsh4jfV1USnptCQrHAEZ2Hk8fTKCulPVs=
github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0=
github.com/VictoriaMetrics/fastcache v1.10.0 h1:5hDJnLsKLpnUEToub7ETuRu8RCkb40woBZAUiKonXzY=
github.com/VictoriaMetrics/fastcache v1.10.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/casbin/casbin/v2 v2.51.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -212,7 +33,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo-contrib v0.13.0 h1:bzSG0SpuZZd7BmJLvsWtPfU23W0Enh3K0tok3aENVKA=
github.com/labstack/echo-contrib v0.13.0/go.mod h1:IF9+MJu22ADOZEHD+bAV67XMIO3vNXUy7Naz/ABPHEs=
github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
@@ -224,436 +44,48 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/openzipkin/zipkin-go v0.4.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rabbitmq/amqp091-go v1.1.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80=
github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4=
github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220728030405-41545e8bf201 h1:bvOltf3SADAfG05iRml8lAB3qjoEX5RCyN4K6G5v3N0=
golang.org/x/net v0.0.0-20220728030405-41545e8bf201/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+98
View File
@@ -0,0 +1,98 @@
package getter
import (
"fmt"
"io"
"net/http"
"net/url"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
type HTMLMeta struct {
Title string `json:"title"`
Description string `json:"description"`
Image string `json:"image"`
}
func GetHTMLMeta(urlStr string) (*HTMLMeta, error) {
if _, err := url.Parse(urlStr); err != nil {
return nil, err
}
response, err := http.Get(urlStr)
if err != nil {
return nil, err
}
defer response.Body.Close()
mediatype, err := getMediatype(response)
if err != nil {
return nil, err
}
if mediatype != "text/html" {
return nil, fmt.Errorf("Wrong website mediatype")
}
htmlMeta := extractHTMLMeta(response.Body)
return htmlMeta, nil
}
func extractHTMLMeta(resp io.Reader) *HTMLMeta {
tokenizer := html.NewTokenizer(resp)
htmlMeta := new(HTMLMeta)
for {
tokenType := tokenizer.Next()
if tokenType == html.ErrorToken {
break
} else if tokenType == html.StartTagToken || tokenType == html.SelfClosingTagToken {
token := tokenizer.Token()
if token.DataAtom == atom.Body {
break
}
if token.DataAtom == atom.Title {
tokenizer.Next()
token := tokenizer.Token()
htmlMeta.Title = token.Data
} else if token.DataAtom == atom.Meta {
description, ok := extractMetaProperty(token, "description")
if ok {
htmlMeta.Description = description
}
ogTitle, ok := extractMetaProperty(token, "og:title")
if ok {
htmlMeta.Title = ogTitle
}
ogDescription, ok := extractMetaProperty(token, "og:description")
if ok {
htmlMeta.Description = ogDescription
}
ogImage, ok := extractMetaProperty(token, "og:image")
if ok {
htmlMeta.Image = ogImage
}
}
}
}
return htmlMeta
}
func extractMetaProperty(token html.Token, prop string) (content string, ok bool) {
content, ok = "", false
for _, attr := range token.Attr {
if attr.Key == "property" && attr.Val == prop {
ok = true
}
if attr.Key == "content" {
content = attr.Val
}
}
return content, ok
}
+28
View File
@@ -0,0 +1,28 @@
package getter
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetHTMLMeta(t *testing.T) {
tests := []struct {
urlStr string
htmlMeta HTMLMeta
}{
{
urlStr: "https://www.bytebase.com/blog/sql-review-tool-for-devs",
htmlMeta: HTMLMeta{
Title: "The SQL Review Tool for Developers",
Description: "Reviewing SQL can be somewhat tedious, yet is essential to keep your database fleet reliable. At Bytebase, we are building a developer-first SQL review tool to empower the DevOps system.",
Image: "https://www.bytebase.com/static/blog/sql-review-tool-for-devs/dev-fighting-dba.webp",
},
},
}
for _, test := range tests {
metadata, err := GetHTMLMeta(test.urlStr)
require.NoError(t, err)
require.Equal(t, test.htmlMeta, *metadata)
}
}
+4
View File
@@ -0,0 +1,4 @@
// getter is using to get resources from url.
// * Get metadata for website;
// * Get image blob to avoid CORS;
package getter
+45
View File
@@ -0,0 +1,45 @@
package getter
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type Image struct {
Blob []byte
Mediatype string
}
func GetImage(urlStr string) (*Image, error) {
if _, err := url.Parse(urlStr); err != nil {
return nil, err
}
response, err := http.Get(urlStr)
if err != nil {
return nil, err
}
defer response.Body.Close()
mediatype, err := getMediatype(response)
if err != nil {
return nil, err
}
if !strings.HasPrefix(mediatype, "image/") {
return nil, fmt.Errorf("Wrong image mediatype")
}
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
image := &Image{
Blob: bodyBytes,
Mediatype: mediatype,
}
return image, nil
}
+21
View File
@@ -0,0 +1,21 @@
package getter
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetImage(t *testing.T) {
tests := []struct {
urlStr string
}{
{
urlStr: "https://star-history.com/bytebase.webp",
},
}
for _, test := range tests {
_, err := GetImage(test.urlStr)
require.NoError(t, err)
}
}
+15
View File
@@ -0,0 +1,15 @@
package getter
import (
"mime"
"net/http"
)
func getMediatype(response *http.Response) (string, error) {
contentType := response.Header.Get("content-type")
mediatype, _, err := mime.ParseMediaType(contentType)
if err != nil {
return "", err
}
return mediatype, nil
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

+4 -7
View File
@@ -55,15 +55,12 @@ func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Request().Context()
path := c.Path()
// Skip auth.
if common.HasPrefixes(path, "/api/auth") {
return next(c)
}
if common.HasPrefixes(path, "/api/ping", "/api/status", "/api/user/:id") && c.Request().Method == http.MethodGet {
return next(c)
}
{
// If there is openId in query string and related user is found, then skip auth.
openID := c.QueryParam("openId")
@@ -97,18 +94,18 @@ func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
}
if user != nil {
if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", user.Email))
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", user.Username))
}
c.Set(getUserIDContextKey(), userID)
}
}
}
if common.HasPrefixes(path, "/api/memo/all", "/api/memo/:memoId", "/api/memo/amount") && c.Request().Method == http.MethodGet {
if common.HasPrefixes(path, "/api/ping", "/api/status", "/api/user/:id", "/api/memo/all", "/api/memo/:memoId", "/api/memo/amount") && c.Request().Method == http.MethodGet {
return next(c)
}
if common.HasPrefixes(path, "/api/memo", "/api/tag", "/api/shortcut", "/api/memo/stats") && c.Request().Method == http.MethodGet {
if common.HasPrefixes(path, "/api/memo", "/api/tag", "/api/shortcut") && c.Request().Method == http.MethodGet {
if _, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
return next(c)
}
+6 -6
View File
@@ -22,16 +22,16 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
}
userFind := &api.UserFind{
Email: &signin.Email,
Username: &signin.Username,
}
user, err := s.Store.FindUser(ctx, userFind)
if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", signin.Email)).SetInternal(err)
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by username %s", signin.Username)).SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with email %s", signin.Email))
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with username %s", signin.Username))
} else if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", signin.Email))
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with username %s", signin.Username))
}
// Compare the stored hashed password, with the hashed version of the password that was received.
@@ -107,9 +107,9 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
}
userCreate := &api.UserCreate{
Email: signup.Email,
Username: signup.Username,
Role: api.Role(signup.Role),
Name: signup.Name,
Nickname: signup.Username,
Password: signup.Password,
OpenID: common.GenUUID(),
}
+72
View File
@@ -0,0 +1,72 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/labstack/echo/v4"
getter "github.com/usememos/memos/plugin/http_getter"
metric "github.com/usememos/memos/plugin/metrics"
)
func (s *Server) registerGetterPublicRoutes(g *echo.Group) {
g.GET("/get/httpmeta", func(c echo.Context) error {
ctx := c.Request().Context()
urlStr := c.QueryParam("url")
if urlStr == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Missing website url")
}
if _, err := url.Parse(urlStr); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
}
htmlMeta, err := getter.GetHTMLMeta(urlStr)
if err != nil {
return echo.NewHTTPError(http.StatusNotAcceptable, fmt.Sprintf("Failed to get website meta with url: %s", urlStr)).SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "getter used",
Labels: map[string]string{
"type": "httpmeta",
},
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(htmlMeta)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode website HTML meta").SetInternal(err)
}
return nil
})
g.GET("/get/image", func(c echo.Context) error {
ctx := c.Request().Context()
urlStr := c.QueryParam("url")
if urlStr == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Missing image url")
}
if _, err := url.Parse(urlStr); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
}
image, err := getter.GetImage(urlStr)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to get image url: %s", urlStr)).SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "getter used",
Labels: map[string]string{
"type": "image",
},
})
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", image.Mediatype)
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
if _, err := c.Response().Writer.Write(image.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write image blob").SetInternal(err)
}
return nil
})
}
+10 -10
View File
@@ -45,7 +45,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
}
if userMemoVisibilitySetting != nil {
memoVisibility := api.Privite
memoVisibility := api.Private
err := json.Unmarshal([]byte(userMemoVisibilitySetting.Value), &memoVisibility)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
@@ -53,7 +53,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
memoCreate.Visibility = memoVisibility
} else {
// Private is the default memo visibility.
memoCreate.Visibility = api.Privite
memoCreate.Visibility = api.Private
}
}
@@ -176,10 +176,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
contentSearch := "#" + tag + " "
memoFind.ContentSearch = &contentSearch
}
visibilitListStr := c.QueryParam("visibility")
if visibilitListStr != "" {
visibilityListStr := c.QueryParam("visibility")
if visibilityListStr != "" {
visibilityList := []api.Visibility{}
for _, visibility := range strings.Split(visibilitListStr, ",") {
for _, visibility := range strings.Split(visibilityListStr, ",") {
visibilityList = append(visibilityList, api.Visibility(visibility))
}
memoFind.VisibilityList = visibilityList
@@ -271,7 +271,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
if *memoFind.CreatorID != currentUserID {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
} else {
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected, api.Privite}
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected, api.Private}
}
}
@@ -313,10 +313,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
contentSearch := "#" + tag + " "
memoFind.ContentSearch = &contentSearch
}
visibilitListStr := c.QueryParam("visibility")
if visibilitListStr != "" {
visibilityListStr := c.QueryParam("visibility")
if visibilityListStr != "" {
visibilityList := []api.Visibility{}
for _, visibility := range strings.Split(visibilitListStr, ",") {
for _, visibility := range strings.Split(visibilityListStr, ",") {
visibilityList = append(visibilityList, api.Visibility(visibility))
}
memoFind.VisibilityList = visibilityList
@@ -372,7 +372,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
}
userID, ok := c.Get(getUserIDContextKey()).(int)
if memo.Visibility == api.Privite {
if memo.Visibility == api.Private {
if !ok || memo.CreatorID != userID {
return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
}
+15 -9
View File
@@ -13,9 +13,10 @@ import (
// MetricCollector is the metric collector.
type MetricCollector struct {
collector metric.Collector
profile *profile.Profile
store *store.Store
Collector metric.Collector
Enabled bool
Profile *profile.Profile
Store *store.Store
}
const (
@@ -26,23 +27,28 @@ func NewMetricCollector(profile *profile.Profile, store *store.Store) MetricColl
c := segment.NewCollector(segmentMetricWriteKey)
return MetricCollector{
collector: c,
profile: profile,
store: store,
Collector: c,
Enabled: true,
Profile: profile,
Store: store,
}
}
func (mc *MetricCollector) Collect(_ context.Context, metric *metric.Metric) {
if mc.profile.Mode == "dev" {
if !mc.Enabled {
return
}
if mc.Profile.Mode == "dev" {
return
}
if metric.Labels == nil {
metric.Labels = map[string]string{}
}
metric.Labels["version"] = version.GetCurrentVersion(mc.profile.Mode)
metric.Labels["version"] = version.GetCurrentVersion(mc.Profile.Mode)
err := mc.collector.Collect(metric)
err := mc.Collector.Collect(metric)
if err != nil {
fmt.Printf("Failed to request segment, error: %+v\n", err)
}
+12 -7
View File
@@ -16,6 +16,11 @@ import (
"github.com/labstack/echo/v4"
)
const (
// The max file size is 32MB.
maxFileSize = (32 * 8) << 20
)
func (s *Server) registerResourceRoutes(g *echo.Group) {
g.POST("/resource", func(c echo.Context) error {
ctx := c.Request().Context()
@@ -24,13 +29,15 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
err := c.Request().ParseMultipartForm(64 << 20)
if err != nil {
if err := c.Request().ParseMultipartForm(maxFileSize); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Upload file overload max size").SetInternal(err)
}
file, err := c.FormFile("file")
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get uploading file").SetInternal(err)
}
if file == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Upload file not found").SetInternal(err)
}
@@ -86,13 +93,13 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
}
for _, resource := range list {
memoResoureceList, err := s.Store.FindMemoResourceList(ctx, &api.MemoResourceFind{
memoResourceList, err := s.Store.FindMemoResourceList(ctx, &api.MemoResourceFind{
ResourceID: &resource.ID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo resource list").SetInternal(err)
}
resource.LinkedMemoAmount = len(memoResoureceList)
resource.LinkedMemoAmount = len(memoResourceList)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
@@ -154,7 +161,6 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write resource blob").SetInternal(err)
}
return nil
})
@@ -256,11 +262,10 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
c.Response().Writer.WriteHeader(http.StatusOK)
c.Response().Writer.Header().Set("Content-Type", resource.Type)
c.Response().Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write response").SetInternal(err)
}
return nil
})
}
+2 -2
View File
@@ -46,14 +46,14 @@ func (s *Server) registerRSSRoutes(g *echo.Group) {
Title: "Memos",
Link: &feeds.Link{Href: baseURL},
Description: "Memos",
Author: &feeds.Author{Name: user.Name},
Author: &feeds.Author{Name: user.Username},
Created: time.Now(),
}
feed.Items = make([]*feeds.Item, len(memoList))
for i, memo := range memoList {
feed.Items[i] = &feeds.Item{
Title: memo.Content,
Title: user.Username + "-memos-" + strconv.Itoa(memo.ID),
Link: &feeds.Link{Href: baseURL + "/m/" + strconv.Itoa(memo.ID)},
Description: memo.Content,
Created: time.Unix(memo.CreatedTs, 0),
+1
View File
@@ -66,6 +66,7 @@ func NewServer(profile *profile.Profile) *Server {
publicGroup := e.Group("/o")
s.registerResourcePublicRoutes(publicGroup)
s.registerGetterPublicRoutes(publicGroup)
apiGroup := e.Group("/api")
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
+54 -4
View File
@@ -3,9 +3,11 @@ package server
import (
"encoding/json"
"net/http"
"os"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
metric "github.com/usememos/memos/plugin/metrics"
"github.com/labstack/echo/v4"
)
@@ -38,10 +40,12 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
}
systemStatus := api.SystemStatus{
Host: hostUser,
Profile: s.Profile,
AllowSignUp: false,
AdditionalStyle: "",
Host: hostUser,
Profile: *s.Profile,
DBSize: 0,
AllowSignUp: false,
AdditionalStyle: "",
AdditionalScript: "",
}
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
@@ -59,6 +63,26 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
systemStatus.AllowSignUp = value.(bool)
} else if systemSetting.Name == api.SystemSettingAdditionalStyleName {
systemStatus.AdditionalStyle = value.(string)
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
systemStatus.AdditionalScript = value.(string)
}
}
userID, ok := c.Get(getUserIDContextKey()).(int)
// Get database size for host user.
if ok {
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user != nil && user.Role == api.Host {
fi, err := os.Stat(s.Profile.DSN)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read database fileinfo").SetInternal(err)
}
systemStatus.DBSize = fi.Size()
}
}
@@ -100,6 +124,10 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
}
s.Collector.Collect(ctx, &metric.Metric{
Name: "systemSetting updated",
Labels: map[string]string{"field": string(systemSettingUpsert.Name)},
})
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(systemSetting)); err != nil {
@@ -121,4 +149,26 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
}
return nil
})
g.POST("/system/vacuum", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.FindUser(ctx, &api.UserFind{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != api.Host {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
if err := s.Store.Vacuum(ctx); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to vacuum database").SetInternal(err)
}
c.Response().WriteHeader(http.StatusOK)
return nil
})
}
+9 -3
View File
@@ -12,7 +12,7 @@ import (
"github.com/labstack/echo/v4"
)
var tagRegexpList = []*regexp.Regexp{regexp.MustCompile(`^#([^\s#]+?) `), regexp.MustCompile(`[^\S#]?#([^\s#]+?) `)}
var tagRegexpList = []*regexp.Regexp{regexp.MustCompile(`^#([^\s#]+?) `), regexp.MustCompile(`[^\S]#([^\s#]+?) `), regexp.MustCompile(` #([^\s#]+?) `)}
func (s *Server) registerTagRoutes(g *echo.Group) {
g.GET("/tag", func(c echo.Context) error {
@@ -47,9 +47,15 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
}
tagList := []string{}
tagMapSet := make(map[string]bool)
for _, memo := range memoList {
tagList = append(tagList, findTagListFromMemoContent(memo.Content)...)
for _, tag := range findTagListFromMemoContent(memo.Content) {
tagMapSet[tag] = true
}
}
tagList := []string{}
for tag := range tagMapSet {
tagList = append(tagList, tag)
}
sort.Strings(tagList)
+8
View File
@@ -29,6 +29,14 @@ func TestFindTagListFromMemoContent(t *testing.T) {
memoContent: "#tag1 #tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "tag2", "tag3", "tag4"},
},
{
memoContent: "#tag1 123123#tag2 \n#tag3 #tag4 ",
want: []string{"tag1", "tag3", "tag4"},
},
{
memoContent: "#tag1 http://123123.com?123123#tag2 \n#tag3 #tag4 http://123123.com?123123#tag2) ",
want: []string{"tag1", "tag3", "tag4"},
},
}
for _, test := range tests {
result := findTagListFromMemoContent(test.memoContent)
+9 -1
View File
@@ -199,7 +199,7 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err)
}
if userPatch.Email != nil && !common.ValidateEmail(*userPatch.Email) {
if userPatch.Email != nil && *userPatch.Email != "" && !common.ValidateEmail(*userPatch.Email) {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid email format")
}
@@ -223,6 +223,14 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
}
userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{
UserID: userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
}
user.UserSettingList = userSettingList
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
+2 -2
View File
@@ -7,10 +7,10 @@ import (
// Version is the service current released version.
// Semantic versioning: https://semver.org/
var Version = "0.7.2"
var Version = "0.8.2"
// DevVersion is the service current development version.
var DevVersion = "0.7.2"
var DevVersion = "0.8.2"
func GetCurrentVersion(mode string) string {
if mode == "dev" {
+44 -67
View File
@@ -49,8 +49,8 @@ func (db *DB) Open(ctx context.Context) (err error) {
}
db.Db = sqlDB
// If mode is dev, we should migrate and seed the database.
if db.profile.Mode == "dev" {
// In dev mode, we should migrate and seed the database.
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
if err := db.applyLatestSchema(ctx); err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
@@ -65,55 +65,51 @@ func (db *DB) Open(ctx context.Context) (err error) {
if err := db.applyLatestSchema(ctx); err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
}
} else {
if err := db.createMigrationHistoryTable(ctx); err != nil {
return fmt.Errorf("failed to create migration_history table: %w", err)
}
}
currentVersion := version.GetCurrentVersion(db.profile.Mode)
migrationHistory, err := db.FindMigrationHistory(ctx, &MigrationHistoryFind{})
currentVersion := version.GetCurrentVersion(db.profile.Mode)
migrationHistory, err := db.FindMigrationHistory(ctx, &MigrationHistoryFind{})
if err != nil {
return fmt.Errorf("failed to find migration history, err: %w", err)
}
if migrationHistory == nil {
if _, err = db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
Version: currentVersion,
}); err != nil {
return fmt.Errorf("failed to upsert migration history, err: %w", err)
}
return nil
}
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), migrationHistory.Version) {
minorVersionList := getMinorVersionList()
// backup the raw database file before migration
rawBytes, err := os.ReadFile(db.profile.DSN)
if err != nil {
return err
return fmt.Errorf("failed to read raw database file, err: %w", err)
}
if migrationHistory == nil {
migrationHistory, err = db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
Version: currentVersion,
})
if err != nil {
return err
}
backupDBFilePath := fmt.Sprintf("%s/memos_%s_%d_backup.db", db.profile.Data, db.profile.Version, time.Now().Unix())
if err := os.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
return fmt.Errorf("failed to write raw database file, err: %w", err)
}
println("succeed to copy a backup database file")
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), migrationHistory.Version) {
minorVersionList := getMinorVersionList()
// backup the raw database file before migration
rawBytes, err := os.ReadFile(db.profile.DSN)
if err != nil {
return fmt.Errorf("failed to read raw database file, err: %w", err)
}
backupDBFilePath := fmt.Sprintf("%s/memos_%s_%d_backup.db", db.profile.Data, db.profile.Version, time.Now().Unix())
if err := os.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
return fmt.Errorf("failed to write raw database file, err: %w", err)
}
println("succeed to copy a backup database file")
println("start migrate")
for _, minorVersion := range minorVersionList {
normalizedVersion := minorVersion + ".0"
if version.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
println("applying migration for", normalizedVersion)
if err := db.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
return fmt.Errorf("failed to apply minor version migration: %w", err)
}
println("start migrate")
for _, minorVersion := range minorVersionList {
normalizedVersion := minorVersion + ".0"
if version.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
println("applying migration for", normalizedVersion)
if err := db.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
return fmt.Errorf("failed to apply minor version migration: %w", err)
}
}
}
println("end migrate")
println("end migrate")
// remove the created backup db file after migrate succeed
if err := os.Remove(backupDBFilePath); err != nil {
println(fmt.Sprintf("Failed to remove temp database file, err %v", err))
}
// remove the created backup db file after migrate succeed
if err := os.Remove(backupDBFilePath); err != nil {
println(fmt.Sprintf("Failed to remove temp database file, err %v", err))
}
}
}
@@ -141,7 +137,7 @@ func (db *DB) applyLatestSchema(ctx context.Context) error {
func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion string) error {
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration/prod", minorVersion))
if err != nil {
return err
return fmt.Errorf("failed to read ddl files, err: %w", err)
}
sort.Strings(filenames)
@@ -167,10 +163,11 @@ func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion st
defer tx.Rollback()
// upsert the newest version to migration_history
version := minorVersion + ".0"
if _, err = upsertMigrationHistory(ctx, tx, &MigrationHistoryUpsert{
Version: minorVersion + ".0",
Version: version,
}); err != nil {
return err
return fmt.Errorf("failed to upsert migration history with version: %s, err: %w", version, err)
}
return tx.Commit()
@@ -179,7 +176,7 @@ func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion st
func (db *DB) seed(ctx context.Context) error {
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
if err != nil {
return err
return fmt.Errorf("failed to read seed files, err: %w", err)
}
sort.Strings(filenames)
@@ -207,7 +204,7 @@ func (db *DB) execute(ctx context.Context, stmt string) error {
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, stmt); err != nil {
return err
return fmt.Errorf("failed to execute statement, err: %w", err)
}
return tx.Commit()
@@ -236,23 +233,3 @@ func getMinorVersionList() []string {
return minorVersionList
}
// createMigrationHistoryTable creates the migration_history table if it doesn't exist.
func (db *DB) createMigrationHistoryTable(ctx context.Context) error {
tx, err := db.Db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := createTable(ctx, tx, `
CREATE TABLE IF NOT EXISTS migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
`); err != nil {
return err
}
return tx.Commit()
}
+26 -19
View File
@@ -1,16 +1,39 @@
-- migration_history
CREATE TABLE migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL,
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -55,14 +78,6 @@ CREATE TABLE resource (
size INTEGER NOT NULL DEFAULT 0
);
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
@@ -71,11 +86,3 @@ CREATE TABLE memo_resource (
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
@@ -0,0 +1,5 @@
-- migration_history
CREATE TABLE IF NOT EXISTS migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
@@ -0,0 +1,41 @@
-- add column username TEXT NOT NULL UNIQUE
-- rename column name to nickname
-- add role `ADMIN`
DROP TABLE IF EXISTS _user_old;
ALTER TABLE user RENAME TO _user_old;
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
INSERT INTO user (
id, created_ts, updated_ts, row_status,
username, role, email, nickname, password_hash,
open_id
)
SELECT
id,
created_ts,
updated_ts,
row_status,
email,
role,
email,
name,
password_hash,
open_id
FROM
_user_old;
DROP TABLE IF EXISTS _user_old;
+26 -19
View File
@@ -1,16 +1,39 @@
-- migration_history
CREATE TABLE migration_history (
version TEXT NOT NULL PRIMARY KEY,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL,
username TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER',
email TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE
);
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
-- memo
CREATE TABLE memo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -55,14 +78,6 @@ CREATE TABLE resource (
size INTEGER NOT NULL DEFAULT 0
);
-- user_setting
CREATE TABLE user_setting (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE(user_id, key)
);
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
@@ -71,11 +86,3 @@ CREATE TABLE memo_resource (
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
-- system_setting
CREATE TABLE system_setting (
name TEXT NOT NULL,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
UNIQUE(name)
);
+3 -14
View File
@@ -20,7 +20,7 @@ type MigrationHistoryFind struct {
}
func (db *DB) FindMigrationHistory(ctx context.Context, find *MigrationHistoryFind) (*MigrationHistory, error) {
tx, err := db.Db.Begin()
tx, err := db.Db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
@@ -40,7 +40,7 @@ func (db *DB) FindMigrationHistory(ctx context.Context, find *MigrationHistoryFi
}
func (db *DB) UpsertMigrationHistory(ctx context.Context, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
tx, err := db.Db.Begin()
tx, err := db.Db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
@@ -111,24 +111,13 @@ func upsertMigrationHistory(ctx context.Context, tx *sql.Tx, upsert *MigrationHi
version=EXCLUDED.version
RETURNING version, created_ts
`
row, err := tx.QueryContext(ctx, query, upsert.Version)
if err != nil {
return nil, err
}
defer row.Close()
row.Next()
var migrationHistory MigrationHistory
if err := row.Scan(
if err := tx.QueryRowContext(ctx, query, upsert.Version).Scan(
&migrationHistory.Version,
&migrationHistory.CreatedTs,
); err != nil {
return nil, err
}
if err := row.Err(); err != nil {
return nil, err
}
return &migrationHistory, nil
}
+16 -10
View File
@@ -1,18 +1,20 @@
INSERT INTO
user (
`id`,
`email`,
`username`,
`role`,
`name`,
`email`,
`nickname`,
`open_id`,
`password_hash`
)
VALUES
(
101,
'demo@usememos.com',
'demohero',
'HOST',
'Demo Host',
'demo@usememos.com',
'Demo Hero',
'demo_open_id',
-- raw password: secret
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK'
@@ -21,17 +23,19 @@ VALUES
INSERT INTO
user (
`id`,
`email`,
`username`,
`role`,
`name`,
`email`,
`nickname`,
`open_id`,
`password_hash`
)
VALUES
(
102,
'jack@usememos.com',
'jack',
'USER',
'jack@usememos.com',
'Jack',
'jack_open_id',
-- raw password: secret
@@ -42,9 +46,10 @@ INSERT INTO
user (
`id`,
`row_status`,
`email`,
`username`,
`role`,
`name`,
`email`,
`nickname`,
`open_id`,
`password_hash`
)
@@ -52,8 +57,9 @@ VALUES
(
103,
'ARCHIVED',
'bob@usememos.com',
'bob',
'USER',
'bob@usememos.com',
'Bob',
'bob_open_id',
-- raw password: secret
+2 -2
View File
@@ -40,9 +40,9 @@ INSERT INTO
VALUES
(
1003,
"**Bytebase** - Database CI/CD for DevOps teams.
"**Bytebase** - An open source Database CI/CD for DevOps teams.
![](https://star-history.com/bytebase.webp)
🌐 [https://bytebase.com](https://www.bytebase.com)",
🌐 [Source code](https://github.com/bytebase/bytebase)",
101,
'PUBLIC'
);
-65
View File
@@ -1,65 +0,0 @@
package db
import (
"context"
"database/sql"
"strings"
)
type Table struct {
Name string
SQL string
}
//lint:ignore U1000 Ignore unused function temporarily for debugging
//nolint:all
func findTable(ctx context.Context, tx *sql.Tx, tableName string) (*Table, error) {
where, args := []string{"1 = 1"}, []interface{}{}
where, args = append(where, "type = ?"), append(args, "table")
where, args = append(where, "name = ?"), append(args, tableName)
query := `
SELECT
tbl_name,
sql
FROM sqlite_schema
WHERE ` + strings.Join(where, " AND ")
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
tableList := make([]*Table, 0)
for rows.Next() {
var table Table
if err := rows.Scan(
&table.Name,
&table.SQL,
); err != nil {
return nil, err
}
tableList = append(tableList, &table)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(tableList) == 0 {
return nil, nil
} else {
return tableList[0], nil
}
}
func createTable(ctx context.Context, tx *sql.Tx, stmt string) error {
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return err
}
return nil
}
+1 -1
View File
@@ -49,7 +49,7 @@ func (s *Store) Vacuum(ctx context.Context) error {
return nil
}
// Exec vacuum records in a transcation.
// Exec vacuum records in a transaction.
func vacuum(ctx context.Context, tx *sql.Tx) error {
if err := vacuumMemo(ctx, tx); err != nil {
return err
+48 -48
View File
@@ -21,9 +21,10 @@ type userRaw struct {
UpdatedTs int64
// Domain specific fields
Email string
Username string
Role api.Role
Name string
Email string
Nickname string
PasswordHash string
OpenID string
}
@@ -36,9 +37,10 @@ func (raw *userRaw) toUser() *api.User {
CreatedTs: raw.CreatedTs,
UpdatedTs: raw.UpdatedTs,
Email: raw.Email,
Username: raw.Username,
Role: raw.Role,
Name: raw.Name,
Email: raw.Email,
Nickname: raw.Nickname,
PasswordHash: raw.PasswordHash,
OpenID: raw.OpenID,
}
@@ -194,27 +196,30 @@ func (s *Store) DeleteUser(ctx context.Context, delete *api.UserDelete) error {
func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userRaw, error) {
query := `
INSERT INTO user (
email,
username,
role,
name,
email,
nickname,
password_hash,
open_id
)
VALUES (?, ?, ?, ?, ?)
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts, row_status
VALUES (?, ?, ?, ?, ?, ?)
RETURNING id, username, role, email, nickname, password_hash, open_id, created_ts, updated_ts, row_status
`
var userRaw userRaw
if err := tx.QueryRowContext(ctx, query,
create.Email,
create.Username,
create.Role,
create.Name,
create.Email,
create.Nickname,
create.PasswordHash,
create.OpenID,
).Scan(
&userRaw.ID,
&userRaw.Email,
&userRaw.Username,
&userRaw.Role,
&userRaw.Name,
&userRaw.Email,
&userRaw.Nickname,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
@@ -236,11 +241,14 @@ func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw,
if v := patch.RowStatus; v != nil {
set, args = append(set, "row_status = ?"), append(args, *v)
}
if v := patch.Username; v != nil {
set, args = append(set, "username = ?"), append(args, *v)
}
if v := patch.Email; v != nil {
set, args = append(set, "email = ?"), append(args, *v)
}
if v := patch.Name; v != nil {
set, args = append(set, "name = ?"), append(args, *v)
if v := patch.Nickname; v != nil {
set, args = append(set, "nickname = ?"), append(args, *v)
}
if v := patch.PasswordHash; v != nil {
set, args = append(set, "password_hash = ?"), append(args, *v)
@@ -255,38 +263,25 @@ func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw,
UPDATE user
SET ` + strings.Join(set, ", ") + `
WHERE id = ?
RETURNING id, email, role, name, password_hash, open_id, created_ts, updated_ts, row_status
RETURNING id, username, role, email, nickname, password_hash, open_id, created_ts, updated_ts, row_status
`
row, err := tx.QueryContext(ctx, query, args...)
if err != nil {
var userRaw userRaw
if err := tx.QueryRowContext(ctx, query, args...).Scan(
&userRaw.ID,
&userRaw.Username,
&userRaw.Role,
&userRaw.Email,
&userRaw.Nickname,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
&userRaw.UpdatedTs,
&userRaw.RowStatus,
); err != nil {
return nil, FormatError(err)
}
defer row.Close()
if row.Next() {
var userRaw userRaw
if err := row.Scan(
&userRaw.ID,
&userRaw.Email,
&userRaw.Role,
&userRaw.Name,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
&userRaw.UpdatedTs,
&userRaw.RowStatus,
); err != nil {
return nil, FormatError(err)
}
if err := row.Err(); err != nil {
return nil, err
}
return &userRaw, nil
}
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", patch.ID)}
return &userRaw, nil
}
func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userRaw, error) {
@@ -295,14 +290,17 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
if v := find.ID; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if v := find.Username; v != nil {
where, args = append(where, "username = ?"), append(args, *v)
}
if v := find.Role; v != nil {
where, args = append(where, "role = ?"), append(args, *v)
}
if v := find.Email; v != nil {
where, args = append(where, "email = ?"), append(args, *v)
}
if v := find.Name; v != nil {
where, args = append(where, "name = ?"), append(args, *v)
if v := find.Nickname; v != nil {
where, args = append(where, "nickname = ?"), append(args, *v)
}
if v := find.OpenID; v != nil {
where, args = append(where, "open_id = ?"), append(args, *v)
@@ -311,9 +309,10 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
query := `
SELECT
id,
email,
username,
role,
name,
email,
nickname,
password_hash,
open_id,
created_ts,
@@ -334,9 +333,10 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
var userRaw userRaw
if err := rows.Scan(
&userRaw.ID,
&userRaw.Email,
&userRaw.Username,
&userRaw.Role,
&userRaw.Name,
&userRaw.Email,
&userRaw.Nickname,
&userRaw.PasswordHash,
&userRaw.OpenID,
&userRaw.CreatedTs,
+2 -1
View File
@@ -3,7 +3,8 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/logo.webp" type="image/*" />
<meta name="theme-color" content="#f6f5f4" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f6f5f4" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#27272a" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<link rel="manifest" href="/manifest.json" />
<title>Memos</title>
+3 -4
View File
@@ -10,12 +10,11 @@
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@mui/joy": "^5.0.0-alpha.52",
"@mui/joy": "^5.0.0-alpha.56",
"@reduxjs/toolkit": "^1.8.1",
"axios": "^0.27.2",
"copy-to-clipboard": "^3.3.2",
"dayjs": "^1.11.3",
"emoji-picker-react": "^3.6.2",
"highlight.js": "^11.6.0",
"i18next": "^21.9.2",
"lodash-es": "^4.17.21",
@@ -25,7 +24,8 @@
"react-feather": "^2.0.10",
"react-i18next": "^11.18.6",
"react-redux": "^8.0.1",
"react-router-dom": "^6.4.0"
"react-router-dom": "^6.4.0",
"tailwindcss": "^3.2.4"
},
"devDependencies": {
"@jest/globals": "^29.1.2",
@@ -47,7 +47,6 @@
"lodash": "^4.17.21",
"postcss": "^8.4.5",
"prettier": "2.5.1",
"tailwindcss": "^3.0.18",
"ts-jest": "^29.0.3",
"typescript": "^4.3.2",
"vite": "^3.0.0"
+53 -18
View File
@@ -1,49 +1,84 @@
import { CssVarsProvider } from "@mui/joy/styles";
import { useEffect } from "react";
import { useColorScheme } from "@mui/joy";
import { useEffect, Suspense } from "react";
import { useTranslation } from "react-i18next";
import { RouterProvider } from "react-router-dom";
import { globalService, locationService } from "./services";
import { useAppSelector } from "./store";
import router from "./router";
import * as api from "./helpers/api";
import * as storage from "./helpers/storage";
import { getSystemColorScheme } from "./helpers/utils";
import Loading from "./pages/Loading";
function App() {
const { i18n } = useTranslation();
const global = useAppSelector((state) => state.global);
const { appearance, locale, systemStatus } = useAppSelector((state) => state.global);
const { mode, setMode } = useColorScheme();
useEffect(() => {
locationService.updateStateWithLocation();
window.onpopstate = () => {
locationService.updateStateWithLocation();
};
globalService.initialState();
}, []);
useEffect(() => {
api.getSystemStatus().then(({ data }) => {
const { data: status } = data;
if (status.additionalStyle) {
const styleEl = document.createElement("style");
styleEl.innerHTML = status.additionalStyle;
styleEl.setAttribute("type", "text/css");
document.head.appendChild(styleEl);
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
if (globalService.getState().appearance === "system") {
const mode = e.matches ? "dark" : "light";
setMode(mode);
}
});
}, []);
// Inject additional style and script codes.
useEffect(() => {
i18n.changeLanguage(global.locale);
if (systemStatus.additionalStyle) {
const styleEl = document.createElement("style");
styleEl.innerHTML = systemStatus.additionalStyle;
styleEl.setAttribute("type", "text/css");
document.head.appendChild(styleEl);
}
if (systemStatus.additionalScript) {
const scriptEl = document.createElement("script");
scriptEl.innerHTML = systemStatus.additionalScript;
document.head.appendChild(scriptEl);
}
}, [systemStatus]);
useEffect(() => {
document.documentElement.setAttribute("lang", locale);
i18n.changeLanguage(locale);
storage.set({
locale: global.locale,
locale: locale,
});
}, [global.locale]);
}, [locale]);
useEffect(() => {
storage.set({
appearance: appearance,
});
let currentAppearance = appearance;
if (appearance === "system") {
currentAppearance = getSystemColorScheme();
}
setMode(currentAppearance);
}, [appearance]);
useEffect(() => {
const root = document.documentElement;
if (mode === "light") {
root.classList.remove("dark");
} else if (mode === "dark") {
root.classList.add("dark");
}
}, [mode]);
return (
<CssVarsProvider>
<Suspense fallback={<Loading />}>
<RouterProvider router={router} />
</CssVarsProvider>
</Suspense>
);
}
+24 -34
View File
@@ -1,6 +1,5 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import * as api from "../helpers/api";
import { useAppSelector } from "../store";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import GitHubBadge from "./GitHubBadge";
@@ -10,23 +9,7 @@ type Props = DialogProps;
const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
const { t } = useTranslation();
const [profile, setProfile] = useState<Profile>();
useEffect(() => {
try {
api.getSystemStatus().then(({ data }) => {
const {
data: { profile },
} = data;
setProfile(profile);
});
} catch (error) {
setProfile({
mode: "dev",
version: "0.0.0",
});
}
}, []);
const profile = useAppSelector((state) => state.global.systemStatus.profile);
const handleCloseBtnClick = () => {
destroy();
@@ -35,29 +18,36 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
return (
<>
<div className="dialog-header-container">
<p className="title-text">
<span className="icon-text">🤠</span>
{t("common.about")}
<p className="title-text flex items-center">
<img className="w-7 h-auto mr-1" src="/logo.webp" alt="" />
{t("common.about")} memos
</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X />
</button>
</div>
<div className="dialog-content-container">
<img className="logo-img" src="/logo-full.webp" alt="" />
<p>{t("slogan")}</p>
<br />
<div className="addtion-info-container">
<div className="border-t mt-1 pt-2 flex flex-row justify-start items-center">
<span className=" text-gray-500 mr-2">Other projects:</span>
<a href="https://github.com/boojack/sticky-notes" className="flex items-center underline text-blue-600 hover:opacity-80">
<img
className="w-5 h-auto mr-1"
src="https://raw.githubusercontent.com/boojack/sticky-notes/main/public/sticky-notes.ico"
alt=""
/>
<span>Sticky notes</span>
</a>
</div>
<div className="mt-4 flex flex-row text-sm justify-start items-center">
<GitHubBadge />
{profile !== undefined && (
<>
{t("common.version")}:
<span className="pre-text">
{profile?.version}-{profile?.mode}
</span>
🎉
</>
)}
<span className="ml-2">
{t("common.version")}:
<span className="font-mono">
{profile.version}-{profile.mode}
</span>
🎉
</span>
</div>
</div>
</>
+52
View File
@@ -0,0 +1,52 @@
import { Option, Select } from "@mui/joy";
import { useTranslation } from "react-i18next";
import { globalService, userService } from "../services";
import { useAppSelector } from "../store";
import Icon from "./Icon";
const appearanceList = ["system", "light", "dark"];
const AppearanceSelect = () => {
const user = useAppSelector((state) => state.user.user);
const appearance = useAppSelector((state) => state.global.appearance);
const { t } = useTranslation();
const getPrefixIcon = (apperance: Appearance) => {
const className = "w-4 h-auto";
if (apperance === "light") {
return <Icon.Sun className={className} />;
} else if (apperance === "dark") {
return <Icon.Moon className={className} />;
} else {
return <Icon.Smile className={className} />;
}
};
const handleSelectChange = async (appearance: Appearance) => {
if (user) {
await userService.upsertUserSetting("appearance", appearance);
}
globalService.setAppearance(appearance);
};
return (
<Select
className="!min-w-[10rem] w-auto text-sm"
value={appearance}
onChange={(_, appearance) => {
if (appearance) {
handleSelectChange(appearance);
}
}}
startDecorator={getPrefixIcon(appearance)}
>
{appearanceList.map((item) => (
<Option key={item} value={item} className="whitespace-nowrap">
{t(`setting.apperance-option.${item}`)}
</Option>
))}
</Select>
);
};
export default AppearanceSelect;
+3 -3
View File
@@ -51,16 +51,16 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
};
return (
<div className={`memo-wrapper archived-memo ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
<div className={`memo-wrapper archived ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
<div className="memo-top-wrapper">
<span className="time-text">
{t("common.archived-at")} {utils.getDateTimeString(memo.updatedTs)}
</span>
<div className="btns-container">
<span className="btn restore-btn" onClick={handleRestoreMemoClick}>
<span className="btn-text" onClick={handleRestoreMemoClick}>
{t("common.restore")}
</span>
<span className={`btn delete-btn ${showConfirmDeleteBtn ? "final-confirm" : ""}`} onClick={handleDeleteMemoClick}>
<span className={`btn-text ${showConfirmDeleteBtn ? "final-confirm" : ""}`} onClick={handleDeleteMemoClick}>
{t("common.delete")}
{showConfirmDeleteBtn ? "!" : ""}
</span>
@@ -0,0 +1,125 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { validate, ValidatorConfig } from "../helpers/validator";
import { userService } from "../services";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
const validateConfig: ValidatorConfig = {
minLength: 4,
maxLength: 320,
noSpace: true,
noChinese: true,
};
interface Props extends DialogProps {
user: User;
}
const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
const { user: propsUser, destroy } = props;
const { t } = useTranslation();
const [newPassword, setNewPassword] = useState("");
const [newPasswordAgain, setNewPasswordAgain] = useState("");
useEffect(() => {
// do nth
}, []);
const handleCloseBtnClick = () => {
destroy();
};
const handleNewPasswordChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value as string;
setNewPassword(text);
};
const handleNewPasswordAgainChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = e.target.value as string;
setNewPasswordAgain(text);
};
const handleSaveBtnClick = async () => {
if (newPassword === "" || newPasswordAgain === "") {
toastHelper.error(t("message.fill-all"));
return;
}
if (newPassword !== newPasswordAgain) {
toastHelper.error(t("message.new-password-not-match"));
setNewPasswordAgain("");
return;
}
const passwordValidResult = validate(newPassword, validateConfig);
if (!passwordValidResult.result) {
toastHelper.error(`${t("common.password")} ${t(passwordValidResult.reason as string)}`);
return;
}
try {
await userService.patchUser({
id: propsUser.id,
password: newPassword,
});
toastHelper.info(t("message.password-changed"));
handleCloseBtnClick();
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.message);
}
};
return (
<>
<div className="dialog-header-container !w-64">
<p className="title-text">
{t("setting.account-section.change-password")} ({propsUser.username})
</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X />
</button>
</div>
<div className="dialog-content-container">
<p className="text-sm mb-1">{t("common.new-password")}</p>
<input
type="password"
className="input-text"
placeholder={t("common.repeat-new-password")}
value={newPassword}
onChange={handleNewPasswordChanged}
/>
<p className="text-sm mb-1 mt-2">{t("common.repeat-new-password")}</p>
<input
type="password"
className="input-text"
placeholder={t("common.repeat-new-password")}
value={newPasswordAgain}
onChange={handleNewPasswordAgainChanged}
/>
<div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
<span className="btn-text" onClick={handleCloseBtnClick}>
{t("common.cancel")}
</span>
<span className="btn-primary" onClick={handleSaveBtnClick}>
{t("common.save")}
</span>
</div>
</div>
</>
);
};
function showChangeMemberPasswordDialog(user: User) {
generateDialog(
{
className: "change-member-password-dialog",
},
ChangeMemberPasswordDialog,
{ user }
);
}
export default showChangeMemberPasswordDialog;
@@ -69,14 +69,21 @@ const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
</button>
</div>
<div className="dialog-content-container">
<label className="form-label input-form-label">
<input type="datetime-local" value={createdAt} max={maxDatetimeValue} onChange={handleDatetimeInputChange} />
</label>
<p className="w-full bg-yellow-100 border border-yellow-400 rounded p-2 text-xs leading-4">
THIS IS NOT A NORMAL BEHAVIOR. PLEASE MAKE SURE YOU REALLY NEED IT.
</p>
<input
className="input-text mt-2"
type="datetime-local"
value={createdAt}
max={maxDatetimeValue}
onChange={handleDatetimeInputChange}
/>
<div className="btns-container">
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
<span className="btn-text" onClick={handleCloseBtnClick}>
{t("common.cancel")}
</span>
<span className="btn confirm-btn" onClick={handleSaveBtnClick}>
<span className="btn-primary" onClick={handleSaveBtnClick}>
{t("common.save")}
</span>
</div>
+24 -18
View File
@@ -5,11 +5,10 @@ import { userService } from "../services";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
import "../less/change-password-dialog.less";
const validateConfig: ValidatorConfig = {
minLength: 4,
maxLength: 24,
maxLength: 320,
noSpace: true,
noChinese: true,
};
@@ -53,7 +52,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
const passwordValidResult = validate(newPassword, validateConfig);
if (!passwordValidResult.result) {
toastHelper.error(`${t("common.password")} ${passwordValidResult.reason}`);
toastHelper.error(`${t("common.password")} ${t(passwordValidResult.reason as string)}`);
return;
}
@@ -73,29 +72,36 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
return (
<>
<div className="dialog-header-container">
<div className="dialog-header-container !w-64">
<p className="title-text">{t("setting.account-section.change-password")}</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X />
</button>
</div>
<div className="dialog-content-container">
<label className="form-label input-form-label">
<input type="password" placeholder={t("common.new-password")} value={newPassword} onChange={handleNewPasswordChanged} />
</label>
<label className="form-label input-form-label">
<input
type="password"
placeholder={t("common.repeat-new-password")}
value={newPasswordAgain}
onChange={handleNewPasswordAgainChanged}
/>
</label>
<div className="btns-container">
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
<p className="text-sm mb-1">{t("common.new-password")}</p>
<input
type="password"
autoComplete="new-password"
className="input-text"
placeholder={t("common.repeat-new-password")}
value={newPassword}
onChange={handleNewPasswordChanged}
/>
<p className="text-sm mb-1 mt-2">{t("common.repeat-new-password")}</p>
<input
type="password"
autoComplete="new-password"
className="input-text"
placeholder={t("common.repeat-new-password")}
value={newPasswordAgain}
onChange={handleNewPasswordAgainChanged}
/>
<div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
<span className="btn-text" onClick={handleCloseBtnClick}>
{t("common.cancel")}
</span>
<span className="btn confirm-btn" onClick={handleSaveBtnClick}>
<span className="btn-primary" onClick={handleSaveBtnClick}>
{t("common.save")}
</span>
</div>
@@ -68,16 +68,14 @@ const ChangeResourceFilenameDialog: React.FC<Props> = (props: Props) => {
</button>
</div>
<div className="dialog-content-container">
<label className="form-label input-form-label">
<input type="text" value={filename} onChange={handleFilenameChanged} />
</label>
<input className="input-text" type="text" value={filename} onChange={handleFilenameChanged} />
<div className="btns-container">
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
<button className="btn-text" onClick={handleCloseBtnClick}>
{t("common.cancel")}
</span>
<span className="btn confirm-btn" onClick={handleSaveBtnClick}>
</button>
<button className="btn-primary" onClick={handleSaveBtnClick}>
{t("common.save")}
</span>
</button>
</div>
</div>
</>
+12 -12
View File
@@ -1,14 +1,14 @@
import dayjs from "dayjs";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { memoService, shortcutService } from "../services";
import { checkShouldShowMemoWithFilters, filterConsts, getDefaultFilter, relationConsts } from "../helpers/filter";
import { filterConsts, getDefaultFilter, relationConsts } from "../helpers/filter";
import useLoading from "../hooks/useLoading";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
import Selector from "./common/Selector";
import "../less/create-shortcut-dialog.less";
import dayjs from "dayjs";
interface Props extends DialogProps {
shortcutId?: ShortcutId;
@@ -21,10 +21,6 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
const requestState = useLoading(false);
const { t } = useTranslation();
const shownMemoLength = memoService.getState().memos.filter((memo) => {
return checkShouldShowMemoWithFilters(memo, filters);
}).length;
useEffect(() => {
if (shortcutId) {
const shortcutTemp = shortcutService.getShortcutById(shortcutId);
@@ -48,7 +44,12 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
toastHelper.error(t("shortcut-list.title-required"));
return;
}
for (const filter of filters) {
if (!filter.value.value) {
toastHelper.error(t("shortcut-list.value-required"));
return;
}
}
try {
if (shortcutId) {
await shortcutService.patchShortcut({
@@ -141,10 +142,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
<div className="dialog-footer-container">
<div></div>
<div className="btns-container">
<span className={`tip-text ${filters.length === 0 && "hidden"}`}>
<strong>{shownMemoLength}</strong> {t("shortcut-list.eligible-memo")}
</span>
<button className={`btn save-btn ${requestState.isLoading ? "requesting" : ""}`} onClick={handleSaveBtnClick}>
<button className={`btn-primary ${requestState.isLoading ? "requesting" : ""}`} onClick={handleSaveBtnClick}>
{t("common.save")}
</button>
</div>
@@ -173,6 +171,8 @@ const MemoFilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterIn
const valueDataSource =
type === "TYPE"
? filterConsts["TYPE"].values.map(({ text, value }) => ({ text: t(text), value }))
: type === "VISIBILITY"
? filterConsts["VISIBILITY"].values.map(({ text, value }) => ({ text: t(text), value }))
: tags.sort().map((t) => {
return { text: t, value: t };
});
@@ -192,7 +192,7 @@ const MemoFilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterIn
if (["AND", "OR"].includes(value)) {
handleFilterChange(index, {
...filter,
relation: value as MemoFilterRalation,
relation: value as MemoFilterRelation,
});
}
};
+1 -1
View File
@@ -21,7 +21,7 @@ const DailyMemo: React.FC<Props> = (props: Props) => {
</div>
<div className="memo-container">
<MemoContent content={memo.content} displayConfig={displayConfig} />
<MemoResources resourceList={memo.resourceList} />
<MemoResources resourceList={memo.resourceList} style="col" />
</div>
<div className="split-line"></div>
</div>
+1 -2
View File
@@ -16,7 +16,7 @@ interface Props extends DialogProps {
currentDateStamp: DateStamp;
}
const monthChineseStrArray = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dev"];
const monthChineseStrArray = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"];
const weekdayChineseStrArray = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const DailyReviewDialog: React.FC<Props> = (props: Props) => {
@@ -43,7 +43,6 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
toggleShowDatePicker(false);
toImage(memosElRef.current, {
backgroundColor: "#ffffff",
pixelRatio: window.devicePixelRatio * 2,
})
.then((url) => {
+7 -3
View File
@@ -4,6 +4,8 @@ import { Provider } from "react-redux";
import { ANIMATION_DURATION } from "../../helpers/consts";
import store from "../../store";
import "../../less/base-dialog.less";
import { CssVarsProvider } from "@mui/joy";
import theme from "../../theme";
interface DialogConfig {
className: string;
@@ -77,9 +79,11 @@ export function generateDialog<T extends DialogProps>(
const Fragment = (
<Provider store={store}>
<BaseDialog destroy={cbs.destroy} clickSpaceDestroy={true} {...config}>
<DialogComponent {...dialogProps} />
</BaseDialog>
<CssVarsProvider theme={theme}>
<BaseDialog destroy={cbs.destroy} clickSpaceDestroy={true} {...config}>
<DialogComponent {...dialogProps} />
</BaseDialog>
</CssVarsProvider>
</Provider>
);
+15 -1
View File
@@ -5,6 +5,7 @@ import "../../less/editor.less";
export interface EditorRefActions {
focus: FunctionType;
insertText: (text: string, prefix?: string, suffix?: string) => void;
removeText: (start: number, length: number) => void;
setContent: (text: string) => void;
getContent: () => string;
getSelectedContent: () => string;
@@ -63,7 +64,20 @@ const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef<
editorRef.current.value = value;
editorRef.current.focus();
editorRef.current.selectionEnd = endPosition + prefix.length + suffix.length + content.length;
editorRef.current.selectionEnd = endPosition + prefix.length + content.length;
handleContentChangeCallback(editorRef.current.value);
refresh();
},
removeText: (start: number, length: number) => {
if (!editorRef.current) {
return;
}
const prevValue = editorRef.current.value;
const value = prevValue.slice(0, start) + prevValue.slice(start + length);
editorRef.current.value = value;
editorRef.current.focus();
editorRef.current.selectionEnd = start;
handleContentChangeCallback(editorRef.current.value);
refresh();
},
-38
View File
@@ -1,38 +0,0 @@
import Picker, { IEmojiPickerProps } from "emoji-picker-react";
import { useEffect } from "react";
interface Props {
shouldShow: boolean;
onEmojiClick: IEmojiPickerProps["onEmojiClick"];
onShouldShowEmojiPickerChange: (status: boolean) => void;
}
export const EmojiPicker: React.FC<Props> = (props: Props) => {
const { shouldShow, onEmojiClick, onShouldShowEmojiPickerChange } = props;
useEffect(() => {
if (shouldShow) {
const handleClickOutside = (event: MouseEvent) => {
event.stopPropagation();
const emojiWrapper = document.querySelector(".emoji-picker-react");
const isContains = emojiWrapper?.contains(event.target as Node);
if (!isContains) {
onShouldShowEmojiPickerChange(false);
}
};
window.addEventListener("click", handleClickOutside, {
capture: true,
once: true,
});
}
}, [shouldShow]);
return (
<div className={`emoji-picker ${shouldShow ? "" : "hidden"}`}>
<Picker onEmojiClick={onEmojiClick} disableSearchBar />
</div>
);
};
export default EmojiPicker;
+7 -5
View File
@@ -1,7 +1,6 @@
import { useEffect, useState } from "react";
import * as api from "../helpers/api";
import Icon from "./Icon";
import "../less/github-badge.less";
const GitHubBadge = () => {
const [starCount, setStarCount] = useState(0);
@@ -13,12 +12,15 @@ const GitHubBadge = () => {
}, []);
return (
<a className="github-badge-container" href="https://github.com/usememos/memos">
<div className="github-icon">
<Icon.GitHub className="icon-img" />
<a
className="h-7 flex flex-row justify-start items-center border dark:border-zinc-600 rounded cursor-pointer hover:opacity-80"
href="https://github.com/usememos/memos"
>
<div className="apply w-auto h-full px-2 flex flex-row justify-center items-center text-xs bg-gray-100 dark:bg-zinc-700">
<Icon.GitHub className="mr-1 w-4 h-4" />
Star
</div>
<div className="count-text">{starCount || ""}</div>
<div className="w-auto h-full flex flex-row justify-center items-center px-3 text-xs font-bold">{starCount || ""}</div>
</a>
);
};
+31 -49
View File
@@ -1,25 +1,21 @@
import copy from "copy-to-clipboard";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { memo, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import "dayjs/locale/zh";
import { UNKNOWN_ID } from "../helpers/consts";
import { editorStateService, locationService, memoService, userService } from "../services";
import Icon from "./Icon";
import toastHelper from "./Toast";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources";
import showMemoCardDialog from "./MemoCardDialog";
import showShareMemoImageDialog from "./ShareMemoImageDialog";
import showShareMemo from "./ShareMemoDialog";
import showPreviewImageDialog from "./PreviewImageDialog";
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
import "../less/memo.less";
dayjs.extend(relativeTime);
interface Props {
memo: Memo;
highlightWord?: string;
}
export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
@@ -31,7 +27,7 @@ export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
};
const Memo: React.FC<Props> = (props: Props) => {
const memo = props.memo;
const { memo, highlightWord } = props;
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const [displayTimeStr, setDisplayTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.displayTs, i18n.language));
@@ -51,14 +47,6 @@ const Memo: React.FC<Props> = (props: Props) => {
};
}, [i18n.language]);
const handleShowMemoStoryDialog = () => {
if (isVisitorMode) {
return;
}
showMemoCardDialog(memo);
};
const handleViewMemoDetailPage = () => {
navigate(`/m/${memo.id}`);
};
@@ -80,10 +68,6 @@ const Memo: React.FC<Props> = (props: Props) => {
}
};
const handleMarkMemoClick = () => {
editorStateService.setMarkMemoWithId(memo.id);
};
const handleEditMemoClick = () => {
editorStateService.setEditMemoWithId(memo.id);
};
@@ -105,23 +89,13 @@ const Memo: React.FC<Props> = (props: Props) => {
};
const handleGenMemoImageBtnClick = () => {
showShareMemoImageDialog(memo);
showShareMemo(memo);
};
const handleMemoContentClick = async (e: React.MouseEvent) => {
const targetEl = e.target as HTMLElement;
if (targetEl.className === "memo-link-text") {
const memoId = targetEl.dataset?.value;
const memoTemp = await memoService.getMemoById(Number(memoId) ?? UNKNOWN_ID);
if (memoTemp) {
showMemoCardDialog(memoTemp);
} else {
toastHelper.error(t("message.memo-not-found"));
targetEl.classList.remove("memo-link-text");
}
} else if (targetEl.className === "tag-span") {
if (targetEl.className === "tag-span") {
const tagName = targetEl.innerText.slice(1);
const currTagQuery = locationService.getState().query?.tag;
if (currTagQuery === tagName) {
@@ -161,15 +135,9 @@ const Memo: React.FC<Props> = (props: Props) => {
}
}
} else if (targetEl.tagName === "IMG") {
const currImgUrl = targetEl.getAttribute("src");
if (currImgUrl) {
// use regex to get all image urls from memo content
const imageUrls = memo.content.match(/!\[.*?\]\((.*?)\)/g)?.map((item) => item.match(/\((.*?)\)/)?.[1] ?? "") ?? [];
showPreviewImageDialog(
imageUrls,
imageUrls.findIndex((item) => item === currImgUrl)
);
const imgUrl = targetEl.getAttribute("src");
if (imgUrl) {
showPreviewImageDialog([imgUrl], 0);
}
}
};
@@ -177,9 +145,7 @@ const Memo: React.FC<Props> = (props: Props) => {
const handleMemoContentDoubleClick = (e: React.MouseEvent) => {
const targetEl = e.target as HTMLElement;
if (targetEl.className === "memo-link-text") {
return;
} else if (targetEl.className === "tag-span") {
if (targetEl.className === "tag-span") {
return;
} else if (targetEl.classList.contains("todo-block")) {
return;
@@ -188,16 +154,34 @@ const Memo: React.FC<Props> = (props: Props) => {
editorStateService.setEditMemoWithId(memo.id);
};
const handleMemoDisplayTimeClick = () => {
showChangeMemoCreatedTsDialog(memo.id);
};
const handleMemoVisibilityClick = (visibility: Visibility) => {
const currVisibilityQuery = locationService.getState().query?.visibility;
if (currVisibilityQuery === visibility) {
locationService.setMemoVisibilityQuery(undefined);
} else {
locationService.setMemoVisibilityQuery(visibility);
}
};
return (
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`} ref={memoContainerRef}>
{memo.pinned && <div className="corner-container"></div>}
<div className="memo-top-wrapper">
<div className="status-text-container">
<span className="time-text" onClick={handleShowMemoStoryDialog}>
<span className="time-text" onDoubleClick={handleMemoDisplayTimeClick}>
{displayTimeStr}
</span>
{memo.visibility !== "PRIVATE" && !isVisitorMode && (
<span className={`status-text ${memo.visibility.toLocaleLowerCase()}`}>{memo.visibility}</span>
<span
className={`status-text ${memo.visibility.toLocaleLowerCase()}`}
onClick={() => handleMemoVisibilityClick(memo.visibility)}
>
{memo.visibility}
</span>
)}
</div>
{!isVisitorMode && (
@@ -221,9 +205,6 @@ const Memo: React.FC<Props> = (props: Props) => {
<span className="tip-text">{t("common.share")}</span>
</div>
</div>
<span className="btn" onClick={handleMarkMemoClick}>
{t("common.mark")}
</span>
<span className="btn" onClick={handleCopyContent}>
{t("memo.copy")}
</span>
@@ -240,6 +221,7 @@ const Memo: React.FC<Props> = (props: Props) => {
</div>
<MemoContent
content={memo.content}
highlightWord={highlightWord}
onMemoContentClick={handleMemoContentClick}
onMemoContentDoubleClick={handleMemoContentDoubleClick}
/>
-260
View File
@@ -1,260 +0,0 @@
import copy from "copy-to-clipboard";
import { useState, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { editorStateService, memoService, userService } from "../services";
import { useAppSelector } from "../store";
import { UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
import * as utils from "../helpers/utils";
import { parseHTMLToRawText } from "../helpers/utils";
import { marked } from "../labs/marked";
import { MARK_REG } from "../labs/marked/parser";
import toastHelper from "./Toast";
import { generateDialog } from "./Dialog";
import Icon from "./Icon";
import Selector from "./common/Selector";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources";
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
import "../less/memo-card-dialog.less";
interface LinkedMemo extends Memo {
createdAtStr: string;
dateStr: string;
}
interface Props extends DialogProps {
memo: Memo;
}
const MemoCardDialog: React.FC<Props> = (props: Props) => {
const { t } = useTranslation();
const memos = useAppSelector((state) => state.memo.memos);
const [memo, setMemo] = useState<Memo>({
...props.memo,
});
const [linkMemos, setLinkMemos] = useState<LinkedMemo[]>([]);
const [linkedMemos, setLinkedMemos] = useState<LinkedMemo[]>([]);
const isVisitorMode = userService.isVisitorMode();
const visibilitySelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
return {
value: item.value,
text: t(`memo.visibility.${item.text.toLowerCase()}`),
};
});
useEffect(() => {
const fetchLinkedMemos = async () => {
try {
const linkMemos: LinkedMemo[] = [];
const matchedArr = [...memo.content.matchAll(MARK_REG)];
for (const matchRes of matchedArr) {
if (matchRes && matchRes.length === 3) {
const id = Number(matchRes[2]);
if (id === memo.id) {
continue;
}
const memoTemp = await memoService.getMemoById(id);
if (memoTemp) {
linkMemos.push({
...memoTemp,
createdAtStr: utils.getDateTimeString(memoTemp.displayTs),
dateStr: utils.getDateString(memoTemp.displayTs),
});
}
}
}
setLinkMemos([...linkMemos]);
const linkedMemos = await memoService.getLinkedMemos(memo.id);
setLinkedMemos(
linkedMemos
.filter((m) => m.rowStatus === "NORMAL" && m.id !== memo.id)
.sort((a, b) => utils.getTimeStampByDate(b.displayTs) - utils.getTimeStampByDate(a.displayTs))
.map((m) => ({
...m,
createdAtStr: utils.getDateTimeString(m.displayTs),
dateStr: utils.getDateString(m.displayTs),
}))
);
} catch (error) {
// do nth
}
};
fetchLinkedMemos();
memoService.getMemoById(memo.id).then((memo) => {
setMemo(memo);
});
}, [memos, memo.id]);
const handleMemoCreatedAtClick = () => {
if (isVisitorMode) {
return;
}
showChangeMemoCreatedTsDialog(memo.id);
};
const handleMemoContentClick = useCallback(async (e: React.MouseEvent) => {
const targetEl = e.target as HTMLElement;
if (targetEl.className === "memo-link-text") {
const nextMemoId = targetEl.dataset?.value;
const memoTemp = await memoService.getMemoById(Number(nextMemoId) ?? UNKNOWN_ID);
if (memoTemp) {
const nextMemo = {
...memoTemp,
createdAtStr: utils.getDateTimeString(memoTemp.displayTs),
};
setLinkMemos([]);
setLinkedMemos([]);
setMemo(nextMemo);
} else {
toastHelper.error(t("message.memo-not-found"));
targetEl.classList.remove("memo-link-text");
}
}
}, []);
const handleLinkedMemoClick = useCallback((memo: Memo) => {
setLinkMemos([]);
setLinkedMemos([]);
setMemo(memo);
}, []);
const handleGotoMemoLinkBtnClick = () => {
window.open(`/m/${memo.id}`);
};
const handleEditMemoBtnClick = () => {
props.destroy();
editorStateService.setEditMemoWithId(memo.id);
};
const handleCopyContent = () => {
copy(memo.content);
toastHelper.success(t("message.succeed-copy-content"));
};
const handleVisibilitySelectorChange = async (visibility: Visibility) => {
if (memo.visibility === visibility) {
return;
}
await memoService.patchMemo({
id: memo.id,
visibility: visibility,
});
setMemo({
...memo,
visibility: visibility,
});
};
return (
<>
{!isVisitorMode && (
<div className="card-header-container">
<div className="visibility-selector-container">
<Icon.Eye className="icon-img" />
<Selector
className="visibility-selector"
dataSource={visibilitySelectorItems}
value={memo.visibility}
handleValueChanged={(value) => handleVisibilitySelectorChange(value as Visibility)}
/>
</div>
</div>
)}
<div className="memo-card-container">
<div className="header-container">
<p className="time-text" onClick={handleMemoCreatedAtClick}>
{utils.getDateTimeString(memo.displayTs)}
</p>
<div className="btns-container">
{!isVisitorMode && (
<>
<button className="btn edit-btn" onClick={handleGotoMemoLinkBtnClick}>
<Icon.ExternalLink className="icon-img" />
</button>
<button className="btn copy-btn" onClick={handleCopyContent}>
<Icon.Clipboard className="icon-img" />
</button>
<button className="btn edit-btn" onClick={handleEditMemoBtnClick}>
<Icon.Edit3 className="icon-img" />
</button>
<span className="split-line">/</span>
</>
)}
<button className="btn close-btn" onClick={props.destroy}>
<Icon.X className="icon-img" />
</button>
</div>
</div>
<div className="memo-container">
<MemoContent displayConfig={{ enableExpand: false }} content={memo.content} onMemoContentClick={handleMemoContentClick} />
<MemoResources resourceList={memo.resourceList} />
</div>
<div className="layer-container"></div>
{linkMemos.map((_, idx) => {
if (idx < 4) {
return (
<div
className="background-layer-container"
key={idx}
style={{
bottom: (idx + 1) * -3 + "px",
left: (idx + 1) * 5 + "px",
width: `calc(100% - ${(idx + 1) * 10}px)`,
zIndex: -idx - 1,
}}
></div>
);
} else {
return null;
}
})}
</div>
{linkMemos.length > 0 ? (
<div className="linked-memos-wrapper">
<p className="normal-text">{linkMemos.length} related MEMO</p>
{linkMemos.map((memo, index) => {
const rawtext = parseHTMLToRawText(marked(memo.content)).replaceAll("\n", " ");
return (
<div className="linked-memo-container" key={`${index}-${memo.id}`} onClick={() => handleLinkedMemoClick(memo)}>
<span className="time-text">{memo.dateStr} </span>
{rawtext}
</div>
);
})}
</div>
) : null}
{linkedMemos.length > 0 ? (
<div className="linked-memos-wrapper">
<p className="normal-text">{linkedMemos.length} linked MEMO</p>
{linkedMemos.map((memo, index) => {
const rawtext = parseHTMLToRawText(marked(memo.content)).replaceAll("\n", " ");
return (
<div className="linked-memo-container" key={`${index}-${memo.id}`} onClick={() => handleLinkedMemoClick(memo)}>
<span className="time-text">{memo.dateStr} </span>
{rawtext}
</div>
);
})}
</div>
) : null}
</>
);
};
export default function showMemoCardDialog(memo: Memo): void {
generateDialog(
{
className: "memo-card-dialog",
},
MemoCardDialog,
{ memo }
);
}
+22 -8
View File
@@ -1,7 +1,9 @@
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { marked } from "../labs/marked";
import { highlightWithWord } from "../labs/highlighter";
import Icon from "./Icon";
import { useAppSelector } from "../store";
import "../less/memo-content.less";
export interface DisplayConfig {
@@ -10,6 +12,7 @@ export interface DisplayConfig {
interface Props {
content: string;
highlightWord?: string;
className?: string;
displayConfig?: Partial<DisplayConfig>;
onMemoContentClick?: (e: React.MouseEvent) => void;
@@ -26,11 +29,15 @@ const defaultDisplayConfig: DisplayConfig = {
enableExpand: true,
};
const MAX_MEMO_CONTAINER_HEIGHT = 384;
const MemoContent: React.FC<Props> = (props: Props) => {
const { className, content, onMemoContentClick, onMemoContentDoubleClick } = props;
const { className, content, highlightWord, onMemoContentClick, onMemoContentDoubleClick } = props;
const foldedContent = useMemo(() => {
const firstHorizontalRuleIndex = content.search(/^---$|^\*\*\*$|^___$/m);
return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content;
}, [content]);
const { t } = useTranslation();
const user = useAppSelector((state) => state.user.user);
const [state, setState] = useState<State>({
expandButtonStatus: -1,
});
@@ -45,15 +52,20 @@ const MemoContent: React.FC<Props> = (props: Props) => {
return;
}
if (displayConfig.enableExpand) {
if (Number(memoContentContainerRef.current?.clientHeight) > MAX_MEMO_CONTAINER_HEIGHT) {
if (displayConfig.enableExpand && user && user.localSetting.enableFoldMemo) {
if (foldedContent.length !== content.length) {
setState({
...state,
expandButtonStatus: 0,
});
}
} else {
setState({
...state,
expandButtonStatus: -1,
});
}
}, []);
}, [user?.localSetting.enableFoldMemo]);
const handleMemoContentClick = async (e: React.MouseEvent) => {
if (onMemoContentClick) {
@@ -81,7 +93,9 @@ const MemoContent: React.FC<Props> = (props: Props) => {
className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
onClick={handleMemoContentClick}
onDoubleClick={handleMemoContentDoubleClick}
dangerouslySetInnerHTML={{ __html: marked(content) }}
dangerouslySetInnerHTML={{
__html: highlightWithWord(marked(state.expandButtonStatus === 0 ? foldedContent : content), highlightWord),
}}
></div>
{state.expandButtonStatus !== -1 && (
<div className="expand-btn-container">
+120 -143
View File
@@ -1,4 +1,4 @@
import { IEmojiData } from "emoji-picker-react";
import { last, toLower } from "lodash";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { deleteMemoResource, upsertMemoResource } from "../helpers/api";
@@ -10,10 +10,12 @@ import Icon from "./Icon";
import toastHelper from "./Toast";
import Selector from "./common/Selector";
import Editor, { EditorRefActions } from "./Editor/Editor";
import EmojiPicker from "./Editor/EmojiPicker";
import { toLower } from "lodash";
import ResourceIcon from "./ResourceIcon";
import showResourcesSelectorDialog from "./ResourcesSelectorDialog";
import "../less/memo-editor.less";
const listItemSymbolList = ["* ", "- ", "- [ ] ", "- [x] ", "- [X] "];
const getEditorContentCache = (): string => {
return storage.get(["editorContentCache"]).editorContentCache ?? "";
};
@@ -33,12 +35,10 @@ const setEditingMemoVisibilityCache = (visibility: Visibility) => {
interface State {
fullscreen: boolean;
isUploadingResource: boolean;
resourceList: Resource[];
shouldShowEmojiPicker: boolean;
isEditorFocused: boolean;
}
const MemoEditor: React.FC = () => {
const MemoEditor = () => {
const { t, i18n } = useTranslation();
const user = useAppSelector((state) => state.user.user as User);
const setting = user.setting;
@@ -48,14 +48,11 @@ const MemoEditor: React.FC = () => {
isUploadingResource: false,
fullscreen: false,
shouldShowEmojiPicker: false,
resourceList: [],
isEditorFocused: false,
});
const [allowSave, setAllowSave] = useState<boolean>(false);
const prevGlobalStateRef = useRef(editorState);
const prevEditorStateRef = useRef(editorState);
const editorRef = useRef<EditorRefActions>(null);
const tagSeletorRef = useRef<HTMLDivElement>(null);
const mobileEditorStyle = user?.setting.mobileEditorStyle || "normal";
const tagSelectorRef = useRef<HTMLDivElement>(null);
const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
return {
value: item.value,
@@ -75,28 +72,14 @@ const MemoEditor: React.FC = () => {
}
}, []);
useEffect(() => {
if (editorState.markMemoId && editorState.markMemoId !== UNKNOWN_ID) {
const editorCurrentValue = editorRef.current?.getContent();
const memoLinkText = `${editorCurrentValue ? "\n" : ""}Mark: @[MEMO](${editorState.markMemoId})`;
editorRef.current?.insertText(memoLinkText);
editorStateService.clearMarkMemo();
}
}, [editorState.markMemoId]);
useEffect(() => {
if (editorState.editMemoId) {
memoService.getMemoById(editorState.editMemoId ?? UNKNOWN_ID).then((memo) => {
if (memo) {
handleEditorFocus();
editorStateService.setMemoVisibility(memo.visibility);
editorStateService.setResourceList(memo.resourceList);
editorRef.current?.setContent(memo.content ?? "");
setState((state) => {
return {
...state,
resourceList: memo.resourceList,
};
});
}
});
storage.set({
@@ -106,19 +89,14 @@ const MemoEditor: React.FC = () => {
storage.remove(["editingMemoIdCache"]);
}
prevGlobalStateRef.current = editorState;
prevEditorStateRef.current = editorState;
}, [editorState.editMemoId]);
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Escape" && state.fullscreen) {
handleFullscreenBtnClick();
return;
}
if (event.key === "Tab") {
event.preventDefault();
editorRef.current?.insertText(" ".repeat(TAB_SPACE_WIDTH));
if (!editorRef.current) {
return;
}
if (event.ctrlKey || event.metaKey) {
if (event.key === "Enter") {
handleSaveBtnClick();
@@ -126,20 +104,53 @@ const MemoEditor: React.FC = () => {
}
if (event.key === "b") {
event.preventDefault();
editorRef.current?.insertText("", "**", "**");
editorRef.current.insertText("", "**", "**");
return;
}
if (event.key === "i") {
event.preventDefault();
editorRef.current?.insertText("", "*", "*");
editorRef.current.insertText("", "*", "*");
return;
}
if (event.key === "e") {
event.preventDefault();
editorRef.current?.insertText("", "`", "`");
editorRef.current.insertText("", "`", "`");
return;
}
}
if (event.key === "Enter") {
const cursorPosition = editorRef.current.getCursorPosition();
const contentBeforeCursor = editorRef.current.getContent().slice(0, cursorPosition);
const rowValue = last(contentBeforeCursor.split("\n"));
if (rowValue) {
if (listItemSymbolList.includes(rowValue)) {
event.preventDefault();
editorRef.current.removeText(cursorPosition - rowValue.length, rowValue.length);
} else {
for (const listItemSymbol of listItemSymbolList) {
if (rowValue.startsWith(listItemSymbol)) {
event.preventDefault();
editorRef.current.insertText("", `\n${listItemSymbol}`);
}
}
}
}
return;
}
if (event.key === "Escape") {
if (state.fullscreen) {
handleFullscreenBtnClick();
} else if (editorState.editMemoId) {
handleCancelEdit();
}
return;
}
if (event.key === "Tab") {
event.preventDefault();
editorRef.current.insertText(" ".repeat(TAB_SPACE_WIDTH));
return;
}
};
const handleDropEvent = async (event: React.DragEvent) => {
@@ -155,10 +166,7 @@ const MemoEditor: React.FC = () => {
}
}
}
setState({
...state,
resourceList: [...state.resourceList, ...resourceList],
});
editorStateService.setResourceList([...editorState.resourceList, ...resourceList]);
}
};
@@ -168,18 +176,17 @@ const MemoEditor: React.FC = () => {
const file = event.clipboardData.files[0];
const resource = await handleUploadResource(file);
if (resource) {
setState({
...state,
resourceList: [...state.resourceList, resource],
});
editorStateService.setResourceList([...editorState.resourceList, resource]);
}
}
};
const handleUploadResource = async (file: File) => {
setState({
...state,
isUploadingResource: true,
setState((state) => {
return {
...state,
isUploadingResource: true,
};
});
let resource = undefined;
@@ -191,9 +198,11 @@ const MemoEditor: React.FC = () => {
toastHelper.error(error.response.data.message);
}
setState({
...state,
isUploadingResource: false,
setState((state) => {
return {
...state,
isUploadingResource: false,
};
});
return resource;
};
@@ -215,7 +224,7 @@ const MemoEditor: React.FC = () => {
id: prevMemo.id,
content,
visibility: editorState.memoVisibility,
resourceIdList: state.resourceList.map((resource) => resource.id),
resourceIdList: editorState.resourceList.map((resource) => resource.id),
});
}
editorStateService.clearEditMemo();
@@ -223,7 +232,7 @@ const MemoEditor: React.FC = () => {
await memoService.createMemo({
content,
visibility: editorState.memoVisibility,
resourceIdList: state.resourceList.map((resource) => resource.id),
resourceIdList: editorState.resourceList.map((resource) => resource.id),
});
locationService.clearQuery();
}
@@ -232,25 +241,26 @@ const MemoEditor: React.FC = () => {
toastHelper.error(error.response.data.message);
}
setState({
...state,
fullscreen: false,
resourceList: [],
setState((state) => {
return {
...state,
fullscreen: false,
};
});
editorStateService.clearResourceList();
setEditorContentCache("");
storage.remove(["editingMemoVisibilityCache"]);
editorRef.current?.setContent("");
};
const handleCancelEdit = () => {
setState({
...state,
resourceList: [],
});
editorStateService.clearEditMemo();
editorRef.current?.setContent("");
setEditorContentCache("");
storage.remove(["editingMemoVisibilityCache"]);
if (editorState.editMemoId) {
editorStateService.clearEditMemo();
editorStateService.clearResourceList();
editorRef.current?.setContent("");
setEditorContentCache("");
storage.remove(["editingMemoVisibilityCache"]);
}
};
const handleContentChange = (content: string) => {
@@ -258,10 +268,6 @@ const MemoEditor: React.FC = () => {
setEditorContentCache(content);
};
const handleEmojiPickerBtnClick = () => {
handleChangeShouldShowEmojiPicker(!state.shouldShowEmojiPicker);
};
const handleCheckBoxBtnClick = () => {
if (!editorRef.current) {
return;
@@ -314,51 +320,30 @@ const MemoEditor: React.FC = () => {
}
}
}
setState({
...state,
resourceList: [...state.resourceList, ...resourceList],
});
editorStateService.setResourceList([...editorState.resourceList, ...resourceList]);
document.body.removeChild(inputEl);
};
inputEl.click();
};
const handleFullscreenBtnClick = () => {
setState({
...state,
fullscreen: !state.fullscreen,
setState((state) => {
return {
...state,
fullscreen: !state.fullscreen,
};
});
};
const handleTagSeletorClick = useCallback((event: React.MouseEvent) => {
if (tagSeletorRef.current !== event.target && tagSeletorRef.current?.contains(event.target as Node)) {
const handleTagSelectorClick = useCallback((event: React.MouseEvent) => {
if (tagSelectorRef.current !== event.target && tagSelectorRef.current?.contains(event.target as Node)) {
editorRef.current?.insertText(`#${(event.target as HTMLElement).textContent} ` ?? "");
handleEditorFocus();
}
}, []);
const handleChangeShouldShowEmojiPicker = (status: boolean) => {
setState({
...state,
shouldShowEmojiPicker: status,
});
};
const handleEmojiClick = (_: any, emojiObject: IEmojiData) => {
if (!editorRef.current) {
return;
}
editorRef.current.insertText(`${emojiObject.emoji}`);
handleChangeShouldShowEmojiPicker(false);
};
const handleDeleteResource = async (resourceId: ResourceId) => {
setState({
...state,
resourceList: state.resourceList.filter((resource) => resource.id !== resourceId),
});
editorStateService.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId));
if (editorState.editMemoId) {
await deleteMemoResource(editorState.editMemoId, resourceId);
}
@@ -372,17 +357,10 @@ const MemoEditor: React.FC = () => {
const handleEditorFocus = () => {
editorRef.current?.focus();
setState({
...state,
isEditorFocused: true,
});
};
const handleEditorBlur = () => {
setState({
...state,
isEditorFocused: false,
});
// do nth
};
const isEditing = Boolean(editorState.editMemoId && editorState.editMemoId !== UNKNOWN_ID);
@@ -401,33 +379,19 @@ const MemoEditor: React.FC = () => {
return (
<div
className={`memo-editor-container ${mobileEditorStyle} ${isEditing ? "edit-ing" : ""} ${state.fullscreen ? "fullscreen" : ""}`}
className={`memo-editor-container ${isEditing ? "edit-ing" : ""} ${state.fullscreen ? "fullscreen" : ""}`}
tabIndex={0}
onKeyDown={handleKeyDown}
onDrop={handleDropEvent}
onFocus={handleEditorFocus}
onBlur={handleEditorBlur}
>
<div className="editor-header-container">
<Selector
className={`visibility-selector ${state.isEditorFocused || allowSave ? "" : "!hidden"}`}
value={editorState.memoVisibility}
dataSource={memoVisibilityOptionSelectorItems}
handleValueChanged={handleMemoVisibilityOptionChanged}
/>
<div className={`editing-container ${isEditing ? "" : "!hidden"}`}>
<span className="tip-text">{t("editor.editing")}</span>
<button className="cancel-btn" onClick={handleCancelEdit}>
{t("common.cancel")}
</button>
</div>
</div>
<Editor ref={editorRef} {...editorConfig} />
<div className="common-tools-wrapper">
<div className="common-tools-container">
<div className="action-btn tag-action">
<Icon.Hash className="icon-img" />
<div ref={tagSeletorRef} className="tag-list" onClick={handleTagSeletorClick}>
<div ref={tagSelectorRef} className="tag-list" onClick={handleTagSelectorClick}>
{tags.length > 0 ? (
tags.map((tag) => {
return (
@@ -443,41 +407,37 @@ const MemoEditor: React.FC = () => {
)}
</div>
</div>
<button className="action-btn !hidden sm:!flex ">
<Icon.Smile className="icon-img" onClick={handleEmojiPickerBtnClick} />
</button>
<button className="action-btn">
<Icon.CheckSquare className="icon-img" onClick={handleCheckBoxBtnClick} />
</button>
<button className="action-btn">
<Icon.Code className="icon-img" onClick={handleCodeBlockBtnClick} />
</button>
<button className="action-btn">
<Icon.FileText className="icon-img" onClick={handleUploadFileBtnClick} />
<div className="action-btn resource-btn">
<Icon.FileText className="icon-img" />
<span className={`tip-text ${state.isUploadingResource ? "!block" : ""}`}>Uploading</span>
</button>
<div className="resource-action-list">
<div className="resource-action-item" onClick={handleUploadFileBtnClick}>
<Icon.Upload className="icon-img" />
<span>{t("editor.local")}</span>
</div>
<div className="resource-action-item" onClick={showResourcesSelectorDialog}>
<Icon.Database className="icon-img" />
<span>{t("editor.resources")}</span>
</div>
</div>
</div>
<button className="action-btn" onClick={handleFullscreenBtnClick}>
{state.fullscreen ? <Icon.Minimize className="icon-img" /> : <Icon.Maximize className="icon-img" />}
</button>
<EmojiPicker
shouldShow={state.shouldShowEmojiPicker}
onEmojiClick={handleEmojiClick}
onShouldShowEmojiPickerChange={handleChangeShouldShowEmojiPicker}
/>
</div>
<div className="btns-container">
<button className="action-btn confirm-btn" disabled={!allowSave || state.isUploadingResource} onClick={handleSaveBtnClick}>
{t("editor.save")}
<img className="icon-img" src="/logo.webp" />
</button>
</div>
</div>
{state.resourceList.length > 0 && (
{editorState.resourceList && editorState.resourceList.length > 0 && (
<div className="resource-list-wrapper">
{state.resourceList.map((resource) => {
{editorState.resourceList.map((resource) => {
return (
<div key={resource.id} className="resource-container">
{resource.type.includes("image") ? <Icon.Image className="icon-img" /> : <Icon.FileText className="icon-img" />}
<ResourceIcon resourceType="resource.type" className="icon-img" />
<span className="name-text">{resource.filename}</span>
<Icon.X className="close-icon" onClick={() => handleDeleteResource(resource.id)} />
</div>
@@ -485,6 +445,23 @@ const MemoEditor: React.FC = () => {
})}
</div>
)}
<div className="editor-footer-container">
<Selector
className="visibility-selector"
value={editorState.memoVisibility}
dataSource={memoVisibilityOptionSelectorItems}
handleValueChanged={handleMemoVisibilityOptionChanged}
/>
<div className="buttons-container">
<button className={`action-btn cancel-btn ${isEditing ? "" : "!hidden"}`} onClick={handleCancelEdit}>
{t("editor.cancel-edit")}
</button>
<button className="action-btn confirm-btn" disabled={!allowSave || state.isUploadingResource} onClick={handleSaveBtnClick}>
{t("editor.save")}
<img className="icon-img w-4 h-auto" src="/logo.webp" />
</button>
</div>
</div>
</div>
);
};
+12 -3
View File
@@ -8,10 +8,11 @@ import "../less/memo-filter.less";
const MemoFilter = () => {
const { t } = useTranslation();
useAppSelector((state) => state.shortcut.shortcuts);
const query = useAppSelector((state) => state.location.query);
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query;
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query;
const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null;
const showFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut);
const showFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut || visibility);
return (
<div className={`filter-query-container ${showFilter ? "" : "!hidden"}`}>
@@ -40,6 +41,14 @@ const MemoFilter = () => {
>
<Icon.Box className="icon-text" /> {t(getTextWithMemoType(memoType as MemoSpecType))}
</div>
<div
className={"filter-item-container " + (visibility ? "" : "!hidden")}
onClick={() => {
locationService.setMemoVisibilityQuery(undefined);
}}
>
<Icon.Eye className="icon-text" /> {visibility}
</div>
{duration && duration.from < duration.to ? (
<div
className="filter-item-container"
@@ -53,7 +62,7 @@ const MemoFilter = () => {
<div
className={"filter-item-container " + (textQuery ? "" : "!hidden")}
onClick={() => {
locationService.setTextQuery("");
locationService.setTextQuery(undefined);
}}
>
<Icon.Search className="icon-text" /> {textQuery}
+8 -3
View File
@@ -16,10 +16,11 @@ const MemoList = () => {
const memoDisplayTsOption = useAppSelector((state) => state.user.user?.setting.memoDisplayTsOption);
const { memos, isFetching } = useAppSelector((state) => state.memo);
const [isComplete, setIsComplete] = useState<boolean>(false);
const [highlightWord, setHighlightWord] = useState<string | undefined>("");
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query ?? {};
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query ?? {};
const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null;
const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut);
const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut || visibility);
const shownMemos =
showMemoFilter || shortcut
@@ -65,6 +66,9 @@ const MemoList = () => {
if (textQuery && !memo.content.toLowerCase().includes(textQuery.toLowerCase())) {
shouldShow = false;
}
if (visibility) {
shouldShow = memo.visibility === visibility;
}
return shouldShow;
})
@@ -100,6 +104,7 @@ const MemoList = () => {
if (pageWrapper) {
pageWrapper.scrollTo(0, 0);
}
setHighlightWord(query?.text);
}, [query]);
useEffect(() => {
@@ -128,7 +133,7 @@ const MemoList = () => {
return (
<div className="memo-list-container">
{sortedMemos.map((memo) => (
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} />
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} highlightWord={highlightWord} />
))}
{isFetching ? (
<div className="status-text-container fetching-tip">
+38 -15
View File
@@ -3,37 +3,60 @@ import Icon from "./Icon";
import "../less/memo-resources.less";
interface Props {
className?: string;
resourceList: Resource[];
className?: string;
style?: "row" | "col";
}
const MemoResources: React.FC<Props> = (props: Props) => {
const { className, resourceList } = props;
const imageList = resourceList.filter((resource) => resource.type.includes("image"));
const otherResourceList = resourceList.filter((resource) => !resource.type.includes("image"));
const getDefaultProps = (): Props => {
return {
className: "",
style: "row",
resourceList: [],
};
};
const handlPreviewBtnClick = (resource: Resource) => {
const MemoResources: React.FC<Props> = (props: Props) => {
const { className, style, resourceList } = {
...getDefaultProps(),
...props,
};
const availableResourceList = resourceList.filter((resource) => resource.type.startsWith("image") || resource.type.startsWith("video"));
const otherResourceList = resourceList.filter((resource) => !availableResourceList.includes(resource));
const handlePreviewBtnClick = (resource: Resource) => {
const resourceUrl = `${window.location.origin}/o/r/${resource.id}/${resource.filename}`;
window.open(resourceUrl);
};
const imgUrls = imageList.map((resource) => {
return `/o/r/${resource.id}/${resource.filename}`;
});
const imgUrls = availableResourceList
.filter((resource) => resource.type.startsWith("image"))
.map((resource) => {
return `/o/r/${resource.id}/${resource.filename}`;
});
return (
<div className={`resource-wrapper ${className || ""}`}>
{imageList.length > 0 && (
<div className="images-wrapper">
{imageList.map((resource, index) => (
<Image className="memo-img" key={resource.id} imgUrls={imgUrls} index={index} />
))}
{availableResourceList.length > 0 && (
<div className={`images-wrapper ${style}`}>
{availableResourceList.map((resource) => {
const url = `/o/r/${resource.id}/${resource.filename}`;
if (resource.type.startsWith("image")) {
return (
<Image className="memo-resource" key={resource.id} imgUrls={imgUrls} index={imgUrls.findIndex((item) => item === url)} />
);
} else if (resource.type.startsWith("video")) {
return <video className="memo-resource" controls key={resource.id} src={url} />;
} else {
return null;
}
})}
</div>
)}
<div className="other-resource-wrapper">
{otherResourceList.map((resource) => {
return (
<div className="other-resource-container" key={resource.id} onClick={() => handlPreviewBtnClick(resource)}>
<div className="other-resource-container" key={resource.id} onClick={() => handlePreviewBtnClick(resource)}>
<Icon.FileText className="icon-img" />
<span className="name-text">{resource.filename}</span>
</div>
+2 -2
View File
@@ -3,7 +3,7 @@ import { memoService, shortcutService } from "../services";
import { useAppSelector } from "../store";
import Icon from "./Icon";
import SearchBar from "./SearchBar";
import { toggleSiderbar } from "./Sidebar";
import { toggleSidebar } from "./Sidebar";
import "../less/memos-header.less";
let prevRequestTimestamp = Date.now();
@@ -38,7 +38,7 @@ const MemosHeader = () => {
return (
<div className="section-header-container memos-header-container">
<div className="title-container">
<div className="action-btn" onClick={() => toggleSiderbar(true)}>
<div className="action-btn" onClick={() => toggleSidebar(true)}>
<Icon.Menu className="icon-img" />
</div>
<span className="title-text" onClick={handleTitleTextClick}>
+62 -2
View File
@@ -1,16 +1,33 @@
import { useState } from "react";
import React, { useState } from "react";
import * as utils from "../helpers/utils";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import "../less/preview-image-dialog.less";
const MIN_SCALE = 0.5;
const MAX_SCALE = 5;
const SCALE_UNIT = 0.25;
interface Props extends DialogProps {
imgUrls: string[];
initialIndex: number;
}
interface State {
angle: number;
scale: number;
originX: number;
originY: number;
}
const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrls, initialIndex }: Props) => {
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const [state, setState] = useState<State>({
angle: 0,
scale: 1,
originX: -1,
originY: -1,
});
const handleCloseBtnClick = () => {
destroy();
@@ -39,6 +56,38 @@ const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrls, initialIndex }:
}
};
const handleImgRotate = (event: React.MouseEvent, angle: number) => {
const curImgAngle = (state.angle + angle + 360) % 360;
setState({
...state,
originX: -1,
originY: -1,
angle: curImgAngle,
});
};
const handleImgContainerScroll = (event: React.WheelEvent) => {
const offsetX = event.nativeEvent.offsetX;
const offsetY = event.nativeEvent.offsetY;
const sign = event.deltaY < 0 ? 1 : -1;
const curAngle = Math.max(MIN_SCALE, Math.min(MAX_SCALE, state.scale + sign * SCALE_UNIT));
setState({
...state,
originX: offsetX,
originY: offsetY,
scale: curAngle,
});
};
const getImageComputedStyle = () => {
return {
transform: `scale(${state.scale}) rotate(${state.angle}deg)`,
transformOrigin: `${state.originX === -1 ? "center" : `${state.originX}px`} ${
state.originY === -1 ? "center" : `${state.originY}px`
}`,
};
};
return (
<>
<div className="btns-container">
@@ -48,9 +97,20 @@ const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrls, initialIndex }:
<button className="btn" onClick={handleDownloadBtnClick}>
<Icon.Download className="icon-img" />
</button>
<button className="btn" onClick={(e) => handleImgRotate(e, -90)}>
<Icon.RotateCcw className="icon-img" />
</button>
<button className="btn" onClick={(e) => handleImgRotate(e, 90)}>
<Icon.RotateCw className="icon-img" />
</button>
</div>
<div className="img-container" onClick={handleImgContainerClick}>
<img onClick={(e) => e.stopPropagation()} src={imgUrls[currentIndex]} />
<img
onClick={(e) => e.stopPropagation()}
src={imgUrls[currentIndex]}
onWheel={handleImgContainerScroll}
style={getImageComputedStyle()}
/>
</div>
</>
);
+19
View File
@@ -0,0 +1,19 @@
import Icon from "./Icon";
interface Props {
className: string;
resourceType: string;
}
const ResourceIcon = (props: Props) => {
const { className, resourceType } = props;
let ResourceIcon = Icon.FileText;
if (resourceType.includes("image")) {
ResourceIcon = Icon.Image;
}
return <ResourceIcon className={className} />;
};
export default ResourceIcon;
+11 -30
View File
@@ -1,7 +1,7 @@
import { Tooltip } from "@mui/joy";
import copy from "copy-to-clipboard";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import * as utils from "../helpers/utils";
import useLoading from "../hooks/useLoading";
import { resourceService } from "../services";
import { useAppSelector } from "../store";
@@ -83,15 +83,15 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
inputEl.click();
};
const getResouceUrl = useCallback((resource: Resource) => {
const getResourceUrl = useCallback((resource: Resource) => {
return `${window.location.origin}/o/r/${resource.id}/${resource.filename}`;
}, []);
const handlePreviewBtnClick = (resource: Resource) => {
const resourceUrl = getResouceUrl(resource);
const resourceUrl = getResourceUrl(resource);
if (resource.type.startsWith("image")) {
showPreviewImageDialog(
resources.filter((r) => r.type.startsWith("image")).map((r) => getResouceUrl(r)),
resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)),
resources.findIndex((r) => r.id === resource.id)
);
} else {
@@ -149,20 +149,6 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
});
};
const handleResourceNameOrTypeMouseEnter = useCallback((event: React.MouseEvent, nameOrType: string) => {
const tempDiv = document.createElement("div");
tempDiv.className = "usage-detail-container pop-up";
const bounding = utils.getElementBounding(event.target as HTMLElement);
tempDiv.style.left = bounding.left + "px";
tempDiv.style.top = bounding.top - 2 + "px";
tempDiv.innerHTML = `<span>${nameOrType}</span>`;
document.body.appendChild(tempDiv);
}, []);
const handleResourceNameOrTypeMouseLeave = useCallback(() => {
document.body.querySelectorAll("div.usage-detail-container.pop-up").forEach((node) => node.remove());
}, []);
return (
<>
<div className="dialog-header-container">
@@ -206,39 +192,34 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
resources.map((resource) => (
<div key={resource.id} className="resource-container">
<span className="field-text id-text">{resource.id}</span>
<span className="field-text name-text">
<span
onMouseEnter={(e) => handleResourceNameOrTypeMouseEnter(e, resource.filename)}
onMouseLeave={handleResourceNameOrTypeMouseLeave}
>
{resource.filename}
</span>
</span>
<Tooltip title={resource.filename}>
<span className="field-text name-text">{resource.filename}</span>
</Tooltip>
<div className="buttons-container">
<Dropdown
actionsClassName="!w-28"
actions={
<>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100"
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handlePreviewBtnClick(resource)}
>
{t("resources.preview")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100"
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleRenameBtnClick(resource)}
>
{t("resources.rename")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100"
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleCopyResourceLinkBtnClick(resource)}
>
{t("resources.copy-link")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100"
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleDeleteResourceBtnClick(resource)}
>
{t("common.delete")}
@@ -0,0 +1,155 @@
import { Checkbox, Tooltip } from "@mui/joy";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import useLoading from "../hooks/useLoading";
import { editorStateService, resourceService } from "../services";
import { useAppSelector } from "../store";
import Icon from "./Icon";
import toastHelper from "./Toast";
import { generateDialog } from "./Dialog";
import showPreviewImageDialog from "./PreviewImageDialog";
import "../less/resources-selector-dialog.less";
type Props = DialogProps;
interface State {
checkedArray: boolean[];
}
const ResourcesSelectorDialog: React.FC<Props> = (props: Props) => {
const { destroy } = props;
const { t } = useTranslation();
const loadingState = useLoading();
const { resources } = useAppSelector((state) => state.resource);
const editorState = useAppSelector((state) => state.editor);
const [state, setState] = useState<State>({
checkedArray: [],
});
useEffect(() => {
resourceService
.fetchResourceList()
.catch((error) => {
console.error(error);
toastHelper.error(error.response.data.message);
})
.finally(() => {
loadingState.setFinish();
});
}, []);
useEffect(() => {
const checkedResourceIdArray = editorState.resourceList.map((resource) => resource.id);
setState({
checkedArray: resources.map((resource) => {
return checkedResourceIdArray.includes(resource.id);
}),
});
}, [resources]);
const getResourceUrl = useCallback((resource: Resource) => {
return `${window.location.origin}/o/r/${resource.id}/${resource.filename}`;
}, []);
const handlePreviewBtnClick = (resource: Resource) => {
const resourceUrl = getResourceUrl(resource);
if (resource.type.startsWith("image")) {
showPreviewImageDialog(
resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)),
resources.findIndex((r) => r.id === resource.id)
);
} else {
window.open(resourceUrl);
}
};
const handleCheckboxChange = (index: number) => {
const newCheckedArr = state.checkedArray;
newCheckedArr[index] = !newCheckedArr[index];
setState({
checkedArray: newCheckedArr,
});
};
const handleConfirmBtnClick = () => {
const resourceList = resources.filter((_, index) => {
return state.checkedArray[index];
});
editorStateService.setResourceList(resourceList);
destroy();
};
return (
<>
<div className="dialog-header-container">
<p className="title-text">
<span className="icon-text">🌄</span>
{t("sidebar.resources")}
</p>
<button className="btn close-btn" onClick={destroy}>
<Icon.X className="icon-img" />
</button>
</div>
<div className="dialog-content-container">
{loadingState.isLoading ? (
<div className="loading-text-container">
<p className="tip-text">{t("resources.fetching-data")}</p>
</div>
) : (
<div className="resource-table-container">
<div className="fields-container">
<span className="field-text id-text">ID</span>
<span className="field-text name-text">NAME</span>
<span></span>
</div>
{resources.length === 0 ? (
<p className="tip-text">{t("resources.no-resources")}</p>
) : (
resources.map((resource, index) => (
<div key={resource.id} className="resource-container">
<span className="field-text id-text">{resource.id}</span>
<Tooltip placement="top-start" title={resource.filename}>
<span className="field-text name-text">{resource.filename}</span>
</Tooltip>
<div className="flex justify-end">
<Icon.Eye
className=" text-left text-sm leading-6 px-1 mr-2 cursor-pointer hover:opacity-80"
onClick={() => handlePreviewBtnClick(resource)}
>
{t("resources.preview")}
</Icon.Eye>
<Checkbox checked={state.checkedArray[index]} onChange={() => handleCheckboxChange(index)} />
</div>
</div>
))
)}
</div>
)}
<div className="flex justify-between w-full mt-2 px-2">
<span className="text-sm font-mono text-gray-400 leading-8">
{t("message.count-selected-resources")}: {state.checkedArray.filter((checked) => checked).length}
</span>
<div className="flex flex-row justify-start items-center">
<div
className="text-sm cursor-pointer px-3 py-1 rounded flex flex-row justify-center items-center border border-blue-600 text-blue-600 bg-blue-50 hover:opacity-80"
onClick={handleConfirmBtnClick}
>
<Icon.PlusSquare className=" w-4 h-auto mr-1" />
<span>{t("common.confirm")}</span>
</div>
</div>
</div>
</div>
</>
);
};
export default function showResourcesSelectorDialog() {
generateDialog(
{
className: "resources-selector-dialog",
},
ResourcesSelectorDialog,
{}
);
}
+17 -2
View File
@@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { locationService } from "../services";
import { useAppSelector } from "../store";
@@ -8,6 +9,12 @@ import "../less/search-bar.less";
const SearchBar = () => {
const { t } = useTranslation();
const memoType = useAppSelector((state) => state.location.query?.type);
const [queryText, setQueryText] = useState("");
useEffect(() => {
const text = locationService.getState().query.text;
setQueryText(text === undefined ? "" : text);
}, [locationService.getState().query.text]);
const handleMemoTypeItemClick = (type: MemoSpecType | undefined) => {
const { type: prevType } = locationService.getState().query ?? {};
@@ -19,14 +26,22 @@ const SearchBar = () => {
const handleTextQueryInput = (event: React.FormEvent<HTMLInputElement>) => {
const text = event.currentTarget.value;
locationService.setTextQuery(text);
setQueryText(text);
locationService.setTextQuery(text.length === 0 ? undefined : text);
};
return (
<div className="search-bar-container">
<div className="search-bar-inputer">
<Icon.Search className="icon-img" />
<input className="text-input" type="text" placeholder="" onChange={handleTextQueryInput} />
<input
className="text-input"
autoComplete="new-password"
type="text"
placeholder=""
value={queryText}
onChange={handleTextQueryInput}
/>
</div>
<div className="quickly-action-wrapper">
<div className="quickly-action-container">
+1 -1
View File
@@ -66,7 +66,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
onClick={() => handleSectionSelectorItemClick("system")}
className={`section-item ${state.selectedSection === "system" ? "selected" : ""}`}
>
<span className="icon-text">🧑🔧</span> System
<span className="icon-text">🧑🔧</span> {t("setting.system")}
</span>
</div>
</>
+46 -22
View File
@@ -6,10 +6,11 @@ import * as api from "../../helpers/api";
import toastHelper from "../Toast";
import Dropdown from "../common/Dropdown";
import { showCommonDialog } from "../Dialog/CommonDialog";
import showChangeMemberPasswordDialog from "../ChangeMemberPasswordDialog";
import "../../less/settings/member-section.less";
interface State {
createUserEmail: string;
createUserUsername: string;
createUserPassword: string;
}
@@ -17,7 +18,7 @@ const PreferencesSection = () => {
const { t } = useTranslation();
const currentUser = useAppSelector((state) => state.user.user);
const [state, setState] = useState<State>({
createUserEmail: "",
createUserUsername: "",
createUserPassword: "",
});
const [userList, setUserList] = useState<User[]>([]);
@@ -31,10 +32,10 @@ const PreferencesSection = () => {
setUserList(data);
};
const handleEmailInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleUsernameInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setState({
...state,
createUserEmail: event.target.value,
createUserUsername: event.target.value,
});
};
@@ -46,35 +47,37 @@ const PreferencesSection = () => {
};
const handleCreateUserBtnClick = async () => {
if (state.createUserEmail === "" || state.createUserPassword === "") {
if (state.createUserUsername === "" || state.createUserPassword === "") {
toastHelper.error(t("message.fill-form"));
return;
}
const userCreate: UserCreate = {
email: state.createUserEmail,
username: state.createUserUsername,
password: state.createUserPassword,
role: "USER",
name: state.createUserEmail,
};
try {
await api.createUser(userCreate);
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.message);
}
await fetchUserList();
setState({
createUserEmail: "",
createUserUsername: "",
createUserPassword: "",
});
};
const handleChangePasswordClick = (user: User) => {
showChangeMemberPasswordDialog(user);
};
const handleArchiveUserClick = (user: User) => {
showCommonDialog({
title: `Archive Member`,
content: `❗️Are you sure to archive ${user.name}?`,
content: `❗️Are you sure to archive ${user.username}?`,
style: "warning",
onConfirm: async () => {
await userService.patchUser({
@@ -97,7 +100,7 @@ const PreferencesSection = () => {
const handleDeleteUserClick = (user: User) => {
showCommonDialog({
title: `Delete Member`,
content: `Are you sure to delete ${user.name}? THIS ACTION IS IRREVERSIABLE.❗️`,
content: `Are you sure to delete ${user.username}? THIS ACTION IS IRREVERSIABLE.❗️`,
style: "warning",
onConfirm: async () => {
await userService.deleteUser({
@@ -113,38 +116,59 @@ const PreferencesSection = () => {
<p className="title-text">{t("setting.member-section.create-a-member")}</p>
<div className="create-member-container">
<div className="input-form-container">
<span className="field-text">{t("common.email")}</span>
<input type="email" placeholder={t("common.email")} value={state.createUserEmail} onChange={handleEmailInputChange} />
<span className="field-text">{t("common.username")}</span>
<input
type="text"
autoComplete="new-password"
placeholder={t("common.username")}
value={state.createUserUsername}
onChange={handleUsernameInputChange}
/>
</div>
<div className="input-form-container">
<span className="field-text">{t("common.password")}</span>
<input type="text" placeholder={t("common.password")} value={state.createUserPassword} onChange={handlePasswordInputChange} />
<input
type="password"
autoComplete="new-password"
placeholder={t("common.password")}
value={state.createUserPassword}
onChange={handlePasswordInputChange}
/>
</div>
<div className="btns-container">
<button onClick={handleCreateUserBtnClick}>{t("common.create")}</button>
<button className="btn-normal" onClick={handleCreateUserBtnClick}>
{t("common.create")}
</button>
</div>
</div>
<p className="title-text">{t("setting.member-list")}</p>
<div className="w-full flex flex-row justify-between items-center">
<div className="title-text">{t("setting.member-list")}</div>
</div>
<div className="member-container field-container">
<span className="field-text">ID</span>
<span className="field-text">{t("common.email")}</span>
<span className="field-text username-field">{t("common.username")}</span>
<span></span>
</div>
{userList.map((user) => (
<div key={user.id} className={`member-container ${user.rowStatus === "ARCHIVED" ? "archived" : ""}`}>
<span className="field-text id-text">{user.id}</span>
<span className="field-text email-text">{user.email}</span>
<span className="field-text username-text">{user.username}</span>
<div className="buttons-container">
{currentUser?.id === user.id ? (
<span className="tip-text">{t("common.yourself")}</span>
) : (
<Dropdown
actionsClassName="!w-24"
actions={
<>
<button
className="w-full text-left text-sm whitespace-nowrap leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleChangePasswordClick(user)}
>
{t("setting.account-section.change-password")}
</button>
{user.rowStatus === "NORMAL" ? (
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100"
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleArchiveUserClick(user)}
>
{t("common.archive")}
@@ -152,13 +176,13 @@ const PreferencesSection = () => {
) : (
<>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100"
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleRestoreUserClick(user)}
>
{t("common.restore")}
</button>
<button
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100"
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
onClick={() => handleDeleteUserClick(user)}
>
{t("common.delete")}
@@ -1,58 +1,16 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppSelector } from "../../store";
import { userService } from "../../services";
import { validate, ValidatorConfig } from "../../helpers/validator";
import toastHelper from "../Toast";
import { showCommonDialog } from "../Dialog/CommonDialog";
import showChangePasswordDialog from "../ChangePasswordDialog";
import showUpdateAccountDialog from "../UpdateAccountDialog";
import "../../less/settings/my-account-section.less";
const validateConfig: ValidatorConfig = {
minLength: 1,
maxLength: 24,
noSpace: true,
noChinese: false,
};
const MyAccountSection = () => {
const { t, i18n } = useTranslation();
const { t } = useTranslation();
const user = useAppSelector((state) => state.user.user as User);
const [username, setUsername] = useState<string>(user.name);
const openAPIRoute = `${window.location.origin}/api/memo?openId=${user.openId}`;
const handleUsernameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const nextUsername = e.target.value as string;
setUsername(nextUsername);
};
const handleConfirmEditUsernameBtnClick = async () => {
if (username === user.name) {
return;
}
const usernameValidResult = validate(username, validateConfig);
if (!usernameValidResult.result) {
toastHelper.error(t("common.username") + i18n.language === "zh" ? "" : " " + usernameValidResult.reason);
return;
}
try {
await userService.patchUser({
id: user.id,
name: username,
});
toastHelper.info(t("common.username") + i18n.language === "zh" ? "" : " " + t("common.changed"));
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.message);
}
};
const handleChangePasswordBtnClick = () => {
showChangePasswordDialog();
};
const handleResetOpenIdBtnClick = async () => {
showCommonDialog({
title: "Reset Open API",
@@ -67,47 +25,28 @@ const MyAccountSection = () => {
});
};
const handlePreventDefault = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
};
return (
<>
<div className="section-container account-section-container">
<p className="title-text">{t("setting.account-section.title")}</p>
<label className="form-label">
<span className="normal-text">{t("common.email")}:</span>
<span className="normal-text">{user.email}</span>
</label>
<label className="form-label input-form-label username-label">
<span className="normal-text">{t("common.username")}:</span>
<input type="text" value={username} onChange={handleUsernameChanged} />
<div className={`btns-container ${username === user.name ? "!hidden" : ""}`} onClick={handlePreventDefault}>
<span className="btn confirm-btn" onClick={handleConfirmEditUsernameBtnClick}>
{t("common.save")}
</span>
<span
className="btn cancel-btn"
onClick={() => {
setUsername(user.name);
}}
>
{t("common.cancel")}
</span>
</div>
</label>
<label className="form-label password-label">
<span className="normal-text">{t("common.password")}:</span>
<span className="btn" onClick={handleChangePasswordBtnClick}>
{t("common.change")}
</span>
</label>
<div className="flex flex-row justify-start items-end">
<span className="text-2xl leading-10 font-medium">{user.nickname}</span>
<span className="text-base ml-1 text-gray-500 leading-8">({user.username})</span>
</div>
<div className="flex flex-row justify-start items-center text-base text-gray-600">{user.email}</div>
<div className="w-full flex flex-row justify-start items-center mt-2 space-x-2">
<button className="btn-normal" onClick={showUpdateAccountDialog}>
{t("setting.account-section.update-information")}
</button>
<button className="btn-normal" onClick={showChangePasswordDialog}>
{t("setting.account-section.change-password")}
</button>
</div>
</div>
<div className="section-container openapi-section-container">
<p className="title-text">Open API</p>
<p className="value-text">{openAPIRoute}</p>
<span className="reset-btn" onClick={handleResetOpenIdBtnClick}>
<span className="btn-danger mt-2" onClick={handleResetOpenIdBtnClick}>
{t("common.reset")} API
</span>
<div className="usage-guide-container">
@@ -1,8 +1,10 @@
import { Select, Switch, Option } from "@mui/joy";
import { useTranslation } from "react-i18next";
import { globalService, userService } from "../../services";
import { useAppSelector } from "../../store";
import { VISIBILITY_SELECTOR_ITEMS, MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS } from "../../helpers/consts";
import Selector from "../common/Selector";
import Icon from "../Icon";
import AppearanceSelect from "../AppearanceSelect";
import "../../less/settings/preferences-section.less";
const localeSelectorItems = [
@@ -18,22 +20,27 @@ const localeSelectorItems = [
text: "Tiếng Việt",
value: "vi",
},
];
const mobileEditorStyleSelectorItems = [
{
text: "Normal",
value: "normal",
text: "French",
value: "fr",
},
{
text: "Float",
value: "float",
text: "Nederlands",
value: "nl",
},
{
text: "Svenska",
value: "sv",
},
{
text: "German",
value: "de",
},
];
const PreferencesSection = () => {
const { t } = useTranslation();
const { setting } = useAppSelector((state) => state.user.user as User);
const { setting, localSetting } = useAppSelector((state) => state.user.user as User);
const visibilitySelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
return {
value: item.value,
@@ -57,48 +64,80 @@ const PreferencesSection = () => {
await userService.upsertUserSetting("memoVisibility", value);
};
const handleMobileEditorStyleChanged = async (value: string) => {
await userService.upsertUserSetting("mobileEditorStyle", value);
};
const handleMemoDisplayTsOptionChanged = async (value: string) => {
await userService.upsertUserSetting("memoDisplayTsOption", value);
};
const handleIsFoldingEnabledChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
userService.upsertLocalSetting("enableFoldMemo", event.target.checked);
};
return (
<div className="section-container preferences-section-container">
<p className="title-text">{t("common.basic")}</p>
<label className="form-label selector">
<div className="form-label selector">
<span className="normal-text">{t("common.language")}</span>
<Selector className="ml-2 w-32" value={setting.locale} dataSource={localeSelectorItems} handleValueChanged={handleLocaleChanged} />
</label>
<Select
className="!min-w-[10rem] w-auto text-sm"
value={setting.locale}
onChange={(_, locale) => {
if (locale) {
handleLocaleChanged(locale);
}
}}
startDecorator={<Icon.Globe className="w-4 h-auto" />}
>
{localeSelectorItems.map((item) => (
<Option key={item.value} value={item.value} className="whitespace-nowrap">
{item.text}
</Option>
))}
</Select>
</div>
<div className="form-label selector">
<span className="normal-text">Theme</span>
<AppearanceSelect />
</div>
<p className="title-text">{t("setting.preference")}</p>
<label className="form-label selector">
<div className="form-label selector">
<span className="normal-text">{t("setting.preference-section.default-memo-visibility")}</span>
<Selector
className="ml-2 w-32"
<Select
className="!min-w-[10rem] w-auto text-sm"
value={setting.memoVisibility}
dataSource={visibilitySelectorItems}
handleValueChanged={handleDefaultMemoVisibilityChanged}
/>
</label>
<label className="form-label selector">
<span className="normal-text">{t("setting.preference-section.mobile-editor-style")}</span>
<Selector
className="ml-2 w-32"
value={setting.mobileEditorStyle}
dataSource={mobileEditorStyleSelectorItems}
handleValueChanged={handleMobileEditorStyleChanged}
/>
</label>
onChange={(_, visibility) => {
if (visibility) {
handleDefaultMemoVisibilityChanged(visibility);
}
}}
>
{visibilitySelectorItems.map((item) => (
<Option key={item.value} value={item.value} className="whitespace-nowrap">
{item.text}
</Option>
))}
</Select>
</div>
<label className="form-label selector">
<span className="normal-text">{t("setting.preference-section.default-memo-sort-option")}</span>
<Selector
className="ml-2 w-32"
<Select
className="!min-w-[10rem] w-auto text-sm"
value={setting.memoDisplayTsOption}
dataSource={memoDisplayTsOptionSelectorItems}
handleValueChanged={handleMemoDisplayTsOptionChanged}
/>
onChange={(_, value) => {
if (value) {
handleMemoDisplayTsOptionChanged(value);
}
}}
>
{memoDisplayTsOptionSelectorItems.map((item) => (
<Option key={item.value} value={item.value} className="whitespace-nowrap">
{item.text}
</Option>
))}
</Select>
</label>
<label className="form-label selector">
<span className="normal-text">{t("setting.preference-section.enable-folding-memo")}</span>
<Switch className="ml-2" checked={localSetting.enableFoldMemo} onChange={handleIsFoldingEnabledChanged} />
</label>
</div>
);
+98 -16
View File
@@ -2,26 +2,42 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Switch, Textarea } from "@mui/joy";
import * as api from "../../helpers/api";
import "../../less/settings/preferences-section.less";
import toastHelper from "../Toast";
import "../../less/settings/system-section.less";
interface State {
dbSize: number;
allowSignUp: boolean;
additionalStyle: string;
additionalScript: string;
}
const formatBytes = (bytes: number) => {
if (bytes <= 0) return "0 Bytes";
const k = 1024,
dm = 2,
sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i];
};
const SystemSection = () => {
const { t } = useTranslation();
const [state, setState] = useState<State>({
dbSize: 0,
allowSignUp: false,
additionalStyle: "",
additionalScript: "",
});
useEffect(() => {
api.getSystemStatus().then(({ data }) => {
const { data: status } = data;
setState({
dbSize: status.dbSize,
allowSignUp: status.allowSignUp,
additionalStyle: status.additionalStyle,
additionalScript: status.additionalScript,
});
});
}, []);
@@ -44,37 +60,103 @@ const SystemSection = () => {
});
};
const handleVacuumBtnClick = async () => {
try {
await api.vacuumDatabase();
const { data: status } = (await api.getSystemStatus()).data;
setState({
dbSize: status.dbSize,
allowSignUp: status.allowSignUp,
additionalStyle: status.additionalStyle,
additionalScript: status.additionalScript,
});
} catch (error) {
console.error(error);
return;
}
toastHelper.success("Succeed to vacuum database");
};
const handleSaveAdditionalStyle = async () => {
await api.upsertSystemSetting({
name: "additionalStyle",
value: JSON.stringify(state.additionalStyle),
try {
await api.upsertSystemSetting({
name: "additionalStyle",
value: JSON.stringify(state.additionalStyle),
});
} catch (error) {
console.error(error);
return;
}
toastHelper.success("Succeed to update additional style");
};
const handleAdditionalScriptChanged = (value: string) => {
setState({
...state,
additionalScript: value,
});
};
const handleSaveAdditionalScript = async () => {
try {
await api.upsertSystemSetting({
name: "additionalScript",
value: JSON.stringify(state.additionalScript),
});
} catch (error) {
console.error(error);
return;
}
toastHelper.success("Succeed to update additional script");
};
return (
<div className="section-container preferences-section-container">
<div className="section-container system-section-container">
<p className="title-text">{t("common.basic")}</p>
<label className="form-label selector">
<span className="normal-text">Allow user signup</span>
<Switch size="sm" checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
<label className="form-label">
<span className="normal-text">
{t("setting.system-section.database-file-size")}: <span className="font-mono font-medium">{formatBytes(state.dbSize)}</span>
</span>
<Button onClick={handleVacuumBtnClick}>{t("common.vacuum")}</Button>
</label>
<label className="form-label selector">
<span className="normal-text">Additional style</span>
<Button size="sm" onClick={handleSaveAdditionalStyle}>
Save
</Button>
<p className="title-text">{t("sidebar.setting")}</p>
<label className="form-label">
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
</label>
<div className="form-label">
<span className="normal-text">{t("setting.system-section.additional-style")}</span>
<Button onClick={handleSaveAdditionalStyle}>{t("common.save")}</Button>
</div>
<Textarea
className="w-full"
sx={{
fontFamily: "monospace",
fontSize: "14px",
}}
minRows={5}
maxRows={10}
defaultValue={state.additionalStyle}
minRows={4}
maxRows={4}
placeholder={t("setting.system-section.additional-style-placeholder")}
value={state.additionalStyle}
onChange={(event) => handleAdditionalStyleChanged(event.target.value)}
/>
<div className="form-label mt-2">
<span className="normal-text">{t("setting.system-section.additional-script")}</span>
<Button onClick={handleSaveAdditionalScript}>{t("common.save")}</Button>
</div>
<Textarea
className="w-full"
color="neutral"
sx={{
fontFamily: "monospace",
fontSize: "14px",
}}
minRows={4}
maxRows={4}
placeholder={t("setting.system-section.additional-script-placeholder")}
value={state.additionalScript}
onChange={(event) => handleAdditionalScriptChanged(event.target.value)}
/>
</div>
);
};
+195
View File
@@ -0,0 +1,195 @@
import { Select, Option } from "@mui/joy";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import copy from "copy-to-clipboard";
import { toLower } from "lodash";
import toImage from "../labs/html2image";
import { VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
import * as utils from "../helpers/utils";
import { getMemoStats } from "../helpers/api";
import { memoService, userService } from "../services";
import useLoading from "../hooks/useLoading";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources";
import "../less/share-memo-dialog.less";
interface Props extends DialogProps {
memo: Memo;
}
interface State {
memoAmount: number;
memoVisibility: string;
generatedImgUrl: string;
}
const ShareMemoDialog: React.FC<Props> = (props: Props) => {
const { memo: propsMemo, destroy } = props;
const { t } = useTranslation();
const user = userService.getState().user as User;
const [state, setState] = useState<State>({
memoAmount: 0,
memoVisibility: propsMemo.visibility,
generatedImgUrl: "",
});
const loadingState = useLoading();
const memoElRef = useRef<HTMLDivElement>(null);
const memo = {
...propsMemo,
createdAtStr: utils.getDateTimeString(propsMemo.displayTs),
};
const createdDays = Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24);
useEffect(() => {
getMemoStats(user.id)
.then(({ data: { data } }) => {
setState((state) => {
return {
...state,
memoAmount: data.length,
};
});
loadingState.setFinish();
})
.catch((error) => {
console.error(error);
});
}, []);
useEffect(() => {
if (loadingState.isLoading) {
return;
}
if (!memoElRef.current) {
return;
}
toImage(memoElRef.current, {
pixelRatio: window.devicePixelRatio * 2,
})
.then((url) => {
setState((state) => {
return {
...state,
generatedImgUrl: url,
};
});
})
.catch((err) => {
console.error(err);
});
}, [loadingState.isLoading]);
const handleCloseBtnClick = () => {
destroy();
};
const handleDownloadBtnClick = () => {
const a = document.createElement("a");
a.href = state.generatedImgUrl;
a.download = `memos-${utils.getDateTimeString(Date.now())}.png`;
a.click();
};
const handleCopyLinkBtnClick = () => {
copy(`${window.location.origin}/m/${memo.id}`);
toastHelper.success(t("message.succeed-copy-content"));
};
const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
return {
value: item.value,
text: t(`memo.visibility.${toLower(item.value)}`),
};
});
const handleMemoVisibilityOptionChanged = async (value: string) => {
const visibilityValue = value as Visibility;
setState({
...state,
memoVisibility: visibilityValue,
});
await memoService.patchMemo({
id: memo.id,
visibility: visibilityValue,
});
};
return (
<>
<div className="dialog-header-container">
<p className="title-text">
<span className="icon-text">🌄</span>
{t("common.share")} Memo
</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X className="icon-img" />
</button>
</div>
<div className="dialog-content-container">
<div className="memo-container" ref={memoElRef}>
{state.generatedImgUrl !== "" && <img className="memo-shortcut-img" src={state.generatedImgUrl} />}
<span className="time-text">{memo.createdAtStr}</span>
<div className="memo-content-wrapper">
<MemoContent content={memo.content} displayConfig={{ enableExpand: false }} />
<MemoResources style="col" resourceList={memo.resourceList} />
</div>
<div className="watermark-container">
<div className="userinfo-container">
<span className="name-text">{user.nickname || user.username}</span>
<span className="usage-text">
{state.memoAmount} MEMOS / {createdDays} DAYS
</span>
</div>
<img className="logo-img" src="/logo.webp" alt="" />
</div>
</div>
<div className="px-4 py-3 w-full flex flex-row justify-between items-center">
<Select
className="!min-w-[10rem] w-auto text-sm"
value={state.memoVisibility}
onChange={(_, visibility) => {
if (visibility) {
handleMemoVisibilityOptionChanged(visibility);
}
}}
>
{memoVisibilityOptionSelectorItems.map((item) => (
<Option key={item.value} value={item.value} className="whitespace-nowrap">
{item.text}
</Option>
))}
</Select>
<div className="flex flex-row justify-end items-center">
<button disabled={state.generatedImgUrl === ""} className="btn-normal mr-2" onClick={handleDownloadBtnClick}>
{state.generatedImgUrl === "" ? (
<Icon.Loader className="w-4 h-auto mr-1 animate-spin" />
) : (
<Icon.Download className="w-4 h-auto mr-1" />
)}
<span>{t("common.image")}</span>
</button>
<button className="btn-normal" onClick={handleCopyLinkBtnClick}>
<Icon.Link className="w-4 h-auto mr-1" />
<span>{t("common.link")}</span>
</button>
</div>
</div>
</div>
</>
);
};
export default function showShareMemoDialog(memo: Memo): void {
generateDialog(
{
className: "share-memo-dialog",
},
ShareMemoDialog,
{ memo }
);
}
@@ -1,99 +0,0 @@
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { userService } from "../services";
import toImage from "../labs/html2image";
import { ANIMATION_DURATION } from "../helpers/consts";
import * as utils from "../helpers/utils";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import MemoContent from "./MemoContent";
import "../less/share-memo-image-dialog.less";
import MemoResources from "./MemoResources";
interface Props extends DialogProps {
memo: Memo;
}
const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
const { memo: propsMemo, destroy } = props;
const { t } = useTranslation();
const { user: userinfo } = userService.getState();
const [shortcutImgUrl, setShortcutImgUrl] = useState("");
const memo = {
...propsMemo,
createdAtStr: utils.getDateTimeString(propsMemo.displayTs),
};
const memoElRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setTimeout(() => {
if (!memoElRef.current) {
return;
}
toImage(memoElRef.current, {
backgroundColor: "#eaeaea",
pixelRatio: window.devicePixelRatio * 2,
})
.then((url) => {
setShortcutImgUrl(url);
})
.catch((err) => {
console.error(err);
});
}, ANIMATION_DURATION);
}, []);
const handleCloseBtnClick = () => {
destroy();
};
const handleDownloadBtnClick = () => {
const a = document.createElement("a");
a.href = shortcutImgUrl;
a.download = `memos-${utils.getDateTimeString(Date.now())}.png`;
a.click();
};
return (
<>
<div className="dialog-header-container">
<p className="title-text">
<span className="icon-text">🌄</span>
{t("common.share")} Memo
</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X className="icon-img" />
</button>
</div>
<div className="dialog-content-container">
<div className={`tip-words-container ${shortcutImgUrl ? "finish" : "loading"}`}>
<p className="tip-text">{shortcutImgUrl ? "Click to save the image 👇" : "Generating the screenshot..."}</p>
</div>
<div className="memo-container" ref={memoElRef}>
{shortcutImgUrl !== "" && <img className="memo-shortcut-img" onClick={handleDownloadBtnClick} src={shortcutImgUrl} />}
<span className="time-text">{memo.createdAtStr}</span>
<div className="memo-content-wrapper">
<MemoContent content={memo.content} displayConfig={{ enableExpand: false }} />
<MemoResources resourceList={memo.resourceList} />
</div>
<div className="watermark-container">
<span className="normal-text">
<span className="icon-text"></span> by <span className="name-text">{userinfo?.name}</span>
</span>
</div>
</div>
</div>
</>
);
};
export default function showShareMemoImageDialog(memo: Memo): void {
generateDialog(
{
className: "share-memo-image-dialog",
},
ShareMemoImageDialog,
{ memo }
);
}
+4
View File
@@ -76,6 +76,10 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
if (showConfirmDeleteBtn) {
try {
await shortcutService.deleteShortcutById(shortcut.id);
if (locationService.getState().query?.shortcutId === shortcut.id) {
// need clear shortcut filter
locationService.setMemoShortcut(undefined);
}
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.message);
+3 -3
View File
@@ -17,7 +17,7 @@ const Sidebar = () => {
const location = useAppSelector((state) => state.location);
useEffect(() => {
toggleSiderbar(false);
toggleSidebar(false);
}, [location.query]);
const handleSettingBtnClick = () => {
@@ -26,7 +26,7 @@ const Sidebar = () => {
return (
<>
<div className="mask" onClick={() => toggleSiderbar(false)}></div>
<div className="mask" onClick={() => toggleSidebar(false)}></div>
<aside className="sidebar-wrapper">
<UserBanner />
<UsageHeatMap />
@@ -52,7 +52,7 @@ const Sidebar = () => {
);
};
export const toggleSiderbar = (show?: boolean) => {
export const toggleSidebar = (show?: boolean) => {
const sidebarEl = document.body.querySelector(".sidebar-wrapper") as HTMLDivElement;
const maskEl = document.body.querySelector(".mask") as HTMLDivElement;
+7 -7
View File
@@ -14,22 +14,22 @@ type ToastItemProps = {
type: ToastType;
content: string;
duration: number;
destory: FunctionType;
destroy: FunctionType;
};
const Toast: React.FC<ToastItemProps> = (props: ToastItemProps) => {
const { destory, duration } = props;
const { destroy, duration } = props;
useEffect(() => {
if (duration > 0) {
setTimeout(() => {
destory();
destroy();
}, duration);
}
}, []);
return (
<div className="toast-container" onClick={destory}>
<div className="toast-container" onClick={destroy}>
<p className="content-text">{props.content}</p>
</div>
);
@@ -57,8 +57,8 @@ const initialToastHelper = () => {
shownToastContainers.push([toast, tempDiv]);
const cbs = {
destory: () => {
tempDiv.classList.add("destory");
destroy: () => {
tempDiv.classList.add("destroy");
setTimeout(() => {
if (!tempDiv.parentElement) {
@@ -77,7 +77,7 @@ const initialToastHelper = () => {
},
};
toast.render(<Toast {...config} destory={cbs.destory} />);
toast.render(<Toast {...config} destroy={cbs.destroy} />);
setTimeout(() => {
tempDiv.classList.add("showup");
+147
View File
@@ -0,0 +1,147 @@
import { isEqual } from "lodash";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppSelector } from "../store";
import { userService } from "../services";
import Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
import { validate, ValidatorConfig } from "../helpers/validator";
const validateConfig: ValidatorConfig = {
minLength: 4,
maxLength: 320,
noSpace: true,
noChinese: true,
};
type Props = DialogProps;
interface State {
username: string;
nickname: string;
email: string;
}
const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
const { t } = useTranslation();
const user = useAppSelector((state) => state.user.user as User);
const [state, setState] = useState<State>({
username: user.username,
nickname: user.nickname,
email: user.email,
});
useEffect(() => {
// do nth
}, []);
const handleCloseBtnClick = () => {
destroy();
};
const handleNicknameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
setState((state) => {
return {
...state,
nickname: e.target.value as string,
};
});
};
const handleUsernameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
setState((state) => {
return {
...state,
username: e.target.value as string,
};
});
};
const handleEmailChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
setState((state) => {
return {
...state,
email: e.target.value as string,
};
});
};
const handleSaveBtnClick = async () => {
if (state.username === "") {
toastHelper.error(t("message.fill-all"));
return;
}
const usernameValidResult = validate(state.username, validateConfig);
if (!usernameValidResult.result) {
toastHelper.error(t("common.username") + ": " + t(usernameValidResult.reason as string));
return;
}
try {
const user = userService.getState().user as User;
const userPatch: UserPatch = {
id: user.id,
};
if (!isEqual(user.nickname, state.nickname)) {
userPatch.nickname = state.nickname;
}
if (!isEqual(user.username, state.username)) {
userPatch.username = state.username;
}
if (!isEqual(user.email, state.email)) {
userPatch.email = state.email;
}
await userService.patchUser(userPatch);
toastHelper.info("Update succeed");
handleCloseBtnClick();
} catch (error: any) {
console.error(error);
toastHelper.error(error.response.data.error);
}
};
return (
<>
<div className="dialog-header-container !w-64">
<p className="title-text">Update information</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<Icon.X />
</button>
</div>
<div className="dialog-content-container">
<p className="text-sm mb-1">
Nickname<span className="text-sm text-gray-400 ml-1">(Display in the banner)</span>
</p>
<input type="text" className="input-text" value={state.nickname} onChange={handleNicknameChanged} />
<p className="text-sm mb-1 mt-2">
Username
<span className="text-sm text-gray-400 ml-1">(Using to sign in)</span>
</p>
<input type="text" className="input-text" value={state.username} onChange={handleUsernameChanged} />
<p className="text-sm mb-1 mt-2">
Email<span className="text-sm text-gray-400 ml-1">(Optional)</span>
</p>
<input type="text" className="input-text" value={state.email} onChange={handleEmailChanged} />
<div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
<span className="btn-text" onClick={handleCloseBtnClick}>
{t("common.cancel")}
</span>
<span className="btn-primary" onClick={handleSaveBtnClick}>
{t("common.save")}
</span>
</div>
</div>
</>
);
};
function showUpdateAccountDialog() {
generateDialog(
{
className: "update-account-dialog",
},
UpdateAccountDialog
);
}
export default showUpdateAccountDialog;
+11 -22
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { useAppSelector } from "../store";
import * as api from "../helpers/api";
import * as storage from "../helpers/storage";
import Icon from "./Icon";
@@ -10,35 +11,23 @@ interface State {
}
const UpdateVersionBanner: React.FC = () => {
const profile = useAppSelector((state) => state.global.systemStatus.profile);
const [state, setState] = useState<State>({
latestVersion: "",
show: false,
});
useEffect(() => {
Promise.all([api.getRepoLatestTag(), api.getSystemStatus()])
.then(
([
latestTag,
{
data: {
data: { profile },
},
},
]) => {
const { skippedVersion } = storage.get(["skippedVersion"]);
const latestVersion = latestTag.slice(1) || "0.0.0";
const currentVersion = profile.version;
const skipped = skippedVersion ? skippedVersion === latestVersion : false;
setState({
latestVersion,
show: !skipped && currentVersion < latestVersion,
});
}
)
.catch(() => {
// do nth
api.getRepoLatestTag().then((latestTag) => {
const { skippedVersion } = storage.get(["skippedVersion"]);
const latestVersion = latestTag.slice(1) || "0.0.0";
const currentVersion = profile.version;
const skipped = skippedVersion ? skippedVersion === latestVersion : false;
setState({
latestVersion,
show: !skipped && currentVersion < latestVersion,
});
});
}, []);
const onSkip = () => {
+17 -8
View File
@@ -11,11 +11,11 @@ const tableConfig = {
height: 7,
};
const getInitialUsageStat = (usedDaysAmount: number, beginDayTimestemp: number): DailyUsageStat[] => {
const getInitialUsageStat = (usedDaysAmount: number, beginDayTimestamp: number): DailyUsageStat[] => {
const initialUsageStat: DailyUsageStat[] = [];
for (let i = 1; i <= usedDaysAmount; i++) {
initialUsageStat.push({
timestamp: beginDayTimestemp + DAILY_TIMESTAMP * i,
timestamp: beginDayTimestamp + DAILY_TIMESTAMP * i,
count: 0,
});
}
@@ -32,21 +32,25 @@ const UsageHeatMap = () => {
const todayDay = new Date(todayTimeStamp).getDay() + 1;
const nullCell = new Array(7 - todayDay).fill(0);
const usedDaysAmount = (tableConfig.width - 1) * tableConfig.height + todayDay;
const beginDayTimestemp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP;
const beginDayTimestamp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP;
const { memos } = useAppSelector((state) => state.memo);
const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestemp));
const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestamp));
const [currentStat, setCurrentStat] = useState<DailyUsageStat | null>(null);
const containerElRef = useRef<HTMLDivElement>(null);
useEffect(() => {
getMemoStats(userService.getCurrentUserId())
.then(({ data: { data } }) => {
const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestemp);
const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestamp);
for (const record of data) {
const index = (utils.getDateStampByDate(record * 1000) - beginDayTimestemp) / (1000 * 3600 * 24) - 1;
const index = (utils.getDateStampByDate(record * 1000) - beginDayTimestamp) / (1000 * 3600 * 24) - 1;
if (index >= 0) {
newStat[index].count += 1;
// because of dailight savings, some days may be 23 hours long instead of 24 hours long
// this causes the calculations to yield weird indices such as 40.93333333333
// rounding them may not give you the exact day on the heat map, but it's not too bad
const exactIndex = +index.toFixed(0);
newStat[exactIndex].count += 1;
}
}
setAllStat([...newStat]);
@@ -64,6 +68,11 @@ const UsageHeatMap = () => {
tempDiv.style.top = bounding.top - 2 + "px";
tempDiv.innerHTML = `${item.count} memos on <span className="date-text">${new Date(item.timestamp as number).toDateString()}</span>`;
document.body.appendChild(tempDiv);
if (tempDiv.offsetLeft - tempDiv.clientWidth / 2 < 0) {
tempDiv.style.left = bounding.left + tempDiv.clientWidth * 0.4 + "px";
tempDiv.className += " offset-left";
}
}, []);
const handleUsageStatItemMouseLeave = useCallback(() => {
@@ -123,7 +132,7 @@ const UsageHeatMap = () => {
})}
{nullCell.map((_, i) => (
<div className="stat-wrapper" key={i}>
<span className="stat-container null"></span>
<span className="null"></span>
</div>
))}
</div>
+8 -9
View File
@@ -28,10 +28,10 @@ const UserBanner = () => {
if (!owner) {
return;
}
setUsername(owner.name);
setUsername(owner.nickname || owner.username);
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(owner.createdTs)) / 1000 / 3600 / 24));
} else if (user) {
setUsername(user.name);
setUsername(user.nickname || user.username);
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24));
}
}, [isVisitorMode, user, owner]);
@@ -63,7 +63,6 @@ const UserBanner = () => {
};
const handleSignOutBtnClick = async () => {
userService.doSignOut().catch();
navigate("/auth");
};
@@ -75,20 +74,20 @@ const UserBanner = () => {
{!isVisitorMode && user?.role === "HOST" ? <span className="tag">MOD</span> : null}
</div>
<Dropdown
trigger={<Icon.MoreHorizontal className="ml-2 w-5 h-auto cursor-pointer" />}
actionsClassName="!w-36"
trigger={<Icon.MoreHorizontal className="ml-2 w-5 h-auto cursor-pointer dark:text-gray-200" />}
actionsClassName="min-w-36"
actions={
<>
{!userService.isVisitorMode() && (
<>
<button
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
onClick={handleResourcesBtnClick}
>
<span className="mr-1">🌄</span> {t("sidebar.resources")}
</button>
<button
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
onClick={handleArchivedBtnClick}
>
<span className="mr-1">🗂</span> {t("sidebar.archived")}
@@ -96,14 +95,14 @@ const UserBanner = () => {
</>
)}
<button
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
onClick={handleAboutBtnClick}
>
<span className="mr-1">🤠</span> {t("common.about")}
</button>
{!userService.isVisitorMode() && (
<button
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
onClick={handleSignOutBtnClick}
>
<span className="mr-1">👋</span> {t("common.sign-out")}
+1 -1
View File
@@ -6,7 +6,7 @@ import "../../less/common/date-picker.less";
interface DatePickerProps {
className?: string;
datestamp: DateStamp;
handleDateStampChange: (datastamp: DateStamp) => void;
handleDateStampChange: (datestamp: DateStamp) => void;
}
const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
+5 -4
View File
@@ -7,10 +7,11 @@ interface Props {
actions?: ReactNode;
className?: string;
actionsClassName?: string;
positionClassName?: string;
}
const Dropdown: React.FC<Props> = (props: Props) => {
const { trigger, actions, className, actionsClassName } = props;
const { trigger, actions, className, actionsClassName, positionClassName } = props;
const [dropdownStatus, toggleDropdownStatus] = useToggle(false);
const dropdownWrapperRef = useRef<HTMLDivElement>(null);
@@ -37,14 +38,14 @@ const Dropdown: React.FC<Props> = (props: Props) => {
{trigger ? (
trigger
) : (
<button className="flex flex-row justify-center items-center border p-1 rounded shadow text-gray-600 cursor-pointer hover:opacity-80">
<button className="flex flex-row justify-center items-center border dark:border-zinc-700 p-1 rounded shadow text-gray-600 dark:text-gray-200 cursor-pointer hover:opacity-80">
<Icon.MoreHorizontal className="w-4 h-auto" />
</button>
)}
<div
className={`w-auto mt-1 absolute top-full right-0 flex flex-col justify-start items-start bg-white z-1 border p-1 rounded-md shadow ${
className={`w-auto absolute flex flex-col justify-start items-start bg-white dark:bg-zinc-700 z-10 p-1 rounded-md shadow ${
actionsClassName ?? ""
} ${dropdownStatus ? "" : "!hidden"}`}
} ${dropdownStatus ? "" : "!hidden"} ${positionClassName ?? "top-full right-0 mt-1"}`}
>
{actions}
</div>
+3 -3
View File
@@ -26,7 +26,7 @@ const Selector: React.FC<Props> = (props: Props) => {
const { t } = useTranslation();
const [showSelector, toggleSelectorStatus] = useToggle(false);
const seletorElRef = useRef<HTMLDivElement>(null);
const selectorElRef = useRef<HTMLDivElement>(null);
let currentItem = nullItem;
for (const d of dataSource) {
@@ -39,7 +39,7 @@ const Selector: React.FC<Props> = (props: Props) => {
useEffect(() => {
if (showSelector) {
const handleClickOutside = (event: MouseEvent) => {
if (!seletorElRef.current?.contains(event.target as Node)) {
if (!selectorElRef.current?.contains(event.target as Node)) {
toggleSelectorStatus(false);
}
};
@@ -63,7 +63,7 @@ const Selector: React.FC<Props> = (props: Props) => {
};
return (
<div className={`selector-wrapper ${className ?? ""}`} ref={seletorElRef}>
<div className={`selector-wrapper ${className ?? ""}`} ref={selectorElRef}>
<div className={`current-value-container ${showSelector ? "active" : ""}`} onClick={handleCurrentValueClick}>
<span className="value-text">{currentItem.text}</span>
<span className="arrow-text">

Some files were not shown because too many files have changed in this diff Show More