diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 0936f6c5e..6c9ebdba2 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -113,6 +113,9 @@ {:name "0032-del-unused-tables" :fn (mg/resource "app/migrations/sql/0032-del-unused-tables.sql")} + + {:name "0033-mod-comment-thread-table" + :fn (mg/resource "app/migrations/sql/0033-mod-comment-thread-table.sql")} ]}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/migrations/sql/0033-mod-comment-thread-table.sql b/backend/src/app/migrations/sql/0033-mod-comment-thread-table.sql new file mode 100644 index 000000000..0e98ef6d1 --- /dev/null +++ b/backend/src/app/migrations/sql/0033-mod-comment-thread-table.sql @@ -0,0 +1,2 @@ +ALTER TABLE comment_thread + ADD COLUMN page_name text NULL; diff --git a/backend/src/app/services/mutations/comments.clj b/backend/src/app/services/mutations/comments.clj index af798bb07..8fd4ce4f7 100644 --- a/backend/src/app/services/mutations/comments.clj +++ b/backend/src/app/services/mutations/comments.clj @@ -29,12 +29,13 @@ (declare upsert-comment-thread-status!) (declare create-comment-thread) +(declare retrieve-page-name) +(s/def ::page-id ::us/uuid) (s/def ::file-id ::us/uuid) (s/def ::profile-id ::us/uuid) (s/def ::position ::us/point) (s/def ::content ::us/string) -(s/def ::page-id ::us/uuid) (s/def ::create-comment-thread (s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id])) @@ -53,13 +54,14 @@ (defn- create-comment-thread* [conn {:keys [profile-id file-id page-id position content] :as params}] - (let [seqn (retrieve-next-seqn conn file-id) - now (dt/now) - + (let [seqn (retrieve-next-seqn conn file-id) + now (dt/now) + pname (retrieve-page-name conn params) thread (db/insert! conn :comment-thread {:file-id file-id :owner-id profile-id :participants (db/tjson #{profile-id}) + :page-name pname :page-id page-id :created-at now :modified-at now @@ -81,10 +83,7 @@ {:comment-thread-seqn seqn} {:id file-id}) - (-> (assoc thread - :content content - :comment comment) - (comments/decode-row)))) + (select-keys thread [:id :file-id :page-id]))) (defn- create-comment-thread [conn params] @@ -104,6 +103,12 @@ :else res)))) +(defn- retrieve-page-name + [conn {:keys [file-id page-id]}] + (let [{:keys [data]} (db/get-by-id conn :file file-id) + data (blob/decode data)] + (get-in data [:pages-index page-id :name]))) + ;; --- Mutation: Update Comment Thread Status @@ -164,14 +169,21 @@ [{:keys [profile-id thread-id content] :as params}] (db/with-atomic [conn db/pool] (let [thread (-> (db/get-by-id conn :comment-thread thread-id {:for-update true}) - (comments/decode-row))] + (comments/decode-row)) + pname (retrieve-page-name conn thread)] ;; Standard Checks - (when-not thread - (ex/raise :type :not-found)) + (when-not thread (ex/raise :type :not-found)) + ;; Permission Checks (files/check-read-permissions! conn profile-id (:file-id thread)) + ;; Update the page-name cachedattribute on comment thread table. + (when (not= pname (:page-name thread)) + (db/update! conn :comment-thread + {:page-name pname} + {:id thread-id})) + ;; NOTE: is important that all timestamptz related fields are ;; created or updated on the database level for avoid clock ;; inconsistencies (some user sees something read that is not @@ -216,15 +228,19 @@ (let [comment (db/get-by-id conn :comment id {:for-update true}) _ (when-not comment (ex/raise :type :not-found)) thread (db/get-by-id conn :comment-thread (:thread-id comment) {:for-update true}) - _ (when-not thread (ex/raise :type :not-found))] + _ (when-not thread (ex/raise :type :not-found)) + pname (retrieve-page-name conn thread)] (files/check-read-permissions! conn profile-id (:file-id thread)) + (db/update! conn :comment {:content content :modified-at (dt/now)} {:id (:id comment)}) + (db/update! conn :comment-thread - {:modified-at (dt/now)} + {:modified-at (dt/now) + :page-name pname} {:id (:id thread)}) nil))) @@ -244,6 +260,7 @@ (db/delete! conn :comment-thread {:id id}) nil))) + ;; --- Mutation: Delete comment (s/def ::delete-comment diff --git a/backend/src/app/services/queries/comments.clj b/backend/src/app/services/queries/comments.clj index 5b000b212..4d2d569b3 100644 --- a/backend/src/app/services/queries/comments.clj +++ b/backend/src/app/services/queries/comments.clj @@ -16,6 +16,7 @@ [app.db :as db] [app.services.queries :as sq] [app.services.queries.files :as files] + [app.services.queries.teams :as teams] [app.util.time :as dt] [app.util.transit :as t] [clojure.spec.alpha :as s] @@ -32,9 +33,13 @@ (declare retrieve-comment-threads) +(s/def ::team-id ::us/uuid) (s/def ::file-id ::us/uuid) + (s/def ::comment-threads - (s/keys :req-un [::profile-id ::file-id])) + (s/and (s/keys :req-un [::profile-id] + :opt-un [::file-id ::team-id]) + #(or (:file-id %) (:team-id %)))) (sq/defquery ::comment-threads [{:keys [profile-id file-id] :as params}] @@ -45,6 +50,8 @@ (def sql:comment-threads "select distinct on (ct.id) ct.*, + f.name as file_name, + f.project_id as project_id, first_value(c.content) over w as content, (select count(1) from comment as c @@ -55,6 +62,7 @@ and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments from comment_thread as ct inner join comment as c on (c.thread_id = ct.id) + inner join file as f on (f.id = ct.file_id) left join comment_thread_status as cts on (cts.thread_id = ct.id and cts.profile_id = ?) @@ -62,10 +70,59 @@ window w as (partition by c.thread_id order by c.created_at asc)") (defn- retrieve-comment-threads - [conn {:keys [profile-id file-id]}] + [conn {:keys [profile-id file-id team-id]}] + (files/check-read-permissions! conn profile-id file-id) (->> (db/exec! conn [sql:comment-threads profile-id file-id]) (into [] (map decode-row)))) + +;; --- Query: Unread Comment Threads + +(declare retrieve-unread-comment-threads) + +(s/def ::team-id ::us/uuid) +(s/def ::unread-comment-threads + (s/keys :req-un [::profile-id ::team-id])) + +(sq/defquery ::unread-comment-threads + [{:keys [profile-id team-id] :as params}] + (with-open [conn (db/open)] + (teams/check-read-permissions! conn profile-id team-id) + (retrieve-unread-comment-threads conn params))) + +(def sql:comment-threads-by-team + "select distinct on (ct.id) + ct.*, + f.name as file_name, + f.project_id as project_id, + first_value(c.content) over w as content, + (select count(1) + from comment as c + where c.thread_id = ct.id) as count_comments, + (select count(1) + from comment as c + where c.thread_id = ct.id + and c.created_at >= coalesce(cts.modified_at, ct.created_at)) as count_unread_comments + from comment_thread as ct + inner join comment as c on (c.thread_id = ct.id) + inner join file as f on (f.id = ct.file_id) + inner join project as p on (p.id = f.project_id) + left join comment_thread_status as cts + on (cts.thread_id = ct.id and + cts.profile_id = ?) + where p.team_id = ? + window w as (partition by c.thread_id order by c.created_at asc)") + +(def sql:unread-comment-threads-by-team + (str "with threads as (" sql:comment-threads-by-team ")" + "select * from threads where count_unread_comments > 0")) + +(defn retrieve-unread-comment-threads + [conn {:keys [profile-id team-id]}] + (->> (db/exec! conn [sql:unread-comment-threads-by-team profile-id team-id]) + (into [] (map decode-row)))) + + ;; --- Query: Single Comment Thread (s/def ::id ::us/uuid) diff --git a/backend/src/app/services/queries/files.clj b/backend/src/app/services/queries/files.clj index 2f8edc315..12b2764ea 100644 --- a/backend/src/app/services/queries/files.clj +++ b/backend/src/app/services/queries/files.clj @@ -193,6 +193,12 @@ inner join file_profile_rel as fpr on (fpr.profile_id = pf.id) where fpr.file_id = ? union + select pf.id, pf.fullname, pf.photo + from profile as pf + inner join project_profile_rel as ppr on (ppr.profile_id = pf.id) + inner join file as f on (f.project_id = ppr.project_id) + where f.id = ? + union select pf.id, pf.fullname, pf.photo from profile as pf inner join team_profile_rel as tpr on (tpr.profile_id = pf.id) @@ -202,7 +208,7 @@ (defn retrieve-file-users [conn id] - (db/exec! conn [sql:file-users id id])) + (db/exec! conn [sql:file-users id id id])) (s/def ::file-users (s/keys :req-un [::profile-id ::id])) @@ -215,17 +221,8 @@ ;; --- Query: Shared Library Files -;; TODO: remove the counts, because they are no longer needed. - (def ^:private sql:shared-files - "select f.*, - (select count(*) from color as c - where c.file_id = f.id - and c.deleted_at is null) as colors_count, - (select count(*) from media_object as m - where m.file_id = f.id - and m.is_local = false - and m.deleted_at is null) as graphics_count + "select f.* from file as f inner join project as p on (p.id = f.project_id) where f.is_shared = true diff --git a/backend/src/app/services/queries/teams.clj b/backend/src/app/services/queries/teams.clj index cddd8b5ab..594ba5633 100644 --- a/backend/src/app/services/queries/teams.clj +++ b/backend/src/app/services/queries/teams.clj @@ -130,3 +130,38 @@ (defn retrieve-team-members [conn team-id] (db/exec! conn [sql:team-members team-id])) + +;; --- Query: Team Users + +;; This is a similar query to team members but can contain more data +;; because some user can be explicitly added to project or file (not +;; implemented in UI) + +(def sql:team-users + "select pf.id, pf.fullname, pf.photo + from profile as pf + inner join team_profile_rel as tpr on (tpr.profile_id = pf.id) + where tpr.team_id = ? + union + select pf.id, pf.fullname, pf.photo + from profile as pf + inner join project_profile_rel as ppr on (ppr.profile_id = pf.id) + inner join project as p on (ppr.project_id = p.id) + where p.team_id = ? + union + select pf.id, pf.fullname, pf.photo + from profile as pf + inner join file_profile_rel as fpr on (fpr.profile_id = pf.id) + inner join file as f on (fpr.file_id = f.id) + inner join project as p on (f.project_id = p.id) + where p.team_id = ?") + +(s/def ::team-users + (s/keys :req-un [::profile-id ::team-id])) + +(sq/defquery ::team-users + [{:keys [profile-id team-id]}] + (with-open [conn (db/open)] + (check-edition-permissions! conn profile-id team-id) + (db/exec! conn [sql:team-users team-id team-id team-id]))) + diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index 11a4c8a6b..c962a72da 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -1,20 +1,21 @@ const fs = require("fs"); -const path = require("path"); const l = require("lodash"); +const path = require("path"); const gulp = require("gulp"); -const gulpSass = require("gulp-sass"); +const gulpConcat = require("gulp-concat"); const gulpGzip = require("gulp-gzip"); const gulpMustache = require("gulp-mustache"); -const gulpRename = require("gulp-rename"); -const svgSprite = require("gulp-svg-sprite"); const gulpPostcss = require("gulp-postcss"); +const gulpRename = require("gulp-rename"); +const gulpSass = require("gulp-sass"); +const svgSprite = require("gulp-svg-sprite"); +const autoprefixer = require("autoprefixer") +const clean = require("postcss-clean"); const mkdirp = require("mkdirp"); const rimraf = require("rimraf"); const sass = require("sass"); -const autoprefixer = require("autoprefixer") -const clean = require("postcss-clean"); const mapStream = require("map-stream"); const paths = {}; @@ -52,7 +53,8 @@ function readManifest() { const content = JSON.parse(fs.readFileSync(path, {encoding: "utf8"})); const index = { - "config": "/js/config.js?ts=" + Date.now() + "config": "/js/config.js?ts=" + Date.now(), + "polyfills": "js/polyfills.js?ts=" + Date.now(), }; for (let item of content) { @@ -64,6 +66,7 @@ function readManifest() { console.error("Error on reading manifest, using default."); return { "config": "/js/config.js", + "polyfills": "js/polyfills.js", "main": "/js/main.js", "shared": "/js/shared.js", "worker": "/js/worker.js" @@ -123,7 +126,7 @@ gulp.task("scss", function() { .pipe(gulpSass().on('error', gulpSass.logError)) .pipe(gulpPostcss([ autoprefixer, - clean({format: "keep-breaks", level: 1}) + // clean({format: "keep-breaks", level: 1}) ])) .pipe(gulp.dest(paths.output + "css/")); }); @@ -142,6 +145,12 @@ gulp.task("template:main", templatePipeline({ gulp.task("templates", gulp.series("svg:sprite", "template:main")); +gulp.task("polyfills", function() { + return gulp.src(paths.resources + "polyfills/*.js") + .pipe(gulpConcat("polyfills.js")) + .pipe(gulp.dest(paths.output + "js/")); +}); + /*********************************************** * Development ***********************************************/ @@ -177,7 +186,7 @@ gulp.task("watch:main", function() { gulp.series("templates")); }); -gulp.task("build", gulp.parallel("scss", "templates", "copy:assets")); +gulp.task("build", gulp.parallel("polyfills", "scss", "templates", "copy:assets")); gulp.task("watch", gulp.series("dev:dirs", "build", "watch:main")); /*********************************************** diff --git a/frontend/package.json b/frontend/package.json index 10098e858..2bffc5c7a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "devDependencies": { "autoprefixer": "^10.0.1", "gulp": "4.0.2", + "gulp-concat": "^2.6.1", "gulp-gzip": "^1.4.2", "gulp-mustache": "^5.0.0", "gulp-postcss": "^9.0.0", diff --git a/frontend/resources/images/icons/comment.svg b/frontend/resources/images/icons/comment.svg new file mode 100644 index 000000000..f6c098e08 --- /dev/null +++ b/frontend/resources/images/icons/comment.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 0c1951a89..f08ce41e8 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -18,7 +18,7 @@ } }, "auth.create-demo-account" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:147" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:136", "src/app/main/ui/auth/login.cljs:147" ], "translations" : { "en" : "Create demo account", "fr" : "Vous voulez juste essayer?", @@ -27,7 +27,7 @@ } }, "auth.create-demo-profile" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:144", "src/app/main/ui/auth/register.cljs:133", "src/app/main/ui/auth/register.cljs:136" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:133", "src/app/main/ui/auth/login.cljs:144" ], "translations" : { "en" : "Just wanna try it?", "fr" : "Vous voulez juste essayer?", @@ -45,7 +45,7 @@ } }, "auth.email" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:92", "src/app/main/ui/auth/register.cljs:101", "src/app/main/ui/auth/recovery_request.cljs:47" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:101", "src/app/main/ui/auth/recovery_request.cljs:47", "src/app/main/ui/auth/login.cljs:92" ], "translations" : { "en" : "Email", "fr" : "Adresse email", @@ -186,7 +186,7 @@ } }, "auth.password" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:99", "src/app/main/ui/auth/register.cljs:106" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:106", "src/app/main/ui/auth/login.cljs:99" ], "translations" : { "en" : "Password", "fr" : "Mot de passe", @@ -249,7 +249,7 @@ } }, "auth.register-submit" : { - "used-in" : [ "src/app/main/ui/auth/login.cljs:128", "src/app/main/ui/auth/register.cljs:110" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:110", "src/app/main/ui/auth/login.cljs:128" ], "translations" : { "en" : "Create an account", "fr" : "Créer un compte", @@ -303,20 +303,20 @@ } }, "dashboard.create-new-team" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:155" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:159" ], "translations" : { "en" : "+ Create new team", "es" : "+ Crear nuevo equipo" } }, "dashboard.default-team-name" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:325" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:329" ], "translations" : { "en" : "Your penpot" } }, "dashboard.delete-team" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:309" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:313" ], "translations" : { "en" : "Delete team" } @@ -340,14 +340,14 @@ } }, "dashboard.invite-profile" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:69" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:72" ], "translations" : { "en" : "Invite to team", "es" : "Invitar al equipo" } }, "dashboard.leave-team" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:302", "src/app/main/ui/dashboard/sidebar.cljs:305" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:306", "src/app/main/ui/dashboard/sidebar.cljs:309" ], "translations" : { "en" : "Leave team" } @@ -470,7 +470,7 @@ } }, "dashboard.no-projects-placeholder" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:423" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:424" ], "translations" : { "en" : "Pinned projects will appear here" } @@ -503,7 +503,7 @@ } }, "dashboard.num-of-members" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:291" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:294" ], "translations" : { "en" : "%s members" } @@ -527,7 +527,7 @@ } }, "dashboard.promote-to-owner" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:193" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:196" ], "translations" : { "en" : "Promote to owner" } @@ -551,7 +551,7 @@ } }, "dashboard.search-placeholder" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:110" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:113" ], "translations" : { "en" : "Search...", "fr" : "Rechercher...", @@ -603,25 +603,25 @@ "unused" : true }, "dashboard.switch-team" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:140" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:144" ], "translations" : { "en" : "Switch Team" } }, "dashboard.team-info" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:274" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:277" ], "translations" : { "en" : "Team info" } }, "dashboard.team-members" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:285" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:288" ], "translations" : { "en" : "Team members" } }, "dashboard.team-projects" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:294" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:297" ], "translations" : { "en" : "Team projects" } @@ -645,7 +645,7 @@ } }, "dashboard.update-settings" : { - "used-in" : [ "src/app/main/ui/settings/options.cljs:72", "src/app/main/ui/settings/profile.cljs:80", "src/app/main/ui/settings/password.cljs:96" ], + "used-in" : [ "src/app/main/ui/settings/profile.cljs:80", "src/app/main/ui/settings/password.cljs:96", "src/app/main/ui/settings/options.cljs:72" ], "translations" : { "en" : "Update settings", "fr" : "Mettre à jour les paramètres", @@ -679,7 +679,7 @@ } }, "dashboard.your-penpot" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:144" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:148" ], "translations" : { "en" : "Your penpot" } @@ -766,7 +766,7 @@ } }, "errors.email-already-exists" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:47", "src/app/main/ui/auth/verify_token.cljs:80" ], + "used-in" : [ "src/app/main/ui/auth/verify_token.cljs:80", "src/app/main/ui/settings/change_email.cljs:47" ], "translations" : { "en" : "Email already used", "fr" : "Adresse e-mail déjà utilisée", @@ -793,7 +793,7 @@ } }, "errors.generic" : { - "used-in" : [ "src/app/main/ui/settings/options.cljs:32", "src/app/main/ui/settings/profile.cljs:40", "src/app/main/ui/auth/verify_token.cljs:89" ], + "used-in" : [ "src/app/main/ui/auth/verify_token.cljs:89", "src/app/main/ui/settings/profile.cljs:40", "src/app/main/ui/settings/options.cljs:32" ], "translations" : { "en" : "Something wrong has happened.", "fr" : "Quelque chose c'est mal passé.", @@ -820,7 +820,7 @@ } }, "errors.media-type-mismatch" : { - "used-in" : [ "src/app/main/data/media.cljs:61", "src/app/main/data/workspace/persistence.cljs:421" ], + "used-in" : [ "src/app/main/data/workspace/persistence.cljs:390", "src/app/main/data/media.cljs:61" ], "translations" : { "en" : "Seems that the contents of the image does not match the file extension.", "fr" : "", @@ -829,7 +829,7 @@ } }, "errors.media-type-not-allowed" : { - "used-in" : [ "src/app/main/data/media.cljs:58", "src/app/main/data/workspace/persistence.cljs:418" ], + "used-in" : [ "src/app/main/data/workspace/persistence.cljs:387", "src/app/main/data/media.cljs:58" ], "translations" : { "en" : "Seems that this is not a valid image.", "fr" : "", @@ -874,7 +874,7 @@ } }, "errors.unexpected-error" : { - "used-in" : [ "src/app/main/data/media.cljs:64", "src/app/main/ui/auth/register.cljs:45", "src/app/main/ui/workspace/sidebar/options/exports.cljs:66" ], + "used-in" : [ "src/app/main/data/media.cljs:64", "src/app/main/ui/workspace/sidebar/options/exports.cljs:66", "src/app/main/ui/auth/register.cljs:45", "src/app/main/ui/handoff/exports.cljs:41" ], "translations" : { "en" : "An unexpected error occurred.", "fr" : "Une erreur inattendue c'est produite", @@ -901,31 +901,31 @@ } }, "handoff.attributes.blur" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/blur.cljs:33" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/blur.cljs:34" ], "translations" : { "en" : "Blur" } }, "handoff.attributes.blur.value" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/blur.cljs:39" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/blur.cljs:40" ], "translations" : { "en" : "Value" } }, "handoff.attributes.color.hex" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/common.cljs:72" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/common.cljs:70" ], "translations" : { "en" : "HEX" } }, "handoff.attributes.color.hsla" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/common.cljs:78" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/common.cljs:76" ], "translations" : { "en" : "HSLA" } }, "handoff.attributes.color.rgba" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/common.cljs:75" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/common.cljs:73" ], "translations" : { "en" : "RGBA" } @@ -937,97 +937,97 @@ "unused" : true }, "handoff.attributes.fill" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/fill.cljs:58" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/fill.cljs:57" ], "translations" : { "en" : "Fill" } }, "handoff.attributes.image.download" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/image.cljs:44" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/image.cljs:45" ], "translations" : { "en" : "Dowload source image" } }, "handoff.attributes.image.height" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/image.cljs:36" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/image.cljs:37" ], "translations" : { "en" : "Height" } }, "handoff.attributes.image.width" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/image.cljs:31" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/image.cljs:32" ], "translations" : { "en" : "Width" } }, "handoff.attributes.layout" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:76" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:76" ], "translations" : { "en" : "Layout" } }, "handoff.attributes.layout.height" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:36" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:43" ], "translations" : { "en" : "Height" } }, "handoff.attributes.layout.left" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:44" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:49" ], "translations" : { "en" : "Left" } }, "handoff.attributes.layout.radius" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:60" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:61" ], "translations" : { "en" : "Radius" } }, "handoff.attributes.layout.rotation" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:60" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:67" ], "translations" : { "en" : "Rotation" } }, "handoff.attributes.layout.top" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:52" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:55" ], "translations" : { "en" : "Top" } }, "handoff.attributes.layout.width" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/layout.cljs:29" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/layout.cljs:38" ], "translations" : { "en" : "Width" } }, "handoff.attributes.shadow" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/shadow.cljs:71" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs:71" ], "translations" : { "en" : "Shadow" } }, "handoff.attributes.shadow.shorthand.blur" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/shadow.cljs:44" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs:53" ], "translations" : { "en" : "B" } }, "handoff.attributes.shadow.shorthand.offset-x" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/shadow.cljs:36" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs:45" ], "translations" : { "en" : "X" } }, "handoff.attributes.shadow.shorthand.offset-y" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/shadow.cljs:40" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs:49" ], "translations" : { "en" : "Y" } }, "handoff.attributes.shadow.shorthand.spread" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/shadow.cljs:48" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/shadow.cljs:57" ], "translations" : { "en" : "S" } @@ -1045,7 +1045,7 @@ "unused" : true }, "handoff.attributes.stroke" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/stroke.cljs:75" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/stroke.cljs:75" ], "translations" : { "en" : "Stroke" } @@ -1099,49 +1099,49 @@ "unused" : true }, "handoff.attributes.stroke.width" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/stroke.cljs:57" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/stroke.cljs:63" ], "translations" : { "en" : "Width" } }, "handoff.attributes.typography" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/text.cljs:159" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:190" ], "translations" : { "en" : "Typography" } }, "handoff.attributes.typography.font-family" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/text.cljs:89" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:121" ], "translations" : { "en" : "Font Family" } }, "handoff.attributes.typography.font-size" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/text.cljs:101" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:133" ], "translations" : { "en" : "Font Size" } }, "handoff.attributes.typography.font-style" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/text.cljs:95" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:127" ], "translations" : { "en" : "Font Style" } }, "handoff.attributes.typography.letter-spacing" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/text.cljs:113" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:145" ], "translations" : { "en" : "Letter Spacing" } }, "handoff.attributes.typography.line-height" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/text.cljs:107" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:139" ], "translations" : { "en" : "Line Height" } }, "handoff.attributes.typography.text-decoration" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/text.cljs:119" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:151" ], "translations" : { "en" : "Text Decoration" } @@ -1165,7 +1165,7 @@ "unused" : true }, "handoff.attributes.typography.text-transform" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/text.cljs:125" ], + "used-in" : [ "src/app/main/ui/handoff/attributes/text.cljs:157" ], "translations" : { "en" : "Text Transform" } @@ -1195,7 +1195,7 @@ "unused" : true }, "handoff.tabs.code" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/right_sidebar.cljs:78" ], + "used-in" : [ "src/app/main/ui/handoff/right_sidebar.cljs:65" ], "translations" : { "en" : "Code" } @@ -1231,7 +1231,7 @@ "unused" : true }, "handoff.tabs.code.selected.multiple" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/right_sidebar.cljs:65" ], + "used-in" : [ "src/app/main/ui/handoff/right_sidebar.cljs:48" ], "translations" : { "en" : "%s Selected" } @@ -1255,7 +1255,7 @@ "unused" : true }, "handoff.tabs.info" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/right_sidebar.cljs:74" ], + "used-in" : [ "src/app/main/ui/handoff/right_sidebar.cljs:59" ], "translations" : { "en" : "Info" } @@ -1270,13 +1270,13 @@ "unused" : true }, "labels.admin" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:82", "src/app/main/ui/dashboard/team.cljs:171", "src/app/main/ui/dashboard/team.cljs:187" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:85", "src/app/main/ui/dashboard/team.cljs:174", "src/app/main/ui/dashboard/team.cljs:190" ], "translations" : { "en" : "Admin" } }, "labels.cancel" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:199" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:203" ], "translations" : { "en" : "Cancel", "fr" : "Annuler", @@ -1284,6 +1284,12 @@ "es" : "Cancelar" } }, + "labels.comments" : { + "used-in" : [ "src/app/main/ui/dashboard/comments.cljs:75" ], + "translations" : { + "en" : "Comments" + } + }, "labels.confirm-password" : { "used-in" : [ "src/app/main/ui/settings/password.cljs:93" ], "translations" : { @@ -1300,7 +1306,7 @@ } }, "labels.delete" : { - "used-in" : [ "src/app/main/ui/dashboard/files.cljs:85", "src/app/main/ui/dashboard/grid.cljs:177" ], + "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:177", "src/app/main/ui/dashboard/files.cljs:85" ], "translations" : { "en" : "Delete", "fr" : "Supprimer", @@ -1308,8 +1314,20 @@ "es" : "Borrar" } }, + "labels.delete-comment" : { + "used-in" : [ "src/app/main/ui/comments.cljs:273" ], + "translations" : { + "en" : "Delete comment" + } + }, + "labels.delete-comment-thread" : { + "used-in" : [ "src/app/main/ui/comments.cljs:272" ], + "translations" : { + "en" : "Delete thread" + } + }, "labels.drafts" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:402" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:404" ], "translations" : { "en" : "Drafts", "fr" : "Brouillons", @@ -1317,14 +1335,20 @@ "es" : "Borradores" } }, + "labels.edit" : { + "used-in" : [ "src/app/main/ui/comments.cljs:270" ], + "translations" : { + "en" : "Edit" + } + }, "labels.editor" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:83", "src/app/main/ui/dashboard/team.cljs:174", "src/app/main/ui/dashboard/team.cljs:188" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:86", "src/app/main/ui/dashboard/team.cljs:177", "src/app/main/ui/dashboard/team.cljs:191" ], "translations" : { "en" : "Editor" } }, "labels.email" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:109", "src/app/main/ui/dashboard/team.cljs:212" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:112", "src/app/main/ui/dashboard/team.cljs:215" ], "translations" : { "en" : "Email", "fr" : "Adresse email", @@ -1332,6 +1356,12 @@ "es" : "Correo electrónico" } }, + "labels.hide-resolved-comments" : { + "used-in" : [ "src/app/main/ui/workspace/comments.cljs:129", "src/app/main/ui/viewer/header.cljs:175" ], + "translations" : { + "en" : "Hide resolved comments" + } + }, "labels.language" : { "used-in" : [ "src/app/main/ui/settings/options.cljs:54" ], "translations" : { @@ -1342,7 +1372,7 @@ } }, "labels.logout" : { - "used-in" : [ "src/app/main/ui/settings.cljs:31", "src/app/main/ui/dashboard/sidebar.cljs:457" ], + "used-in" : [ "src/app/main/ui/settings.cljs:31", "src/app/main/ui/dashboard/sidebar.cljs:456" ], "translations" : { "en" : "Logout", "fr" : "Quitter", @@ -1351,13 +1381,13 @@ } }, "labels.members" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:295", "src/app/main/ui/dashboard/team.cljs:59", "src/app/main/ui/dashboard/team.cljs:63" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:60", "src/app/main/ui/dashboard/team.cljs:66", "src/app/main/ui/dashboard/sidebar.cljs:299" ], "translations" : { "en" : "Members" } }, "labels.name" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:211" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:214" ], "translations" : { "en" : "Name", "fr" : "Nom", @@ -1374,6 +1404,12 @@ "es" : "Nueva contraseña" } }, + "labels.no-comments-available" : { + "translations" : { + "en" : "No comments" + }, + "unused" : true + }, "labels.old-password" : { "used-in" : [ "src/app/main/ui/settings/password.cljs:81" ], "translations" : { @@ -1384,13 +1420,13 @@ } }, "labels.owner" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:168" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:171", "src/app/main/ui/dashboard/team.cljs:291" ], "translations" : { "en" : "Owner" } }, "labels.password" : { - "used-in" : [ "src/app/main/ui/settings/sidebar.cljs:75", "src/app/main/ui/dashboard/sidebar.cljs:454" ], + "used-in" : [ "src/app/main/ui/settings/sidebar.cljs:75", "src/app/main/ui/dashboard/sidebar.cljs:453" ], "translations" : { "en" : "Password", "fr" : "Mot de passe", @@ -1399,14 +1435,14 @@ } }, "labels.permissions" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:213" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:216" ], "translations" : { "en" : "Permissions", "es" : "Permisos" } }, "labels.profile" : { - "used-in" : [ "src/app/main/ui/settings/sidebar.cljs:70", "src/app/main/ui/dashboard/sidebar.cljs:451" ], + "used-in" : [ "src/app/main/ui/settings/sidebar.cljs:70", "src/app/main/ui/dashboard/sidebar.cljs:450" ], "translations" : { "en" : "Profile", "fr" : "Profil", @@ -1415,7 +1451,7 @@ } }, "labels.projects" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:397" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:400" ], "translations" : { "en" : "Projects", "fr" : "Projetes", @@ -1424,7 +1460,7 @@ } }, "labels.remove" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:91", "src/app/main/ui/dashboard/team.cljs:199" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:92", "src/app/main/ui/dashboard/team.cljs:202" ], "translations" : { "en" : "Remove", "fr" : "", @@ -1433,20 +1469,20 @@ } }, "labels.rename" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:298", "src/app/main/ui/dashboard/files.cljs:84", "src/app/main/ui/dashboard/grid.cljs:176" ], + "used-in" : [ "src/app/main/ui/dashboard/grid.cljs:176", "src/app/main/ui/dashboard/sidebar.cljs:302", "src/app/main/ui/dashboard/files.cljs:84" ], "translations" : { "en" : "Rename", "es" : "Renombrar" } }, "labels.role" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:81" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:84" ], "translations" : { "en" : "Role" } }, "labels.settings" : { - "used-in" : [ "src/app/main/ui/settings/sidebar.cljs:80", "src/app/main/ui/dashboard/sidebar.cljs:296", "src/app/main/ui/dashboard/team.cljs:65" ], + "used-in" : [ "src/app/main/ui/settings/sidebar.cljs:80", "src/app/main/ui/dashboard/team.cljs:61", "src/app/main/ui/dashboard/team.cljs:68", "src/app/main/ui/dashboard/sidebar.cljs:300" ], "translations" : { "en" : "Settings", "fr" : "Settings", @@ -1455,7 +1491,7 @@ } }, "labels.shared-libraries" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:408" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:409" ], "translations" : { "en" : "Shared Libraries", "fr" : "", @@ -1463,6 +1499,18 @@ "es" : "Bibliotecas Compartidas" } }, + "labels.show-all-comments" : { + "used-in" : [ "src/app/main/ui/workspace/comments.cljs:117", "src/app/main/ui/viewer/header.cljs:163" ], + "translations" : { + "en" : "Show all comments" + } + }, + "labels.show-your-comments" : { + "used-in" : [ "src/app/main/ui/workspace/comments.cljs:122", "src/app/main/ui/viewer/header.cljs:168" ], + "translations" : { + "en" : "Show only yours comments" + } + }, "labels.update" : { "used-in" : [ "src/app/main/ui/settings/profile.cljs:106" ], "translations" : { @@ -1473,14 +1521,20 @@ } }, "labels.viewer" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:84", "src/app/main/ui/dashboard/team.cljs:177", "src/app/main/ui/dashboard/team.cljs:189" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:87", "src/app/main/ui/dashboard/team.cljs:180", "src/app/main/ui/dashboard/team.cljs:192" ], "translations" : { "en" : "Viewer", "es" : "Visualizador" } }, + "labels.write-new-comment" : { + "used-in" : [ "src/app/main/ui/comments.cljs:151" ], + "translations" : { + "en" : "Write new comment" + } + }, "media.loading" : { - "used-in" : [ "src/app/main/data/media.cljs:43", "src/app/main/data/workspace/persistence.cljs:402" ], + "used-in" : [ "src/app/main/data/workspace/persistence.cljs:371", "src/app/main/data/media.cljs:43" ], "translations" : { "en" : "Loading image...", "fr" : "Chargement de l'image...", @@ -1606,19 +1660,19 @@ } }, "modals.delete-comment-thread.accept" : { - "used-in" : [ "src/app/main/ui/workspace/comments.cljs:236" ], + "used-in" : [ "src/app/main/ui/comments.cljs:222" ], "translations" : { "en" : "Delete conversation" } }, "modals.delete-comment-thread.message" : { - "used-in" : [ "src/app/main/ui/workspace/comments.cljs:235" ], + "used-in" : [ "src/app/main/ui/comments.cljs:221" ], "translations" : { "en" : "Are you sure you want to delete this conversation? All comments in this thread will be deleted." } }, "modals.delete-comment-thread.title" : { - "used-in" : [ "src/app/main/ui/workspace/comments.cljs:234" ], + "used-in" : [ "src/app/main/ui/comments.cljs:220" ], "translations" : { "en" : "Delete conversation" } @@ -1660,109 +1714,109 @@ } }, "modals.delete-team-confirm.accept" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:285" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:289" ], "translations" : { "en" : "Delete team" } }, "modals.delete-team-confirm.message" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:284" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:288" ], "translations" : { "en" : "Are you sure you want to delete this team? All projects and files associated with team will be permanently deleted." } }, "modals.delete-team-confirm.title" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:283" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:287" ], "translations" : { "en" : "Deleting team" } }, "modals.delete-team-member-confirm.accept" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:157" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:160" ], "translations" : { "en" : "Delete member" } }, "modals.delete-team-member-confirm.message" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:156" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:159" ], "translations" : { "en" : "Are you sure wan't to delete this user from team?" } }, "modals.delete-team-member-confirm.title" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:155" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:158" ], "translations" : { "en" : "Delete team member" } }, "modals.invite-member.title" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:105" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:108" ], "translations" : { "en" : "Invite a new team member" } }, "modals.leave-and-reassign.hint1" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:188" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:192" ], "translations" : { "en" : "You are %s owner." } }, "modals.leave-and-reassign.hint2" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:189" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:193" ], "translations" : { "en" : "Select an other member to promote before leave" } }, "modals.leave-and-reassign.promote-and-leave" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:206" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:210" ], "translations" : { "en" : "Promote and leave" } }, "modals.leave-and-reassign.select-memeber-to-promote" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:166" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:170" ], "translations" : { "en" : "Select a member to promote" } }, "modals.leave-and-reassign.title" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:183" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:187" ], "translations" : { "en" : "Select a member to promote" } }, "modals.leave-confirm.accept" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:260" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:264" ], "translations" : { "en" : "Leave team" } }, "modals.leave-confirm.message" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:259" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:263" ], "translations" : { "en" : "Are you sure you want to leave this team?" } }, "modals.leave-confirm.title" : { - "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:258" ], + "used-in" : [ "src/app/main/ui/dashboard/sidebar.cljs:262" ], "translations" : { "en" : "Leaving team" } }, "modals.promote-owner-confirm.accept" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:144" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:147" ], "translations" : { "en" : "Promote" } }, "modals.promote-owner-confirm.message" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:143" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:146" ], "translations" : { "en" : "Are you sure you wan't to promote this user to owner?" } }, "modals.promote-owner-confirm.title" : { - "used-in" : [ "src/app/main/ui/dashboard/team.cljs:142" ], + "used-in" : [ "src/app/main/ui/dashboard/team.cljs:145" ], "translations" : { "en" : "Promote to owner" } @@ -1804,7 +1858,7 @@ } }, "notifications.profile-saved" : { - "used-in" : [ "src/app/main/ui/settings/options.cljs:36", "src/app/main/ui/settings/profile.cljs:36" ], + "used-in" : [ "src/app/main/ui/settings/profile.cljs:36", "src/app/main/ui/settings/options.cljs:36" ], "translations" : { "en" : "Profile saved successfully!", "fr" : "Profil enregistré avec succès!", @@ -1813,7 +1867,7 @@ } }, "notifications.validation-email-sent" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:56", "src/app/main/ui/auth/register.cljs:54" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:54", "src/app/main/ui/settings/change_email.cljs:56" ], "translations" : { "en" : "Verification email sent to %s; check your email!" } @@ -1828,7 +1882,7 @@ } }, "settings.multiple" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:153", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:163", "src/app/main/ui/workspace/sidebar/options/typography.cljs:99", "src/app/main/ui/workspace/sidebar/options/typography.cljs:149", "src/app/main/ui/workspace/sidebar/options/typography.cljs:162", "src/app/main/ui/workspace/sidebar/options/stroke.cljs:147" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:147", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:153", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:163", "src/app/main/ui/workspace/sidebar/options/typography.cljs:99", "src/app/main/ui/workspace/sidebar/options/typography.cljs:149", "src/app/main/ui/workspace/sidebar/options/typography.cljs:162" ], "translations" : { "en" : "Mixed", "fr" : null, @@ -1855,7 +1909,7 @@ "unused" : true }, "viewer.empty-state" : { - "used-in" : [ "src/app/main/ui/viewer/handoff.cljs:56", "src/app/main/ui/viewer.cljs:42" ], + "used-in" : [ "src/app/main/ui/handoff.cljs:55", "src/app/main/ui/viewer.cljs:193" ], "translations" : { "en" : "No frames found on the page.", "fr" : "Aucun cadre trouvé sur la page.", @@ -1864,7 +1918,7 @@ } }, "viewer.frame-not-found" : { - "used-in" : [ "src/app/main/ui/viewer/handoff.cljs:60", "src/app/main/ui/viewer.cljs:46" ], + "used-in" : [ "src/app/main/ui/handoff.cljs:59", "src/app/main/ui/viewer.cljs:197" ], "translations" : { "en" : "Frame not found.", "fr" : "Cadre introuvable.", @@ -1872,12 +1926,8 @@ "es" : "No se encuentra el tablero." } }, - "labels.show-all-comments": "Show all comments", - "labels.show-your-comments": "Show only yours comments", - "labels.hide-resolved-comments": "Hide resolved comments", - "viewer.header.dont-show-interactions" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:68" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:124" ], "translations" : { "en" : "Don't show interactions", "fr" : "Ne pas afficher les interactions", @@ -1886,7 +1936,7 @@ } }, "viewer.header.edit-page" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:183" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:264" ], "translations" : { "en" : "Edit page", "fr" : "Editer la page", @@ -1895,7 +1945,7 @@ } }, "viewer.header.fullscreen" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:194" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:275" ], "translations" : { "en" : "Full Screen", "fr" : "Plein écran", @@ -1904,7 +1954,7 @@ } }, "viewer.header.share.copy-link" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:113" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:92" ], "translations" : { "en" : "Copy link", "fr" : "Copier lien", @@ -1913,7 +1963,7 @@ } }, "viewer.header.share.create-link" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:122" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:101" ], "translations" : { "en" : "Create link", "fr" : "Créer lien", @@ -1922,7 +1972,7 @@ } }, "viewer.header.share.placeholder" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:114" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:93" ], "translations" : { "en" : "Share link will appear here", "fr" : "Le lien de partage apparaîtra ici", @@ -1931,7 +1981,7 @@ } }, "viewer.header.share.remove-link" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:120" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:99" ], "translations" : { "en" : "Remove link", "fr" : "Supprimer le lien", @@ -1940,7 +1990,7 @@ } }, "viewer.header.share.subtitle" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:116" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:95" ], "translations" : { "en" : "Anyone with the link will have access", "fr" : "Toute personne disposant du lien aura accès", @@ -1949,7 +1999,7 @@ } }, "viewer.header.share.title" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:99", "src/app/main/ui/viewer/header.cljs:101", "src/app/main/ui/viewer/header.cljs:107" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:78", "src/app/main/ui/viewer/header.cljs:80", "src/app/main/ui/viewer/header.cljs:86" ], "translations" : { "en" : "Share link", "fr" : "Lien de partage", @@ -1958,7 +2008,7 @@ } }, "viewer.header.show-interactions" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:72" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:129" ], "translations" : { "en" : "Show interactions", "fr" : "Afficher les interactions", @@ -1967,7 +2017,7 @@ } }, "viewer.header.show-interactions-on-click" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:76" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:134" ], "translations" : { "en" : "Show interactions on click", "fr" : "Afficher les interactions au clic", @@ -1976,7 +2026,7 @@ } }, "viewer.header.sitemap" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:156" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:223" ], "translations" : { "en" : "Sitemap", "fr" : "Plan du site", @@ -2057,7 +2107,7 @@ } }, "workspace.assets.assets" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:630" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:668" ], "translations" : { "en" : "Assets", "fr" : "", @@ -2066,7 +2116,7 @@ } }, "workspace.assets.box-filter-all" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:650" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:688" ], "translations" : { "en" : "All assets", "fr" : "", @@ -2093,7 +2143,7 @@ "unused" : true }, "workspace.assets.colors" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:330", "src/app/main/ui/workspace/sidebar/assets.cljs:653" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:366", "src/app/main/ui/workspace/sidebar/assets.cljs:691" ], "translations" : { "en" : "Colors", "fr" : "", @@ -2102,7 +2152,7 @@ } }, "workspace.assets.components" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:84", "src/app/main/ui/workspace/sidebar/assets.cljs:651" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:109", "src/app/main/ui/workspace/sidebar/assets.cljs:689" ], "translations" : { "en" : "Components", "fr" : "", @@ -2111,7 +2161,7 @@ } }, "workspace.assets.delete" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:104", "src/app/main/ui/workspace/sidebar/assets.cljs:192", "src/app/main/ui/workspace/sidebar/assets.cljs:306", "src/app/main/ui/workspace/sidebar/assets.cljs:434" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:140", "src/app/main/ui/workspace/sidebar/assets.cljs:228", "src/app/main/ui/workspace/sidebar/assets.cljs:342", "src/app/main/ui/workspace/sidebar/assets.cljs:470" ], "translations" : { "en" : "Delete", "fr" : "", @@ -2120,6 +2170,7 @@ } }, "workspace.assets.duplicate" : { + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:139" ], "translations" : { "en" : "Duplicate", "fr" : "", @@ -2128,7 +2179,7 @@ } }, "workspace.assets.edit" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:305", "src/app/main/ui/workspace/sidebar/assets.cljs:433" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:341", "src/app/main/ui/workspace/sidebar/assets.cljs:469" ], "translations" : { "en" : "Edit", "fr" : "", @@ -2137,7 +2188,7 @@ } }, "workspace.assets.file-library" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:532" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:568" ], "translations" : { "en" : "File library", "fr" : "", @@ -2146,7 +2197,7 @@ } }, "workspace.assets.graphics" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:165", "src/app/main/ui/workspace/sidebar/assets.cljs:652" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:201", "src/app/main/ui/workspace/sidebar/assets.cljs:690" ], "translations" : { "en" : "Graphics", "fr" : "", @@ -2155,7 +2206,7 @@ } }, "workspace.assets.libraries" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:633" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:671" ], "translations" : { "en" : "Libraries", "fr" : "", @@ -2164,7 +2215,7 @@ } }, "workspace.assets.not-found" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:593" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:629" ], "translations" : { "en" : "No assets found", "fr" : "", @@ -2173,7 +2224,7 @@ } }, "workspace.assets.rename" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:304", "src/app/main/ui/workspace/sidebar/assets.cljs:432" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:138", "src/app/main/ui/workspace/sidebar/assets.cljs:340", "src/app/main/ui/workspace/sidebar/assets.cljs:468" ], "translations" : { "en" : "Rename", "fr" : "", @@ -2182,7 +2233,7 @@ } }, "workspace.assets.search" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:637" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:675" ], "translations" : { "en" : "Search assets", "fr" : "", @@ -2191,7 +2242,7 @@ } }, "workspace.assets.shared" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:534" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:570" ], "translations" : { "en" : "SHARED", "fr" : "", @@ -2200,7 +2251,7 @@ } }, "workspace.assets.typography" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:421", "src/app/main/ui/workspace/sidebar/assets.cljs:654" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:457", "src/app/main/ui/workspace/sidebar/assets.cljs:692" ], "translations" : { "en" : "Typographies" } @@ -2242,7 +2293,7 @@ } }, "workspace.assets.typography.sample" : { - "used-in" : [ "src/app/main/ui/viewer/handoff/attributes/text.cljs:65", "src/app/main/ui/workspace/sidebar/options/typography.cljs:255" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:255", "src/app/main/ui/handoff/attributes/text.cljs:97", "src/app/main/ui/handoff/attributes/text.cljs:106" ], "translations" : { "en" : "Ag" } @@ -2254,13 +2305,13 @@ } }, "workspace.gradients.linear" : { - "used-in" : [ "src/app/main/data/workspace/libraries.cljs:39", "src/app/main/ui/components/color_bullet.cljs:31" ], + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:42", "src/app/main/ui/components/color_bullet.cljs:31" ], "translations" : { "en" : "Linear gradient" } }, "workspace.gradients.radial" : { - "used-in" : [ "src/app/main/data/workspace/libraries.cljs:40", "src/app/main/ui/components/color_bullet.cljs:32" ], + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:43", "src/app/main/ui/components/color_bullet.cljs:32" ], "translations" : { "en" : "Radial gradient" } @@ -2425,7 +2476,7 @@ } }, "workspace.libraries.add" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:115" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:116" ], "translations" : { "en" : "Add", "fr" : "", @@ -2434,7 +2485,7 @@ } }, "workspace.libraries.colors" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:43" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:44" ], "translations" : { "en" : "%s colors", "fr" : "", @@ -2473,7 +2524,7 @@ } }, "workspace.libraries.components" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:37" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:38" ], "translations" : { "en" : "%s components", "fr" : "", @@ -2482,7 +2533,7 @@ } }, "workspace.libraries.file-library" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:84" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:85" ], "translations" : { "en" : "File library", "fr" : "", @@ -2491,7 +2542,7 @@ } }, "workspace.libraries.graphics" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:40" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:41" ], "translations" : { "en" : "%s graphics", "fr" : "", @@ -2500,7 +2551,7 @@ } }, "workspace.libraries.in-this-file" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:81" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:82" ], "translations" : { "en" : "LIBRARIES IN THIS FILE", "fr" : "", @@ -2509,7 +2560,7 @@ } }, "workspace.libraries.libraries" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:175" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:177" ], "translations" : { "en" : "LIBRARIES", "fr" : "", @@ -2518,7 +2569,7 @@ } }, "workspace.libraries.library" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:135" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:136" ], "translations" : { "en" : "LIBRARY", "fr" : "", @@ -2527,7 +2578,7 @@ } }, "workspace.libraries.no-libraries-need-sync" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:133" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:134" ], "translations" : { "en" : "There are no Shared Libraries that need update", "fr" : "", @@ -2536,7 +2587,7 @@ } }, "workspace.libraries.no-matches-for" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:121" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:122" ], "translations" : { "en" : "No matches found for “%s“", "fr" : "Aucune correspondance pour “%s“", @@ -2545,7 +2596,7 @@ } }, "workspace.libraries.no-shared-libraries-available" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:120" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:121" ], "translations" : { "en" : "There are no Shared Libraries available", "fr" : "", @@ -2554,7 +2605,7 @@ } }, "workspace.libraries.search-shared-libraries" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:98" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:99" ], "translations" : { "en" : "Search shared libraries", "fr" : "", @@ -2563,7 +2614,7 @@ } }, "workspace.libraries.shared-libraries" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:95" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:96" ], "translations" : { "en" : "SHARED LIBRARIES", "fr" : "", @@ -2584,13 +2635,13 @@ } }, "workspace.libraries.typography" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:46" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:47" ], "translations" : { "en" : "%s typographies" } }, "workspace.libraries.update" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:142" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:143" ], "translations" : { "en" : "Update", "fr" : "", @@ -2599,7 +2650,7 @@ } }, "workspace.libraries.updates" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:179" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:181" ], "translations" : { "en" : "UPDATES", "fr" : "", @@ -2689,6 +2740,7 @@ } }, "workspace.options.component" : { + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/component.cljs:62" ], "translations" : { "en" : "Component", "es" : "Componente" @@ -2704,21 +2756,21 @@ } }, "workspace.options.export" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/exports.cljs:123" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/exports.cljs:123", "src/app/main/ui/handoff/exports.cljs:96" ], "translations" : { "en" : "Export", "ru" : "Экспорт" } }, "workspace.options.export-object" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/exports.cljs:156" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/exports.cljs:156", "src/app/main/ui/handoff/exports.cljs:131" ], "translations" : { "en" : "Export shape", "ru" : "Экспорт фигуры" } }, "workspace.options.exporting-object" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/exports.cljs:155" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/exports.cljs:155", "src/app/main/ui/handoff/exports.cljs:130" ], "translations" : { "en" : "Exporting...", "ru" : "Экспортирую ..." @@ -2959,7 +3011,7 @@ } }, "workspace.options.position" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame.cljs:127", "src/app/main/ui/workspace/sidebar/options/measures.cljs:146" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs:146", "src/app/main/ui/workspace/sidebar/options/frame.cljs:127" ], "translations" : { "en" : "Position", "fr" : "Position", @@ -3073,7 +3125,7 @@ } }, "workspace.options.size" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/frame.cljs:100", "src/app/main/ui/workspace/sidebar/options/measures.cljs:116" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs:116", "src/app/main/ui/workspace/sidebar/options/frame.cljs:100" ], "translations" : { "en" : "Size", "fr" : "Taille", @@ -3280,7 +3332,7 @@ } }, "workspace.options.text-options.none" : { - "used-in" : [ "src/app/main/ui/workspace/sidebar/options/typography.cljs:178", "src/app/main/ui/workspace/sidebar/options/text.cljs:154" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:154", "src/app/main/ui/workspace/sidebar/options/typography.cljs:178" ], "translations" : { "en" : "None", "fr" : "Aucune", @@ -3377,138 +3429,139 @@ } }, "workspace.shape.menu.back" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:103" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:104" ], "translations" : { "en" : "Send to back" } }, "workspace.shape.menu.backward" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:100" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:101" ], "translations" : { "en" : "Send backward" } }, "workspace.shape.menu.copy" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:81" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:82" ], "translations" : { "en" : "Copy" } }, "workspace.shape.menu.create-component" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:145" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:147" ], "translations" : { "en" : "Create component" } }, "workspace.shape.menu.cut" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:84" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:85" ], "translations" : { "en" : "Cut" } }, "workspace.shape.menu.delete" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:163" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:177" ], "translations" : { "en" : "Delete" } }, "workspace.shape.menu.detach-instance" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:152" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/component.cljs:76", "src/app/main/ui/workspace/sidebar/options/component.cljs:81", "src/app/main/ui/workspace/context_menu.cljs:159", "src/app/main/ui/workspace/context_menu.cljs:169" ], "translations" : { "en" : "Detach instance" } }, "workspace.shape.menu.duplicate" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:90" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:91" ], "translations" : { "en" : "Duplicate" } }, "workspace.shape.menu.forward" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:94" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:95" ], "translations" : { "en" : "Bring forward" } }, "workspace.shape.menu.front" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:97" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:98" ], "translations" : { "en" : "Bring to front" } }, "workspace.shape.menu.go-master" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:159" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/component.cljs:83", "src/app/main/ui/workspace/context_menu.cljs:173" ], "translations" : { "en" : "Go to master component file" } }, - "workspace.shape.menu.show-master" : { - "translations" : { - "en" : "Show master component" - } - }, "workspace.shape.menu.group" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:110" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:111" ], "translations" : { "en" : "Group" } }, "workspace.shape.menu.hide" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:133" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:134" ], "translations" : { "en" : "Hide" } }, "workspace.shape.menu.lock" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:139" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:140" ], "translations" : { "en" : "Lock" } }, "workspace.shape.menu.mask" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:113" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:114" ], "translations" : { "en" : "Mask" } }, "workspace.shape.menu.paste" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:87", "src/app/main/ui/workspace/context_menu.cljs:172" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:88", "src/app/main/ui/workspace/context_menu.cljs:186" ], "translations" : { "en" : "Paste" } }, "workspace.shape.menu.reset-overrides" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:154" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/component.cljs:77", "src/app/main/ui/workspace/sidebar/options/component.cljs:82", "src/app/main/ui/workspace/context_menu.cljs:161", "src/app/main/ui/workspace/context_menu.cljs:171" ], "translations" : { "en" : "Reset overrides" } }, "workspace.shape.menu.show" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:131" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:132" ], "translations" : { "en" : "Show" } }, + "workspace.shape.menu.show-master" : { + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/component.cljs:79", "src/app/main/ui/workspace/context_menu.cljs:165" ], + "translations" : { + "en" : "Show master component" + } + }, "workspace.shape.menu.ungroup" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:119" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:120" ], "translations" : { "en" : "Ungroup" } }, "workspace.shape.menu.unlock" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:137" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:138" ], "translations" : { "en" : "Unlock" } }, "workspace.shape.menu.unmask" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:123" ], + "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:124" ], "translations" : { "en" : "Unmask" } }, "workspace.shape.menu.update-master" : { - "used-in" : [ "src/app/main/ui/workspace/context_menu.cljs:157" ], + "used-in" : [ "src/app/main/ui/workspace/sidebar/options/component.cljs:78", "src/app/main/ui/workspace/context_menu.cljs:163" ], "translations" : { "en" : "Update master component" } @@ -3860,7 +3913,7 @@ } }, "workspace.updates.dismiss" : { - "used-in" : [ "src/app/main/data/workspace/libraries.cljs:541" ], + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:638" ], "translations" : { "en" : "Dismiss", "fr" : "", @@ -3869,7 +3922,7 @@ } }, "workspace.updates.there-are-updates" : { - "used-in" : [ "src/app/main/data/workspace/libraries.cljs:537" ], + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:634" ], "translations" : { "en" : "There are updates in shared libraries", "fr" : "", @@ -3878,7 +3931,7 @@ } }, "workspace.updates.update" : { - "used-in" : [ "src/app/main/data/workspace/libraries.cljs:539" ], + "used-in" : [ "src/app/main/data/workspace/libraries.cljs:636" ], "translations" : { "en" : "Update", "fr" : "", diff --git a/frontend/resources/polyfills/scrollIntoViewIfNeeded.js b/frontend/resources/polyfills/scrollIntoViewIfNeeded.js new file mode 100644 index 000000000..a341b3e40 --- /dev/null +++ b/frontend/resources/polyfills/scrollIntoViewIfNeeded.js @@ -0,0 +1,27 @@ +;(function() { + if (!Element.prototype.scrollIntoViewIfNeeded) { + Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) { + centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded; + + var parent = this.parentNode, + parentComputedStyle = window.getComputedStyle(parent, null), + parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')), + parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')), + overTop = this.offsetTop - parent.offsetTop < parent.scrollTop, + overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight), + overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft, + overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth), + alignWithTop = overTop && !overBottom; + + if ((overTop || overBottom) && centerIfNeeded) { + parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2; + } + if ((overLeft || overRight) && centerIfNeeded) { + parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2; + } + if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) { + this.scrollIntoView(alignWithTop); + } + }; + } +})() diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index b8860a4b4..a18f57a44 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -80,6 +80,6 @@ @import 'main/partials/user-settings'; @import 'main/partials/workspace'; @import 'main/partials/workspace-header'; -@import 'main/partials/workspace-comments'; +@import 'main/partials/comments'; @import 'main/partials/color-bullet'; @import "main/partials/handoff"; diff --git a/frontend/resources/styles/main/layouts/main-layout.scss b/frontend/resources/styles/main/layouts/main-layout.scss index 22a1cd54c..b7045cd39 100644 --- a/frontend/resources/styles/main/layouts/main-layout.scss +++ b/frontend/resources/styles/main/layouts/main-layout.scss @@ -23,7 +23,7 @@ .dashboard-sidebar { grid-row: 1 / span 2; grid-column: 1 / span 2; - overflow: hidden; + // overflow: hidden; } .dashboard-content { diff --git a/frontend/resources/styles/main/partials/workspace-comments.scss b/frontend/resources/styles/main/partials/comments.scss similarity index 67% rename from frontend/resources/styles/main/partials/workspace-comments.scss rename to frontend/resources/styles/main/partials/comments.scss index 1f425ae71..b15588d89 100644 --- a/frontend/resources/styles/main/partials/workspace-comments.scss +++ b/frontend/resources/styles/main/partials/comments.scss @@ -1,31 +1,4 @@ -.viewer-comments { - width: 100%; - height: 100%; - z-index: 1000; - position: absolute; - top: 0px; - left: 0px; -} - -.viewer-comments, .workspace-comments { - - .comments-layer { - - width: 100%; - height: 100%; - grid-column: 1/span 2; - grid-row: 1/span 2; - z-index: 1000; - pointer-events: none; - overflow: hidden; - - .threads { - position: absolute; - top: 0px; - left: 0px; - } - } - +.comments-section { .thread-bubble { position: absolute; display: flex; @@ -101,6 +74,8 @@ padding: $small; resize: none; width: 100%; + border-radius: 2px; + border: 1px solid $color-gray-10; } .buttons { @@ -142,7 +117,7 @@ .fullname { font-weight: 700; color: $color-gray-60; - font-size: $fs13; + font-size: $fs10; @include text-ellipsis; width: 150px; @@ -150,7 +125,7 @@ } .timeago { margin-top: -2px; - font-size: $fs11; + font-size: $fs10; color: $color-gray-30; } } @@ -163,8 +138,8 @@ img { border-radius: 50%; flex-shrink: 0; - height: 24px; - width: 24px; + height: 20px; + width: 20px; } } @@ -205,9 +180,8 @@ } .content { - margin: $medium 0; - // margin-left: 26px; - font-size: $fs13; + margin: 10px 0; + font-size: $fs10; color: $color-black; .text { margin-left: 26px; @@ -225,51 +199,51 @@ border: 1px solid #B1B2B5; } - } -.workspace-comments-sidebar { - pointer-events: auto; +.workspace-comment-threads-sidebar-header { + display: flex; + background-color: $color-black; + height: 34px; + align-items: center; + padding: 0px 9px; + color: $color-gray-10; + font-size: $fs12; + justify-content: space-between; - .sidebar-title { + .options { display: flex; - background-color: $color-black; - height: 34px; - align-items: center; - padding: 0px 9px; - color: $color-gray-10; - font-size: $fs12; - justify-content: space-between; + margin-right: 3px; + cursor: pointer; - .options { + .label { + padding-right: 8px; + } + + .icon { display: flex; - margin-right: 3px; - cursor: pointer; + align-items: center; + } - .label { - padding-right: 8px; - } - - .icon { - display: flex; - align-items: center; - } - - svg { - fill: $color-gray-10; - width: 10px; - height: 10px; - } + svg { + fill: $color-gray-10; + width: 10px; + height: 10px; } } - .sidebar-options-dropdown { + .dropdown { top: 80px; right: 7px; } +} - .threads { + +.comment-threads-section { + pointer-events: auto; + + .thread-groups { hr { border: 0; height: 1px; @@ -278,7 +252,7 @@ } } - .page-section { + .thread-group { display: flex; flex-direction: column; font-size: $fs12; @@ -292,6 +266,9 @@ } .label { + &.filename { + font-weight: 700; + } } svg { @@ -312,6 +289,7 @@ } .comment { + cursor: pointer; .author { margin-bottom: 10px; .name { @@ -351,3 +329,118 @@ } } } + + +.viewer-comments-container { + width: 100%; + height: 100%; + z-index: 1000; + position: absolute; + top: 0px; + left: 0px; +} + +.workspace-comments-container { + width: 100%; + height: 100%; + grid-column: 1/span 2; + grid-row: 1/span 2; + z-index: 1000; + pointer-events: none; + overflow: hidden; + + .threads { + position: absolute; + top: 0px; + left: 0px; + } +} + +.dashboard-comments-section { + width: 25px; + height: 25px; + display: flex; + align-items: center; + justify-content: center; + background-color: $color-dashboard; + border-radius: 3px; + position: relative; + + .button { + width: 25px; + height: 25px; + display: flex; + align-items: center; + justify-content: center; + background-color: $color-dashboard; + border-radius: 3px; + + svg { + width: 15px; + height: 15px; + } + + &.unread { + background-color: $color-warning; + } + + &.open { + background-color: $color-black; + svg { fill: $color-primary; } + } + } + + .dropdown { + width: 233px; + bottom: 35px; + left: 0px; + border-radius: 3px; + } + + .header { + display: flex; + height: 40px; + align-items: center; + padding: 0px 11px; + + h3 { + font-weight: 400; + color: $color-black; + font-size: $fs12; + line-height: $fs18; + flex-grow: 1; + } + + .close { + display: flex; + align-items: center; + } + + + svg { + width: 15px; + height: 15px; + transform: rotate(45deg); + } + } + + .thread-groups-placeholder { + padding: 16px; + } + + .thread-group { + .section-title { + color: $color-black; + } + } + + .comment { + .author .name .fullname { + color: $color-gray-40; + } + .content { + color: $color-black; + } + } +} + diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss index 04f1daf17..658411737 100644 --- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss +++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss @@ -363,26 +363,32 @@ padding: 10px 15px; position: relative; - span { - @include text-ellipsis; - color: $color-black; - margin: 10px 5px; - font-size: $fs14; - max-width: 160px; - } + .profile { + align-items: center; + cursor: pointer; + display: flex; + flex-grow: 1; - img { - border-radius: 50%; - flex-shrink: 0; - height: 25px; - width: 25px; - } + span { + @include text-ellipsis; + color: $color-black; + margin: 10px 5px; + font-size: $fs14; + max-width: 160px; + } - svg { - height: 10px; - margin-left: auto; - margin-right: $small; - width: 10px; + img { + border-radius: 50%; + flex-shrink: 0; + height: 25px; + width: 25px; + } + svg { + height: 10px; + margin-left: auto; + margin-right: $small; + width: 10px; + } } .dropdown { @@ -400,6 +406,8 @@ svg { fill: $color-gray-20; + margin-right: $small; + height: 12px; width: 12px; } diff --git a/frontend/resources/templates/index.mustache b/frontend/resources/templates/index.mustache index 99da48768..801633eb1 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -20,6 +20,7 @@ {{# manifest}} + {{/manifest}} diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index 79b5995a9..75a1faaea 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -40,11 +40,14 @@ (s/def ::count-unread-comments ::us/integer) (s/def ::created-at ::us/inst) (s/def ::file-id ::us/uuid) +(s/def ::file-name ::us/string) (s/def ::modified-at ::us/inst) (s/def ::owner-id ::us/uuid) (s/def ::page-id ::us/uuid) +(s/def ::page-name ::us/string) (s/def ::participants (s/every ::us/uuid :kind set?)) (s/def ::position ::us/point) +(s/def ::project-id ::us/uuid) (s/def ::seqn ::us/integer) (s/def ::thread-id ::us/uuid) @@ -52,15 +55,18 @@ (s/keys :req-un [::us/id ::page-id ::file-id + ::project-id + ::page-name + ::file-name ::seqn ::content ::participants - ::count-unread-comments - ::count-comments ::created-at ::modified-at ::owner-id - ::position])) + ::position] + :opt-un [::count-unread-comments + ::count-comments])) (s/def ::comment (s/keys :req-un [::us/id @@ -92,20 +98,18 @@ ptk/WatchEvent (watch [_ state stream] (->> (rp/mutation :create-comment-thread params) + (rx/mapcat #(rp/query :comment-thread {:file-id (:file-id %) :id (:id %)})) (rx/map #(partial created %))))))) (defn update-comment-thread-status [{:keys [id] :as thread}] (us/assert ::comment-thread thread) (ptk/reify ::update-comment-thread-status - ptk/UpdateEvent - (update [_ state] - (d/update-in-when state [:comment-threads id] assoc :count-unread-comments 0)) - ptk/WatchEvent (watch [_ state stream] - (->> (rp/mutation :update-comment-thread-status {:id id}) - (rx/ignore))))) + (let [done #(d/update-in-when % [:comment-threads id] assoc :count-unread-comments 0)] + (->> (rp/mutation :update-comment-thread-status {:id id}) + (rx/map (constantly done))))))) (defn update-comment-thread @@ -211,6 +215,18 @@ (->> (rp/query :comments {:thread-id thread-id}) (rx/map #(partial fetched %))))))) +(defn retrieve-unread-comment-threads + "A event used mainly in dashboard for retrieve all unread threads of a team." + [team-id] + (us/assert ::us/uuid team-id) + (ptk/reify ::retrieve-unread-comment-threads + ptk/WatchEvent + (watch [_ state stream] + (let [fetched #(assoc %2 :comment-threads (d/index-by :id %1))] + (->> (rp/query :unread-comment-threads {:team-id team-id}) + (rx/map #(partial fetched %))))))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Local State ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -221,6 +237,7 @@ (ptk/reify ::open-thread ptk/UpdateEvent (update [_ state] + (prn "open-thread" id) (-> state (update :comments-local assoc :open id) (update :workspace-drawing dissoc :comment))))) @@ -230,6 +247,7 @@ (ptk/reify ::close-thread ptk/UpdateEvent (update [_ state] + (prn "close-thread") (-> state (update :comments-local dissoc :open :draft) (update :workspace-drawing dissoc :comment))))) @@ -282,11 +300,31 @@ (if (= (:page-id current) (:page-id thread)) (cons (update current :items conj thread) (rest result)) - (cons {:page-id (:page-id thread) :items [thread]} + (cons {:page-id (:page-id thread) + :page-name (:page-name thread) + :items [thread]} result))))] (reverse (reduce group-by-page nil threads)))) + +(defn group-threads-by-file-and-page + [threads] + (letfn [(group-by-file-and-page [result thread] + (let [current (first result)] + (if (and (= (:page-id current) (:page-id thread)) + (= (:file-id current) (:file-id thread))) + (cons (update current :items conj thread) + (rest result)) + (cons {:page-id (:page-id thread) + :page-name (:page-name thread) + :file-id (:file-id thread) + :file-name (:file-name thread) + :items [thread]} + result))))] + (reverse + (reduce group-by-file-and-page nil threads)))) + (defn apply-filters [cstate profile threads] (let [{:keys [show mode open]} cstate] diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 52ae6f79a..862a90ba7 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -64,13 +64,6 @@ ;; --- Fetch Team -(defn assoc-team-avatar - [{:keys [photo name] :as team}] - (us/assert ::team team) - (cond-> team - (or (nil? photo) (empty? photo)) - (assoc :photo (avatars/generate {:name name})))) - (defn fetch-team [{:keys [id] :as params}] (letfn [(fetched [team state] @@ -80,20 +73,36 @@ (watch [_ state stream] (let [profile (:profile state)] (->> (rp/query :team params) - (rx/map assoc-team-avatar) + (rx/map #(avatars/assoc-avatar % :name)) (rx/map #(partial fetched %)))))))) (defn fetch-team-members [{:keys [id] :as params}] (us/assert ::us/uuid id) (letfn [(fetched [members state] - (assoc-in state [:team-members id] (d/index-by :id members)))] + (->> (map #(avatars/assoc-avatar % :name) members) + (d/index-by :id) + (assoc-in state [:team-members id])))] (ptk/reify ::fetch-team-members ptk/WatchEvent (watch [_ state stream] (->> (rp/query :team-members {:team-id id}) (rx/map #(partial fetched %))))))) + +(defn fetch-team-users + [{:keys [id] :as params}] + (us/assert ::us/uuid id) + (letfn [(fetched [users state] + (->> (map #(avatars/assoc-avatar % :fullname) users) + (d/index-by :id) + (assoc-in state [:team-users id])))] + (ptk/reify ::fetch-team-users + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :team-users {:team-id id}) + (rx/map #(partial fetched %))))))) + ;; --- Fetch Projects (defn fetch-projects @@ -115,7 +124,8 @@ (watch [_ state stream] (let [profile (:profile state)] (->> (rx/merge (ptk/watch (fetch-team params) state stream) - (ptk/watch (fetch-projects {:team-id id}) state stream)) + (ptk/watch (fetch-projects {:team-id id}) state stream) + (ptk/watch (fetch-team-users params) state stream)) (rx/catch (fn [{:keys [type code] :as error}] (cond (and (= :not-found type) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 2e990f7d0..56b624ae9 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -55,8 +55,8 @@ (update [_ state] (assoc state :profile (cond-> data - (nil? (:photo-uri data)) - (assoc :photo-uri (avatars/generate {:name fullname})) + (empty? (:photo data)) + (assoc :photo (avatars/generate {:name fullname})) (nil? (:lang data)) (assoc :lang cfg/default-language) diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs index ac1d16c05..ccbbd8c30 100644 --- a/frontend/src/app/main/data/workspace/comments.cljs +++ b/frontend/src/app/main/data/workspace/comments.cljs @@ -14,10 +14,12 @@ [app.common.math :as mth] [app.common.spec :as us] [app.main.constants :as c] + [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] [app.main.data.comments :as dcm] [app.main.store :as st] [app.main.streams :as ms] + [app.util.router :as rt] [beicon.core :as rx] [cljs.spec.alpha :as s] [potok.core :as ptk])) @@ -82,12 +84,31 @@ (update [_ state] (update state :workspace-local (fn [{:keys [vbox vport zoom] :as local}] + (prn "center-to-comment-thread" vbox) (let [pw (/ 50 zoom) ph (/ 200 zoom) nw (mth/round (- (/ (:width vbox) 2) pw)) nh (mth/round (- (/ (:height vbox) 2) ph)) nx (- (:x position) nw) ny (- (:y position) nh)] - (update local :vbox assoc :x nx :y ny))))))) + (update local :vbox assoc :x nx :y ny))))))) + +(defn navigate + [{:keys [project-id file-id page-id] :as thread}] + (us/assert ::dcm/comment-thread thread) + (ptk/reify ::navigate + ptk/WatchEvent + (watch [_ state stream] + (let [pparams {:project-id (:project-id thread) + :file-id (:file-id thread)} + qparams {:page-id (:page-id thread)}] + (rx/merge + (rx/of (rt/nav :workspace pparams qparams) + (dw/select-for-drawing :comments)) + (->> stream + (rx/filter (ptk/type? ::dw/initialize-viewport)) + (rx/take 1) + (rx/mapcat #(rx/of (center-to-comment-thread thread) + (dcm/open-thread thread))))))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 37d5b7200..58fa20a6e 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -208,6 +208,9 @@ (def viewer-local (l/derived :viewer-local st/state)) +(def comment-threads + (l/derived :comment-threads st/state)) + (def comments-local (l/derived :comments-local st/state)) diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index 3cf96c363..d681003d2 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -148,7 +148,7 @@ :left (str (+ pos-x 14) "px")} :on-click dom/stop-propagation} [:div.reply-form - [:& resizing-textarea {:placeholder "Write new comment" + [:& resizing-textarea {:placeholder (tr "labels.write-new-comment") :value (or content "") :on-esc on-esc :on-change on-change}] @@ -267,10 +267,10 @@ [:& dropdown {:show @options :on-close on-hide-options} [:ul.dropdown.comment-options-dropdown - [:li {:on-click on-edit-clicked} "Edit"] + [:li {:on-click on-edit-clicked} (tr "labels.edit")] (if thread - [:li {:on-click on-delete-thread} "Delete thread"] - [:li {:on-click on-delete-comment} "Delete comment"])]]])) + [:li {:on-click on-delete-thread} (tr "labels.delete-comment-thread")] + [:li {:on-click on-delete-comment} (tr "labels.delete-comment")])]]])) (defn comments-ref [{:keys [id] :as thread}] @@ -289,12 +289,13 @@ (sort-by :created-at)) comment (first comments)] - (mf/use-effect - (st/emitf (dcm/update-comment-thread-status thread))) + (mf/use-layout-effect + (mf/deps thread) + (st/emitf (dcm/retrieve-comments (:id thread)))) (mf/use-effect (mf/deps thread) - (st/emitf (dcm/retrieve-comments (:id thread)))) + (st/emitf (dcm/update-comment-thread-status thread))) (mf/use-layout-effect (mf/deps thread comments-map) @@ -338,3 +339,62 @@ :unread (pos? (:count-unread-comments thread))) :on-click on-click*} [:span (:seqn thread)]])) + +(mf/defc comment-thread + [{:keys [item users on-click] :as props}] + (let [profile (get users (:owner-id item)) + + on-click* + (mf/use-callback + (mf/deps item) + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (when (fn? on-click) + (on-click item))))] + + [:div.comment {:on-click on-click*} + [:div.author + [:div.thread-bubble + {:class (dom/classnames + :resolved (:is-resolved item) + :unread (pos? (:count-unread-comments item)))} + (:seqn item)] + [:div.avatar + [:img {:src (cfg/resolve-media-path (:photo profile))}]] + [:div.name + [:div.fullname (:fullname profile) ", "] + [:div.timeago (dt/timeago (:modified-at item))]]] + [:div.content + [:span.text (:content item)]] + [:div.content.replies + (let [unread (:count-unread-comments item ::none) + total (:count-comments item 1)] + [:* + (when (> total 1) + (if (= total 2) + [:span.total-replies "1 reply"] + [:span.total-replies (str (dec total) " replies")])) + + (when (and (> total 1) (> unread 0)) + (if (= unread 1) + [:span.new-replies "1 new reply"] + [:span.new-replies (str unread " new replies")]))])]])) + +(mf/defc comment-thread-group + [{:keys [group users on-thread-click]}] + [:div.thread-group + (if (:file-name group) + [:div.section-title + [:span.label.filename (:file-name group) ", "] + [:span.label (:page-name group)]] + [:div.section-title + [:span.icon i/file-html] + [:span.label (:page-name group)]]) + [:div.threads + (for [item (:items group)] + [:& comment-thread + {:item item + :on-click on-thread-click + :users users + :key (:id item)}])]]) diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs new file mode 100644 index 000000000..3febd79e1 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/comments.cljs @@ -0,0 +1,104 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.dashboard.comments + (:require + [okulary.core :as l] + [app.common.data :as d] + [app.common.spec :as us] + [app.config :as cfg] + [app.main.data.auth :as da] + [app.main.data.dashboard :as dd] + [app.main.data.workspace :as dw] + [app.main.data.workspace.comments :as dwcm] + [app.main.data.comments :as dcm] + [app.main.refs :as refs] + [app.main.repo :as rp] + [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.comments :as cmt] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [t tr]] + [app.util.object :as obj] + [app.util.router :as rt] + [app.util.time :as dt] + [app.util.timers :as tm] + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [rumext.alpha :as mf])) + + +(defn team-members-ref + [{:keys [id] :as team}] + (l/derived (l/in [:team-users id]) st/state)) + +(mf/defc comments-section + [{:keys [profile team]}] + + (mf/use-effect + (mf/deps team) + (st/emitf (dcm/retrieve-unread-comment-threads (:id team)))) + + (let [show-dropdown? (mf/use-state false) + show-dropdown (mf/use-fn #(reset! show-dropdown? true)) + hide-dropdown (mf/use-fn #(reset! show-dropdown? false)) + threads-map (mf/deref refs/comment-threads) + + users-ref (mf/use-memo (mf/deps team) #(team-members-ref team)) + users (mf/deref users-ref) + + tgroups (->> (vals threads-map) + (sort-by :modified-at) + (reverse) + (dcm/apply-filters {} profile) + (dcm/group-threads-by-file-and-page)) + + + on-navigate + (mf/use-callback + (fn [thread] + (st/emit! (dwcm/navigate thread))))] + + [:div.dashboard-comments-section + [:div.button + {:on-click show-dropdown + :class (dom/classnames :open @show-dropdown? + :unread (boolean (seq tgroups)))} + i/chat] + + [:& dropdown {:show @show-dropdown? :on-close hide-dropdown} + [:div.dropdown.comments-section.comment-threads-section. + [:div.header + [:h3 (tr "labels.comments")] + [:span.close {:on-click hide-dropdown} i/close]] + + [:hr] + + (if (seq tgroups) + [:div.thread-groups + [:& cmt/comment-thread-group + {:group (first tgroups) + :on-thread-click on-navigate + :show-file-name true + :users users}] + (for [tgroup (rest tgroups)] + [:* + [:hr] + + [:& cmt/comment-thread-group + {:group tgroup + :on-thread-click on-navigate + :show-file-name true + :users users + :key (:page-id tgroup)}]])] + + [:div.thread-groups-placeholder + (tr "labels.no-comments-available")])]]])) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index e05f064ca..73554a514 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -15,21 +15,24 @@ [app.main.data.auth :as da] [app.main.data.dashboard :as dd] [app.main.data.messages :as dm] + [app.main.data.modal :as modal] + [app.main.data.comments :as dcm] [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.components.forms :as fm] + [app.main.ui.dashboard.comments :refer [comments-section]] + [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.team-form] [app.main.ui.icons :as i] [app.main.ui.keyboard :as kbd] - [app.main.data.modal :as modal] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [t tr]] [app.util.object :as obj] [app.util.router :as rt] [app.util.time :as dt] + [app.util.avatars :as avatars] [beicon.core :as rx] [cljs.spec.alpha :as s] [cuerdas.core :as str] @@ -133,7 +136,8 @@ (mf/deps (:id team)) (fn [] (->> (rp/query! :teams) - (rx/map #(mapv dd/assoc-team-avatar %)) + (rx/map (fn [teams] + (mapv #(avatars/assoc-avatar % :name) teams))) (rx/subs #(reset! teams %))))) [:ul.dropdown.teams-dropdown @@ -421,12 +425,9 @@ (mf/defc profile-section - [{:keys [profile locale] :as props}] + [{:keys [profile locale team] :as props}] (let [show (mf/use-state false) - photo (:photo-uri profile "") - photo (if (str/empty? photo) - "/images/avatar.jpg" - photo) + photo (cfg/resolve-media-path (:photo profile)) on-click (mf/use-callback @@ -436,10 +437,10 @@ (st/emit! (rt/nav section)) (st/emit! section))))] - [:div.profile-section {:on-click #(reset! show true)} - [:img {:src photo}] - [:span (:fullname profile)] - i/arrow-down + [:div.profile-section + [:div.profile {:on-click #(reset! show true)} + [:img {:src photo}] + [:span (:fullname profile)] [:& dropdown {:on-close #(reset! show false) :show @show} @@ -452,17 +453,25 @@ [:span.text (t locale "labels.password")]] [:li {:on-click (partial on-click (da/logout))} [:span.icon i/exit] - [:span.text (t locale "labels.logout")]]]]])) + [:span.text (t locale "labels.logout")]]]]] + + (when (and team profile) + [:& comments-section {:profile profile + :team team}])])) (mf/defc sidebar {::mf/wrap-props false ::mf/wrap [mf/memo]} [props] (let [locale (mf/deref i18n/locale) + team (obj/get props "team") profile (obj/get props "profile") props (-> (obj/clone props) (obj/set! "locale" locale))] [:div.dashboard-sidebar [:div.sidebar-inside [:> sidebar-content props] - [:& profile-section {:profile profile :locale locale}]]])) + [:& profile-section + {:profile profile + :team team + :locale locale}]]])) diff --git a/frontend/src/app/main/ui/viewer.cljs b/frontend/src/app/main/ui/viewer.cljs index 6633f236b..73017aaeb 100644 --- a/frontend/src/app/main/ui/viewer.cljs +++ b/frontend/src/app/main/ui/viewer.cljs @@ -106,8 +106,8 @@ (dcm/close-thread))))) ] - [:div.viewer-comments {:on-click on-click} - [:div.comments-layer + [:div.comments-section {:on-click on-click} + [:div.viewer-comments-container [:div.threads (for [item threads] [:& cmt/thread-bubble {:thread item diff --git a/frontend/src/app/main/ui/workspace/comments.cljs b/frontend/src/app/main/ui/workspace/comments.cljs index f2f113e88..25a69c9f8 100644 --- a/frontend/src/app/main/ui/workspace/comments.cljs +++ b/frontend/src/app/main/ui/workspace/comments.cljs @@ -60,7 +60,6 @@ #_(dcm/close-thread)))) ] - (mf/use-effect (mf/deps file-id) (fn [] @@ -68,8 +67,8 @@ (fn [] (st/emit! ::dwcm/finalize)))) - [:div.workspace-comments - [:div.comments-layer + [:div.comments-section + [:div.workspace-comments-container {:style {:width (str (:width vport) "px") :height (str (:height vport) "px")}} [:div.threads {:style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}} @@ -96,66 +95,6 @@ ;; Sidebar ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(mf/defc sidebar-group-item - [{:keys [item] :as props}] - (let [profile (get @refs/workspace-users (:owner-id item)) - page-id (mf/use-ctx ctx/current-page-id) - file-id (mf/use-ctx ctx/current-file-id) - - on-click - (mf/use-callback - (mf/deps item page-id) - (fn [] - (when (not= page-id (:page-id item)) - (st/emit! (dw/go-to-page (:page-id item)))) - (tm/schedule - (st/emitf (dwcm/center-to-comment-thread item) - (dcm/open-thread item)))))] - - [:div.comment {:on-click on-click} - [:div.author - [:div.thread-bubble - {:class (dom/classnames - :resolved (:is-resolved item) - :unread (pos? (:count-unread-comments item)))} - (:seqn item)] - [:div.avatar - [:img {:src (cfg/resolve-media-path (:photo profile))}]] - [:div.name - [:div.fullname (:fullname profile) ", "] - [:div.timeago (dt/timeago (:modified-at item))]]] - [:div.content - [:span.text (:content item)]] - [:div.content.replies - (let [unread (:count-unread-comments item ::none) - total (:count-comments item 1)] - [:* - (when (> total 1) - (if (= total 2) - [:span.total-replies "1 reply"] - [:span.total-replies (str (dec total) " replies")])) - - (when (and (> total 1) (> unread 0)) - (if (= unread 1) - [:span.new-replies "1 new reply"] - [:span.new-replies (str unread " new replies")]))])]])) - -(defn page-name-ref - [id] - (l/derived (l/in [:workspace-data :pages-index id :name]) st/state)) - -(mf/defc sidebar-item - [{:keys [group]}] - (let [page-name-ref (mf/use-memo (mf/deps (:page-id group)) #(page-name-ref (:page-id group))) - page-name (mf/deref page-name-ref)] - [:div.page-section - [:div.section-title - [:span.icon i/file-html] - [:span.label page-name]] - [:div.comments-container - (for [item (:items group)] - [:& sidebar-group-item {:item item :key (:id item)}])]])) - (mf/defc sidebar-options [{:keys [local] :as props}] (let [{cmode :mode cshow :show} (mf/deref refs/comments-local) @@ -171,7 +110,7 @@ (fn [mode] (st/emit! (dcm/update-filters {:show mode}))))] - [:ul.dropdown.with-check.sidebar-options-dropdown + [:ul.dropdown.with-check [:li {:class (dom/classnames :selected (or (= :all cmode) (nil? cmode))) :on-click #(update-mode :all)} [:span.icon i/tick] @@ -193,6 +132,7 @@ [] (let [threads-map (mf/deref threads-ref) profile (mf/deref refs/profile) + users (mf/deref refs/workspace-users) local (mf/deref refs/comments-local) options? (mf/use-state false) @@ -200,28 +140,45 @@ (sort-by :modified-at) (reverse) (dcm/apply-filters local profile) - (dcm/group-threads-by-page))] + (dcm/group-threads-by-page)) - [:div.workspace-comments.workspace-comments-sidebar - [:div.sidebar-title + page-id (mf/use-ctx ctx/current-page-id) + + on-thread-click + (mf/use-callback + (fn [thread] + (when (not= page-id (:page-id thread)) + (st/emit! (dw/go-to-page (:page-id thread)))) + (tm/schedule + (st/emitf (dwcm/center-to-comment-thread thread) + (dcm/open-thread thread)))))] + + [:div.comments-section.comment-threads-section + [:div.workspace-comment-threads-sidebar-header [:div.label "Comments"] [:div.options {:on-click #(reset! options? true)} - [:div.label (case (:filter local) + [:div.label (case (:mode local) (nil :all) "All" :yours "Only yours")] - [:div.icon i/arrow-down]]] + [:div.icon i/arrow-down]] - [:& dropdown {:show @options? - :on-close #(reset! options? false)} - [:& sidebar-options {:local local}]] + [:& dropdown {:show @options? + :on-close #(reset! options? false)} + [:& sidebar-options {:local local}]]] (when (seq tgroups) - [:div.threads - [:& sidebar-item {:group (first tgroups)}] + [:div.thread-groups + [:& cmt/comment-thread-group + {:group (first tgroups) + :on-thread-click on-thread-click + :users users}] (for [tgroup (rest tgroups)] [:* [:hr] - [:& sidebar-item {:group tgroup - :key (:page-id tgroup)}]])])])) + [:& cmt/comment-thread-group + {:group tgroup + :on-thread-click on-thread-click + :users users + :key (:page-id tgroup)}]])])])) diff --git a/frontend/src/app/util/avatars.cljs b/frontend/src/app/util/avatars.cljs index 64a79e1ee..04a9bd38c 100644 --- a/frontend/src/app/util/avatars.cljs +++ b/frontend/src/app/util/avatars.cljs @@ -36,8 +36,13 @@ (.toDataURL canvas))) -(defn assoc-profile-avatar - [{:keys [photo fullname] :as profile}] - (cond-> profile +(defn assoc-avatar + [{:keys [photo] :as object} key] + (cond-> object (or (nil? photo) (empty? photo)) - (assoc :photo (generate {:name fullname})))) + (assoc :photo (generate {:name (get object key)})))) + +(defn assoc-profile-avatar + [object] + (assoc-avatar object :fullname)) + diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 9515c97bf..0225cfb40 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -933,6 +933,13 @@ concat-stream@^1.6.0, concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" +concat-with-sourcemaps@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e" + integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg== + dependencies: + source-map "^0.6.1" + config-chain@^1.1.12: version "1.1.12" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" @@ -2033,6 +2040,15 @@ gulp-cli@^2.2.0: v8flags "^3.2.0" yargs "^7.1.0" +gulp-concat@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/gulp-concat/-/gulp-concat-2.6.1.tgz#633d16c95d88504628ad02665663cee5a4793353" + integrity sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M= + dependencies: + concat-with-sourcemaps "^1.0.0" + through2 "^2.0.0" + vinyl "^2.0.0" + gulp-gzip@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/gulp-gzip/-/gulp-gzip-1.4.2.tgz#0422a94014248655b5b1a9eea1c2abee1d4f4337"