diff --git a/config/jest/client.config.js b/config/jest/client.config.js index d5ba3c585..975397a29 100644 --- a/config/jest/client.config.js +++ b/config/jest/client.config.js @@ -10,6 +10,8 @@ module.exports = { "/src/core/build/polyfills.js", "/src/core/client/test/setup.ts", ], + setupTestFrameworkScriptFile: + "/src/core/client/test/setupTestFramework.ts", testMatch: ["**/*.spec.{js,jsx,mjs,ts,tsx}"], testEnvironment: "node", testURL: "http://localhost", diff --git a/package-lock.json b/package-lock.json index 96e667219..4f4636e4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1086,16 +1086,6 @@ "@babel/helper-plugin-utils": "7.0.0-beta.49" } }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.0.0-beta.49.tgz", - "integrity": "sha1-CMxbZM9qWUKoe92bSkgY1MuhLfM=", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "7.0.0-beta.49", - "@babel/helper-regex": "7.0.0-beta.49" - } - }, "@babel/plugin-transform-template-literals": { "version": "7.0.0-beta.49", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.0.0-beta.49.tgz", @@ -1211,6 +1201,16 @@ "@babel/helper-plugin-utils": "7.0.0-beta.49", "@babel/helper-simple-access": "7.0.0-beta.49" } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.0.0-beta.49", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.0.0-beta.49.tgz", + "integrity": "sha1-CMxbZM9qWUKoe92bSkgY1MuhLfM=", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "7.0.0-beta.49", + "@babel/helper-regex": "7.0.0-beta.49" + } } } }, @@ -2162,9 +2162,13 @@ } }, "@types/recompose": { - "version": "github:coralproject/patched#3ca0deec868322739fc0d750398fd97735d27aac", - "from": "github:coralproject/patched#types/recompose", - "dev": true + "version": "0.26.5", + "resolved": "https://registry.npmjs.org/@types/recompose/-/recompose-0.26.5.tgz", + "integrity": "sha512-Il5stz/Z3pVIMl48pyggl6nnhRLQ8N8YN8hi0Anm0M5UjVh2uMSY0ah2vzwZZKxnca4NzyJArloSjsJ9fL2vWw==", + "dev": true, + "requires": { + "@types/react": "*" + } }, "@types/relateurl": { "version": "0.2.28", @@ -14225,6 +14229,12 @@ "integrity": "sha1-rRxg8p6HGdR8JuETgJi20YsmETQ=", "dev": true }, + "jest-mock-console": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/jest-mock-console/-/jest-mock-console-0.4.0.tgz", + "integrity": "sha512-WElCbNvfqQlD7cpfHfTn1ytZ+RjKg1Ftrvr5wEjdWP7a9esXmaiZuEAPeYUSK5fd0Cra+dR1oF8HAjjKKxDQdg==", + "dev": true + }, "jest-regex-util": { "version": "23.3.0", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz", @@ -18041,6 +18051,11 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "permit": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/permit/-/permit-0.2.4.tgz", + "integrity": "sha512-Mp2XTEMD3mPsZIWq3bp0claE4IxXKa4C6nhSDPZgGri8Q4CLjEjAQrP/xGKq2548a2KFENmA1V7W0Lob8kTuzw==" + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", diff --git a/package.json b/package.json index 30db5c9ef..eb3e0c0e3 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "passport-oauth2": "^1.4.0", "passport-strategy": "^1.0.0", "performance-now": "^2.1.0", + "permit": "^0.2.4", "subscriptions-transport-ws": "^0.9.12", "tlds": "^1.203.1", "uuid": "^3.3.2" @@ -133,7 +134,7 @@ "@types/react-relay": "^1.3.9", "@types/react-responsive": "^3.0.1", "@types/react-test-renderer": "^16.0.1", - "@types/recompose": "github:coralproject/patched#types/recompose", + "@types/recompose": "^0.26.5", "@types/relay-runtime": "^1.3.6", "@types/sane": "^2.0.0", "@types/sinon": "^5.0.1", @@ -185,6 +186,7 @@ "jest": "^23.4.1", "jest-junit": "^5.1.0", "jest-localstorage-mock": "^2.2.0", + "jest-mock-console": "^0.4.0", "jsdom": "^11.11.0", "loader-utils": "^1.1.0", "material-design-icons": "^3.0.1", diff --git a/src/core/build/createWebpackConfig.ts b/src/core/build/createWebpackConfig.ts index 6085d8899..a19c33c47 100644 --- a/src/core/build/createWebpackConfig.ts +++ b/src/core/build/createWebpackConfig.ts @@ -288,6 +288,25 @@ export default function createWebpackConfig({ }, ], }, + // Makes sure node_modules are transpiled the way we need them to be. + { + test: /\.js$/, + include: /node_modules\//, + use: [ + { + loader: require.resolve("babel-loader"), + options: { + presets: [ + [ + "@babel/env", + { targets: "last 2 versions, ie 11", modules: false }, + ], + ], + cacheDirectory: true, + }, + }, + ], + }, // "postcss" loader applies autoprefixer to our CSS. // "css" loader resolves paths in CSS and adds assets as dependencies. // "style" loader turns CSS into JS modules that inject + + + +

+ Default | Article With Button +

+

Talk 5.0 – Article

+

Dismember a mouse and then regurgitate parts of it on the family room floor. Dont wait for the storm to pass, + dance in the rain stand in front of the computer screen, so stares at human while pushing stuff off a table chew + the plant meow hiss at vacuum cleaner. Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not + sorry chew the plant. Litter kitter kitty litty little kitten big roar roar feed me rub whiskers on bare skin act + innocent sleep on keyboard, so give me attention or face the wrath of my claws for demand to be let outside at + once, and expect owner to wait for me as i think about it spread kitty litter all over house so nya nya nyan. Catty + ipsum massacre a bird in the living room and then look like the cutest and most innocent animal on the planet you + have cat to be kitten me right meow. Hiss and stare at nothing then run suddenly away refuse to come home when + humans are going to bed; stay out all night then yowl like i am dying at 4am and lick plastic bags. Chase dog then + run away purrr purr littel cat, little cat purr purr and step on your keyboard while you're gaming and then turn in + a circle . Twitch tail in permanent irritation put butt in owner's face and the dog smells bad yet attempt to leap + between furniture but woefully miscalibrate and bellyflop onto the floor; what's your problem? i meant to do that + now i shall wash myself intently. Sniff all the things groom forever, stretch tongue and leave it slightly out, + blep, but bring your owner a dead bird decide to want nothing to do with my owner today for lay on arms while + you're using the keyboard meow meow, i tell my human or scratch. Sleep on my human's head then cats take over the + world bleghbleghvomit my furball really tie the room together sleep more napping, more napping all the napping is + exhausting. When in doubt, wash drink water out of the faucet, cats are fats i like to pets them they like to meow + back and cat dog hate mouse eat string barf pillow no baths hate everything yet swat at dog kitty kitty but you + call this cat food. Cough furball into food bowl then scratch owner for a new one flex claws on the human's belly + and purr like a lawnmower for has closed eyes but still sees you groom yourself 4 hours - checked, have your beauty + sleep 18 hours - checked, be fabulous for the rest of the day - checked. Freak human out make funny noise mow mow + mow mow mow mow success now attack human flex claws on the human's belly and purr like a lawnmower or meowwww. + Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not sorry paw at your fat belly so yowling + nonstop the whole night small kitty warm kitty little balls of fur or eat owner's food reward the chosen human with + a slow blink. Gate keepers of hell plan steps for world domination for more napping, more napping all the napping + is exhausting give me some of your food give me some of your food give me some of your food meh, i don't want it so + flop over. Make meme, make cute face ears back wide eyed so sit and stare. Dead stare with ears cocked furrier and + even more furrier hairball. Stand in front of the computer screen demand to have some of whatever the human is + cooking, then sniff the offering and walk away for catasstrophe, kitty scratches couch bad kitty. Wack the mini + furry mouse intrigued by the shower, and pooping rainbow while flying in a toasted bread costume in space. + Mesmerizing birds love me! shake treat bag, yet lies down where is my slave? I'm getting hungry so lick face hiss + at owner, pee a lot, and meow repeatedly scratch at fence purrrrrr eat muffins and poutine until owner comes back. + You have cat to be kitten me right meow sniff other cat's butt and hang jaw half open thereafter but run outside as + soon as door open so munch on tasty moths or munch on tasty moths, for paw at beetle and eat it before it gets + away. Sit on human. Gnaw the corn cob massacre a bird in the living room and then look like the cutest and most + innocent animal on the planet for sit on the laptop. Meow scratch leg; meow for can opener to feed me cat fur is + the new black but hide when guests come over, and Gate keepers of hell. Refuse to come home when humans are going + to bed; stay out all night then yowl like i am dying at 4am cat slap dog in face or eat a rug and furry furry hairs + everywhere oh no human coming lie on counter don't get off counter for i like fish sit on human they not getting up + ever but meow meow but cuddle no cuddle cuddle love scratch scratch.

+

I show my fluffy belly but it's a trap! if you pet it i will tear up your hand refuse to drink water except out of + someone's glass mice, so cough hairball, eat toilet paper or curl into a furry donut lick sellotape but wack the + mini furry mouse. When owners are asleep, cry for no apparent reason. Chase imaginary bugs. Stinky cat reward the + chosen human with a slow blink, or chase dog then run away. Chew on cable scratch the furniture for you are a + captive audience while sitting on the toilet, pet me for i like cats because they are fat and fluffy and spend all + night ensuring people don't sleep sleep all day. Scoot butt on the rug need to check on human, have not seen in an + hour might be dead oh look, human is alive, hiss at human, feed me, leave fur on owners clothes, so instantly break + out into full speed gallop across the house for no reason play riveting piece on synthesizer keyboard and scoot + butt on the rug yet meow meow. Attack dog, run away and pretend to be victim annoy the old grumpy cat, start a + fight and then retreat to wash when i lose or meow go back to sleep owner brings food and water tries to pet on + head, so scratch get sprayed by water because bad cat. Meowwww pelt around the house and up and down stairs chasing + phantoms drink water out of the faucet meow meow, i tell my human. Destroy couch.

+

Ask to go outside and ask to come inside and ask to go outside and ask to come inside the dog smells bad. Lick + butt and make a weird face. Toilet paper attack claws fluff everywhere meow miao french ciao litterbox. Shake treat + bag immediately regret falling into bathtub or white cat sleeps on a black shirt so what a cat-ass-trophy! eat + owner's food spit up on light gray carpet instead of adjacent linoleum. Warm up laptop with butt lick butt fart + rainbows until owner yells pee in litter box hiss at cats scratch the box so loved it, hated it, loved it, hated it + but need to check on human, have not seen in an hour might be dead oh look, human is alive, hiss at human, feed me. +

+
+ + + + diff --git a/src/core/client/embed/articleButton.html b/src/core/client/embed/articleButton.html new file mode 100644 index 000000000..bf0c0cf38 --- /dev/null +++ b/src/core/client/embed/articleButton.html @@ -0,0 +1,103 @@ + + + + + Talk 5.0 – Embed Stream + + + + + + + +

+ Default | Article +

+

Talk 5.0 – Article with Button

+

Dismember a mouse and then regurgitate parts of it on the family room floor. Dont wait for the storm to pass, + dance in the rain stand in front of the computer screen, so stares at human while pushing stuff off a table chew + the plant meow hiss at vacuum cleaner. Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not + sorry chew the plant. Litter kitter kitty litty little kitten big roar roar feed me rub whiskers on bare skin act + innocent sleep on keyboard, so give me attention or face the wrath of my claws for demand to be let outside at + once, and expect owner to wait for me as i think about it spread kitty litter all over house so nya nya nyan. Catty + ipsum massacre a bird in the living room and then look like the cutest and most innocent animal on the planet you + have cat to be kitten me right meow. Hiss and stare at nothing then run suddenly away refuse to come home when + humans are going to bed; stay out all night then yowl like i am dying at 4am and lick plastic bags. Chase dog then + run away purrr purr littel cat, little cat purr purr and step on your keyboard while you're gaming and then turn in + a circle . Twitch tail in permanent irritation put butt in owner's face and the dog smells bad yet attempt to leap + between furniture but woefully miscalibrate and bellyflop onto the floor; what's your problem? i meant to do that + now i shall wash myself intently. Sniff all the things groom forever, stretch tongue and leave it slightly out, + blep, but bring your owner a dead bird decide to want nothing to do with my owner today for lay on arms while + you're using the keyboard meow meow, i tell my human or scratch. Sleep on my human's head then cats take over the + world bleghbleghvomit my furball really tie the room together sleep more napping, more napping all the napping is + exhausting. When in doubt, wash drink water out of the faucet, cats are fats i like to pets them they like to meow + back and cat dog hate mouse eat string barf pillow no baths hate everything yet swat at dog kitty kitty but you + call this cat food. Cough furball into food bowl then scratch owner for a new one flex claws on the human's belly + and purr like a lawnmower for has closed eyes but still sees you groom yourself 4 hours - checked, have your beauty + sleep 18 hours - checked, be fabulous for the rest of the day - checked. Freak human out make funny noise mow mow + mow mow mow mow success now attack human flex claws on the human's belly and purr like a lawnmower or meowwww. + Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not sorry paw at your fat belly so yowling + nonstop the whole night small kitty warm kitty little balls of fur or eat owner's food reward the chosen human with + a slow blink. Gate keepers of hell plan steps for world domination for more napping, more napping all the napping + is exhausting give me some of your food give me some of your food give me some of your food meh, i don't want it so + flop over. Make meme, make cute face ears back wide eyed so sit and stare. Dead stare with ears cocked furrier and + even more furrier hairball. Stand in front of the computer screen demand to have some of whatever the human is + cooking, then sniff the offering and walk away for catasstrophe, kitty scratches couch bad kitty. Wack the mini + furry mouse intrigued by the shower, and pooping rainbow while flying in a toasted bread costume in space. + Mesmerizing birds love me! shake treat bag, yet lies down where is my slave? I'm getting hungry so lick face hiss + at owner, pee a lot, and meow repeatedly scratch at fence purrrrrr eat muffins and poutine until owner comes back. + You have cat to be kitten me right meow sniff other cat's butt and hang jaw half open thereafter but run outside as + soon as door open so munch on tasty moths or munch on tasty moths, for paw at beetle and eat it before it gets + away. Sit on human. Gnaw the corn cob massacre a bird in the living room and then look like the cutest and most + innocent animal on the planet for sit on the laptop. Meow scratch leg; meow for can opener to feed me cat fur is + the new black but hide when guests come over, and Gate keepers of hell. Refuse to come home when humans are going + to bed; stay out all night then yowl like i am dying at 4am cat slap dog in face or eat a rug and furry furry hairs + everywhere oh no human coming lie on counter don't get off counter for i like fish sit on human they not getting up + ever but meow meow but cuddle no cuddle cuddle love scratch scratch.

+

I show my fluffy belly but it's a trap! if you pet it i will tear up your hand refuse to drink water except out of + someone's glass mice, so cough hairball, eat toilet paper or curl into a furry donut lick sellotape but wack the + mini furry mouse. When owners are asleep, cry for no apparent reason. Chase imaginary bugs. Stinky cat reward the + chosen human with a slow blink, or chase dog then run away. Chew on cable scratch the furniture for you are a + captive audience while sitting on the toilet, pet me for i like cats because they are fat and fluffy and spend all + night ensuring people don't sleep sleep all day. Scoot butt on the rug need to check on human, have not seen in an + hour might be dead oh look, human is alive, hiss at human, feed me, leave fur on owners clothes, so instantly break + out into full speed gallop across the house for no reason play riveting piece on synthesizer keyboard and scoot + butt on the rug yet meow meow. Attack dog, run away and pretend to be victim annoy the old grumpy cat, start a + fight and then retreat to wash when i lose or meow go back to sleep owner brings food and water tries to pet on + head, so scratch get sprayed by water because bad cat. Meowwww pelt around the house and up and down stairs chasing + phantoms drink water out of the faucet meow meow, i tell my human. Destroy couch.

+

Ask to go outside and ask to come inside and ask to go outside and ask to come inside the dog smells bad. Lick + butt and make a weird face. Toilet paper attack claws fluff everywhere meow miao french ciao litterbox. Shake treat + bag immediately regret falling into bathtub or white cat sleeps on a black shirt so what a cat-ass-trophy! eat + owner's food spit up on light gray carpet instead of adjacent linoleum. Warm up laptop with butt lick butt fart + rainbows until owner yells pee in litter box hiss at cats scratch the box so loved it, hated it, loved it, hated it + but need to check on human, have not seen in an hour might be dead oh look, human is alive, hiss at human, feed me. +

+
+
+ +
+ + + + diff --git a/src/core/client/embed/index.html b/src/core/client/embed/index.html index b579dab8f..e0e8f74e6 100644 --- a/src/core/client/embed/index.html +++ b/src/core/client/embed/index.html @@ -9,16 +9,21 @@ -

Talk 5.0 – Embed Stream

+

+ Article | Article With Button +

+

Talk 5.0 – Embed Stream

diff --git a/src/core/client/embed/index.spec.ts b/src/core/client/embed/index.spec.ts index 66613f1c7..696e6adba 100644 --- a/src/core/client/embed/index.spec.ts +++ b/src/core/client/embed/index.spec.ts @@ -1,8 +1,10 @@ -import * as Talk from "./"; +import mockConsole from "jest-mock-console"; +import * as Coral from "./"; + +// tslint:disable:no-console describe("Basic integration test", () => { const container: HTMLElement = document.createElement("div"); - let streamInterface: ReturnType; beforeAll(() => { container.id = "basic-integration-test-id"; document.body.appendChild(container); @@ -11,13 +13,40 @@ describe("Basic integration test", () => { document.body.removeChild(container); }); it("should render iframe", () => { - streamInterface = Talk.render({ + mockConsole(); + const TalkEmbedStream = Coral.Talk.createStreamEmbed({ id: "basic-integration-test-id", }); + TalkEmbedStream.render(); expect(container.innerHTML).toMatchSnapshot(); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.error).not.toHaveBeenCalled(); + }); + it("should use canonical link", () => { + mockConsole(); + const link = document.createElement("link"); + link.rel = "canonical"; + link.href = "http://localhost/canonical"; + document.head.appendChild(link); + const TalkEmbedStream = Coral.Talk.createStreamEmbed({ + id: "basic-integration-test-id", + }); + TalkEmbedStream.render(); + expect(container.innerHTML).toMatchSnapshot(); + document.head.removeChild(link); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.error).not.toHaveBeenCalled(); }); it("should remove iframe", () => { - streamInterface.remove(); + mockConsole(); + const TalkEmbedStream = Coral.Talk.createStreamEmbed({ + id: "basic-integration-test-id", + }); + TalkEmbedStream.render(); + TalkEmbedStream.remove(); expect(container.innerHTML).toBe(""); + // tslint:disable-next-line:no-console + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.error).not.toHaveBeenCalled(); }); }); diff --git a/src/core/client/embed/index.ts b/src/core/client/embed/index.ts index c440f72a8..542bc1caa 100644 --- a/src/core/client/embed/index.ts +++ b/src/core/client/embed/index.ts @@ -1,32 +1,2 @@ -import { EventEmitter2 } from "eventemitter2"; -import qs from "query-string"; - -import createStreamInterface from "./Stream"; - -export interface Config { - assetID?: string; - assetURL?: string; - commentID?: string; - rootURL?: string; - id?: string; - events?: (eventEmitter: EventEmitter2) => void; -} - -export function render(config: Config = {}) { - // Parse query params - const query = qs.parse(location.search); - const eventEmitter = new EventEmitter2({ wildcard: true }); - - if (config.events) { - config.events(eventEmitter); - } - - return createStreamInterface({ - assetID: config.assetID || query.assetID, - assetURL: config.assetURL || query.assetURL, - commentID: config.commentID || query.commentID, - id: config.id || "talk-embed-stream", - rootURL: config.rootURL || location.origin, - eventEmitter, - }); -} +import * as TalkImport from "./Talk"; +export const Talk = TalkImport; diff --git a/src/core/client/embed/onIntersect.ts b/src/core/client/embed/onIntersect.ts new file mode 100644 index 000000000..761abf970 --- /dev/null +++ b/src/core/client/embed/onIntersect.ts @@ -0,0 +1,20 @@ +export default function onIntersect(el: HTMLElement, callback: () => void) { + if (!IntersectionObserver) { + // tslint:disable-next-line:no-console + console.warn("IntersectionObserver not available"); + callback(); + return; + } + const options = { + rootMargin: "100px", + threshold: 1.0, + }; + + const observer = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + observer.disconnect(); + callback(); + } + }, options); + observer.observe(el); +} diff --git a/src/core/client/embed/utils/index.ts b/src/core/client/embed/utils/index.ts index 9a373ce0b..84796bfff 100644 --- a/src/core/client/embed/utils/index.ts +++ b/src/core/client/embed/utils/index.ts @@ -2,3 +2,4 @@ export { default as buildURL } from "./buildURL"; export { default as ensureEndSlash } from "./ensureEndSlash"; export { default as startsWith } from "./startsWith"; export { default as prefixStorage } from "./prefixStorage"; +export { default as parseHashQuery } from "./parseHashQuery"; diff --git a/src/core/client/embed/utils/parseHashQuery.spec.ts b/src/core/client/embed/utils/parseHashQuery.spec.ts new file mode 100644 index 000000000..93a00de8e --- /dev/null +++ b/src/core/client/embed/utils/parseHashQuery.spec.ts @@ -0,0 +1,24 @@ +import parseHashQuery from "./parseHashQuery"; + +it("should parse hash", () => { + const testCases: Array<[string, ReturnType]> = [ + [ + "#commentID=comment-id", + { + commentID: "comment-id", + }, + ], + [ + "#commentID=comment-id&assetURL=asset-url", + { + commentID: "comment-id", + assetURL: "asset-url", + }, + ], + ["#", {}], + ["", {}], + ]; + testCases.forEach(tc => { + expect(parseHashQuery(tc[0])).toEqual(tc[1]); + }); +}); diff --git a/src/core/client/embed/utils/parseHashQuery.ts b/src/core/client/embed/utils/parseHashQuery.ts new file mode 100644 index 000000000..daf5f5a5a --- /dev/null +++ b/src/core/client/embed/utils/parseHashQuery.ts @@ -0,0 +1,5 @@ +import qs from "query-string"; + +export default function parseQueryHash(hash: string): Record { + return qs.parse(hash); +} diff --git a/src/core/client/framework/utils/index.ts b/src/core/client/framework/utils/index.ts index 4eca76650..0b16c4981 100644 --- a/src/core/client/framework/utils/index.ts +++ b/src/core/client/framework/utils/index.ts @@ -1,2 +1,3 @@ export { default as buildURL } from "./buildURL"; export { default as parseURL } from "./parseURL"; +export { default as modifyQuery } from "./modifyQuery"; diff --git a/src/core/client/framework/utils/modifyQuery.spec.ts b/src/core/client/framework/utils/modifyQuery.spec.ts new file mode 100644 index 000000000..638554e8f --- /dev/null +++ b/src/core/client/framework/utils/modifyQuery.spec.ts @@ -0,0 +1,30 @@ +import modifyQuery from "./modifyQuery"; + +it("should modify query", () => { + const testCases: Array<[string, Record, string]> = [ + [ + "http://localhost:8080/?a=b#hash", + { + c: "d", + }, + "http://localhost:8080/?a=b&c=d#hash", + ], + [ + "http://localhost:8080/#hash", + { + a: "b", + }, + "http://localhost:8080/?a=b#hash", + ], + [ + "http://localhost:8080/?a=b#hash", + { + a: undefined, + }, + "http://localhost:8080/#hash", + ], + ]; + testCases.forEach(([url, params, expected]) => { + expect(modifyQuery(url, params)).toEqual(expected); + }); +}); diff --git a/src/core/client/framework/utils/modifyQuery.ts b/src/core/client/framework/utils/modifyQuery.ts new file mode 100644 index 000000000..b3f0cc41a --- /dev/null +++ b/src/core/client/framework/utils/modifyQuery.ts @@ -0,0 +1,11 @@ +import qs from "query-string"; + +import buildURL from "./buildURL"; +import parseURL from "./parseURL"; + +export default function modifyQuery(url: string, params: {}) { + const parsed = parseURL(url); + const query = qs.parse(parsed.search); + parsed.search = qs.stringify({ ...query, ...params }); + return buildURL(parsed); +} diff --git a/src/core/client/framework/utils/parseHashQuery.spec.ts b/src/core/client/framework/utils/parseHashQuery.spec.ts new file mode 100644 index 000000000..2cbb6eb4d --- /dev/null +++ b/src/core/client/framework/utils/parseHashQuery.spec.ts @@ -0,0 +1,24 @@ +import parseHashQuery from "./parseHashQuery"; + +it("should parse hash", () => { + const testCases: Array<[string, ReturnType]> = [ + [ + "#commentID=comment-id", + { + commentID: "comment-id", + }, + ], + [ + "#commentID=comment-id&assetURL=asset-url", + { + commentID: "comment-id", + assetURL: "asset-url", + }, + ], + ["#", {}], + ["", {}], + ]; + testCases.forEach(([url, expected]) => { + expect(parseHashQuery(url)).toEqual(expected); + }); +}); diff --git a/src/core/client/framework/utils/parseHashQuery.ts b/src/core/client/framework/utils/parseHashQuery.ts new file mode 100644 index 000000000..daf5f5a5a --- /dev/null +++ b/src/core/client/framework/utils/parseHashQuery.ts @@ -0,0 +1,5 @@ +import qs from "query-string"; + +export default function parseQueryHash(hash: string): Record { + return qs.parse(hash); +} diff --git a/src/core/client/stream/local/initLocalState.ts b/src/core/client/stream/local/initLocalState.ts index b2f09b110..b722b630e 100644 --- a/src/core/client/stream/local/initLocalState.ts +++ b/src/core/client/stream/local/initLocalState.ts @@ -42,12 +42,8 @@ export default async function initLocalState( localRecord.setValue(query.assetID, "assetID"); } - // Saving location host for permalink until we get the asset url - the url now points to the tenant - if (location && query.assetID) { - localRecord.setValue( - `${location.origin}/?assetID=${query.assetID}`, - "assetURL" - ); + if (query.assetURL) { + localRecord.setValue(query.assetURL, "assetURL"); } if (query.commentID) { diff --git a/src/core/client/stream/tabs/comments/components/Comment/Comment.spec.tsx b/src/core/client/stream/tabs/comments/components/Comment/Comment.spec.tsx index d9c0a9acf..a793f4d1c 100644 --- a/src/core/client/stream/tabs/comments/components/Comment/Comment.spec.tsx +++ b/src/core/client/stream/tabs/comments/components/Comment/Comment.spec.tsx @@ -7,6 +7,7 @@ import Comment from "./Comment"; it("renders username and body", () => { const props: PropTypesOf = { + id: "comment-id", author: { username: "Marvin", }, diff --git a/src/core/client/stream/tabs/comments/components/Comment/Comment.tsx b/src/core/client/stream/tabs/comments/components/Comment/Comment.tsx index aee9d5740..b30621913 100644 --- a/src/core/client/stream/tabs/comments/components/Comment/Comment.tsx +++ b/src/core/client/stream/tabs/comments/components/Comment/Comment.tsx @@ -10,6 +10,7 @@ import TopBarLeft from "./TopBarLeft"; import Username from "./Username"; export interface CommentProps { + id?: string; className?: string; author: { username: string | null; @@ -28,6 +29,7 @@ const Comment: StatelessComponent = props => { className={styles.topBar} direction="row" justifyContent="space-between" + id={props.id} > {props.author && diff --git a/src/core/client/stream/tabs/comments/components/Comment/__snapshots__/Comment.spec.tsx.snap b/src/core/client/stream/tabs/comments/components/Comment/__snapshots__/Comment.spec.tsx.snap index 64419b0fa..09cd14a62 100644 --- a/src/core/client/stream/tabs/comments/components/Comment/__snapshots__/Comment.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/components/Comment/__snapshots__/Comment.spec.tsx.snap @@ -8,6 +8,7 @@ exports[`renders username and body 1`] = ` diff --git a/src/core/client/stream/tabs/comments/components/Indent.css b/src/core/client/stream/tabs/comments/components/Indent.css index 6aa4c5e7a..b88e0b642 100644 --- a/src/core/client/stream/tabs/comments/components/Indent.css +++ b/src/core/client/stream/tabs/comments/components/Indent.css @@ -6,6 +6,30 @@ border-left: 3px solid var(--palette-grey-darkest); } +.level2 { + padding-left: var(--spacing-unit); + margin-left: var(--spacing-unit); + border-left: 3px solid var(--palette-grey-dark); +} + +.level3 { + padding-left: var(--spacing-unit); + margin-left: calc(2 * var(--spacing-unit)); + border-left: 3px solid var(--palette-grey-main); +} + +.level4 { + padding-left: var(--spacing-unit); + margin-left: calc(3 * var(--spacing-unit)); + border-left: 3px solid var(--palette-grey-light); +} + +.level5 { + padding-left: var(--spacing-unit); + margin-left: calc(4 * var(--spacing-unit)); + border-left: 3px solid var(--palette-grey-lighter); +} + .noBorder { border: 0; } diff --git a/src/core/client/stream/tabs/comments/components/Indent.tsx b/src/core/client/stream/tabs/comments/components/Indent.tsx index e40d10181..e91cdb974 100644 --- a/src/core/client/stream/tabs/comments/components/Indent.tsx +++ b/src/core/client/stream/tabs/comments/components/Indent.tsx @@ -12,13 +12,19 @@ export interface IndentProps { const Indent: StatelessComponent = props => { return ( -
- {props.children} +
+
+ {props.children} +
); }; diff --git a/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkButton.tsx b/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkButton.tsx index cf0fad56c..d93fb46e8 100644 --- a/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkButton.tsx +++ b/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkButton.tsx @@ -15,7 +15,7 @@ import PermalinkPopover from "./PermalinkPopover"; interface PermalinkProps { commentID: string; - assetURL: string | null; + url: string; } class Permalink extends React.Component { @@ -28,7 +28,7 @@ class Permalink extends React.Component { ); public render() { - const { commentID, assetURL } = this.props; + const { commentID, url } = this.props; const popoverID = `permalink-popover-${commentID}`; return ( { } > diff --git a/src/core/client/stream/tabs/comments/components/ReplyList.tsx b/src/core/client/stream/tabs/comments/components/ReplyList.tsx index 47e0dbc39..e942ae5a9 100644 --- a/src/core/client/stream/tabs/comments/components/ReplyList.tsx +++ b/src/core/client/stream/tabs/comments/components/ReplyList.tsx @@ -21,6 +21,17 @@ export interface ReplyListProps { hasMore: boolean; disableShowAll: boolean; indentLevel?: number; + ReplyListComponent?: React.ComponentType; +} + +function getReplyListElement( + { ReplyListComponent, me, asset }: ReplyListProps, + comment: PropTypesOf["comment"] +) { + if (!ReplyListComponent) { + return null; + } + return ; } const ReplyList: StatelessComponent = props => { @@ -30,13 +41,16 @@ const ReplyList: StatelessComponent = props => { role="log" > {props.comments.map(comment => ( - + + + {getReplyListElement(props, comment)} + ))} {props.hasMore && ( diff --git a/src/core/client/stream/tabs/comments/components/__snapshots__/Indent.spec.tsx.snap b/src/core/client/stream/tabs/comments/components/__snapshots__/Indent.spec.tsx.snap index d1b2bd78f..49a938937 100644 --- a/src/core/client/stream/tabs/comments/components/__snapshots__/Indent.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/components/__snapshots__/Indent.spec.tsx.snap @@ -4,28 +4,40 @@ exports[`renders level0 1`] = `
-
- Hello World +
+
+ Hello World +
`; exports[`renders level1 1`] = `
-
- Hello World +
+
+ Hello World +
`; exports[`renders without border 1`] = `
-
- Hello World +
+
+ Hello World +
`; diff --git a/src/core/client/stream/tabs/comments/components/__snapshots__/ReplyList.spec.tsx.snap b/src/core/client/stream/tabs/comments/components/__snapshots__/ReplyList.spec.tsx.snap index 3473f9fe0..cd78852c3 100644 --- a/src/core/client/stream/tabs/comments/components/__snapshots__/ReplyList.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/components/__snapshots__/ReplyList.spec.tsx.snap @@ -5,36 +5,44 @@ exports[`renders correctly 1`] = ` id="talk-comments-replyList-log--comment-id" role="log" > - - + + + + > + + `; @@ -43,36 +51,44 @@ exports[`when there is more disables load more button 1`] = ` id="talk-comments-replyList-log--comment-id" role="log" > - - + + + + > + + - - + + + + > + + - - - - - - - - { return ( <> = ({ commentID, }) => { return local.assetURL ? ( - + ) : null; }; diff --git a/src/core/client/stream/tabs/comments/containers/PermalinkViewContainer.tsx b/src/core/client/stream/tabs/comments/containers/PermalinkViewContainer.tsx index 664168e36..a21057c04 100644 --- a/src/core/client/stream/tabs/comments/containers/PermalinkViewContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/PermalinkViewContainer.tsx @@ -40,6 +40,18 @@ class PermalinkViewContainer extends React.Component< // Remove the commentId url param. return buildURL({ ...urlParts, search }); } + + public componentDidMount() { + if (this.props.pym) { + const scrollTo = this.props.comment + ? document + .getElementById(`comment-${this.props.comment.id}`)! + .getBoundingClientRect().top + window.pageYOffset + : 50; + setTimeout(() => this.props.pym!.scrollParentToChildPos(scrollTo), 100); + } + } + public render() { const { comment, asset, me } = this.props; return ( @@ -66,6 +78,7 @@ const enhanced = withContext(ctx => ({ `, comment: graphql` fragment PermalinkViewContainer_comment on Comment { + id ...CommentContainer_comment } `, diff --git a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.spec.tsx b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.spec.tsx index a23402ce1..08508b157 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.spec.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.spec.tsx @@ -27,6 +27,8 @@ it("renders correctly", () => { isLoading: noop, } as any, me: null, + indentLevel: 1, + ReplyListComponent: () => null, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); @@ -46,6 +48,8 @@ it("renders correctly when replies are null", () => { isLoading: noop, } as any, me: null, + indentLevel: 1, + ReplyListComponent: undefined, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); @@ -69,6 +73,8 @@ describe("when has more replies", () => { loadMore: (_: any, callback: () => void) => (finishLoading = callback), } as any, me: null, + indentLevel: 1, + ReplyListComponent: undefined, }; let wrapper: ShallowWrapper; diff --git a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx index 65c6b9ec9..f9744f55a 100644 --- a/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/ReplyListContainer.tsx @@ -1,15 +1,15 @@ import React from "react"; -import { graphql, RelayPaginationProp } from "react-relay"; +import { graphql, GraphQLTaggedNode, RelayPaginationProp } from "react-relay"; +import { withProps } from "recompose"; import { withPaginationContainer } from "talk-framework/lib/relay"; -import { PropTypesOf } from "talk-framework/types"; -import { ReplyListContainer_asset as AssetData } from "talk-stream/__generated__/ReplyListContainer_asset.graphql"; -import { ReplyListContainer_comment as CommentData } from "talk-stream/__generated__/ReplyListContainer_comment.graphql"; -import { ReplyListContainer_me as MeData } from "talk-stream/__generated__/ReplyListContainer_me.graphql"; +import { ReplyListContainer1_asset as AssetData } from "talk-stream/__generated__/ReplyListContainer1_asset.graphql"; +import { ReplyListContainer1_comment as CommentData } from "talk-stream/__generated__/ReplyListContainer1_comment.graphql"; +import { ReplyListContainer1_me as MeData } from "talk-stream/__generated__/ReplyListContainer1_me.graphql"; import { COMMENT_SORT, - ReplyListContainerPaginationQueryVariables, -} from "talk-stream/__generated__/ReplyListContainerPaginationQuery.graphql"; + ReplyListContainer1PaginationQueryVariables, +} from "talk-stream/__generated__/ReplyListContainer1PaginationQuery.graphql"; import ReplyList from "../components/ReplyList"; @@ -18,8 +18,16 @@ export interface InnerProps { asset: AssetData; comment: CommentData; relay: RelayPaginationProp; + indentLevel: number; + ReplyListComponent: React.ComponentType | undefined; } +// TODO: (cvle) This should be autogenerated. +interface FragmentVariables { + count: number; + cursor?: string; + orderBy: COMMENT_SORT; +} export class ReplyListContainer extends React.Component { public state = { disableShowAll: false, @@ -42,7 +50,8 @@ export class ReplyListContainer extends React.Component { onShowAll={this.showAll} hasMore={this.props.relay.hasMore()} disableShowAll={this.state.disableShowAll} - indentLevel={1} + indentLevel={this.props.indentLevel} + ReplyListComponent={this.props.ReplyListComponent} /> ); } @@ -66,31 +75,61 @@ export class ReplyListContainer extends React.Component { }; } -// TODO: (cvle) This should be autogenerated. -interface FragmentVariables { - count: number; - cursor?: string; - orderBy: COMMENT_SORT; +function createReplyListContainer( + indentLevel: number, + fragments: { + me: GraphQLTaggedNode; + asset: GraphQLTaggedNode; + comment: GraphQLTaggedNode; + }, + query: GraphQLTaggedNode, + ReplyListComponent?: React.ComponentType +) { + return withProps({ indentLevel, ReplyListComponent })( + withPaginationContainer< + InnerProps, + ReplyListContainer1PaginationQueryVariables, + FragmentVariables + >(fragments, { + direction: "forward", + getConnectionFromProps(props) { + return props.comment && props.comment.replies; + }, + // This is also the default implementation of `getFragmentVariables` if it isn't provided. + getFragmentVariables(prevVars, totalCount) { + return { + ...prevVars, + count: totalCount, + }; + }, + getVariables(props, { count, cursor }, fragmentVariables) { + return { + count, + cursor, + orderBy: fragmentVariables.orderBy, + commentID: props.comment.id, + }; + }, + query, + })(ReplyListContainer) + ); } -const enhanced = withPaginationContainer< - InnerProps, - ReplyListContainerPaginationQueryVariables, - FragmentVariables ->( +const ReplyListContainer5 = createReplyListContainer( + 5, { me: graphql` - fragment ReplyListContainer_me on User { + fragment ReplyListContainer5_me on User { ...CommentContainer_me } `, asset: graphql` - fragment ReplyListContainer_asset on Asset { + fragment ReplyListContainer5_asset on Asset { ...CommentContainer_asset } `, comment: graphql` - fragment ReplyListContainer_comment on Comment + fragment ReplyListContainer5_comment on Comment @argumentDefinitions( count: { type: "Int!", defaultValue: 5 } cursor: { type: "Cursor" } @@ -109,43 +148,237 @@ const enhanced = withPaginationContainer< } `, }, + graphql` + # Pagination query to be fetched upon calling 'loadMore'. + # Notice that we re-use our fragment, and the shape of this query matches our fragment spec. + query ReplyListContainer5PaginationQuery( + $count: Int! + $cursor: Cursor + $orderBy: COMMENT_SORT! + $commentID: ID! + ) { + comment(id: $commentID) { + ...ReplyListContainer5_comment + @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) + } + } + ` +); + +const ReplyListContainer4 = createReplyListContainer( + 4, { - direction: "forward", - getConnectionFromProps(props) { - return props.comment && props.comment.replies; - }, - // This is also the default implementation of `getFragmentVariables` if it isn't provided. - getFragmentVariables(prevVars, totalCount) { - return { - ...prevVars, - count: totalCount, - }; - }, - getVariables(props, { count, cursor }, fragmentVariables) { - return { - count, - cursor, - orderBy: fragmentVariables.orderBy, - commentID: props.comment.id, - }; - }, - query: graphql` - # Pagination query to be fetched upon calling 'loadMore'. - # Notice that we re-use our fragment, and the shape of this query matches our fragment spec. - query ReplyListContainerPaginationQuery( - $count: Int! - $cursor: Cursor - $orderBy: COMMENT_SORT! - $commentID: ID! - ) { - comment(id: $commentID) { - ...ReplyListContainer_comment - @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) + me: graphql` + fragment ReplyListContainer4_me on User { + ...ReplyListContainer5_me + ...CommentContainer_me + } + `, + asset: graphql` + fragment ReplyListContainer4_asset on Asset { + ...ReplyListContainer5_asset + ...CommentContainer_asset + } + `, + comment: graphql` + fragment ReplyListContainer4_comment on Comment + @argumentDefinitions( + count: { type: "Int!", defaultValue: 5 } + cursor: { type: "Cursor" } + orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC } + ) { + id + replies(first: $count, after: $cursor, orderBy: $orderBy) + @connection(key: "ReplyList_replies") { + edges { + node { + id + ...CommentContainer_comment + ...ReplyListContainer5_comment + } + } } } `, - } -)(ReplyListContainer); + }, + graphql` + # Pagination query to be fetched upon calling 'loadMore'. + # Notice that we re-use our fragment, and the shape of this query matches our fragment spec. + query ReplyListContainer4PaginationQuery( + $count: Int! + $cursor: Cursor + $orderBy: COMMENT_SORT! + $commentID: ID! + ) { + comment(id: $commentID) { + ...ReplyListContainer4_comment + @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) + } + } + `, + ReplyListContainer5 +); -export type ReplyListContainerProps = PropTypesOf; -export default enhanced; +const ReplyListContainer3 = createReplyListContainer( + 3, + { + me: graphql` + fragment ReplyListContainer3_me on User { + ...ReplyListContainer4_me + ...CommentContainer_me + } + `, + asset: graphql` + fragment ReplyListContainer3_asset on Asset { + ...ReplyListContainer4_asset + ...CommentContainer_asset + } + `, + comment: graphql` + fragment ReplyListContainer3_comment on Comment + @argumentDefinitions( + count: { type: "Int!", defaultValue: 5 } + cursor: { type: "Cursor" } + orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC } + ) { + id + replies(first: $count, after: $cursor, orderBy: $orderBy) + @connection(key: "ReplyList_replies") { + edges { + node { + id + ...CommentContainer_comment + ...ReplyListContainer4_comment + } + } + } + } + `, + }, + graphql` + # Pagination query to be fetched upon calling 'loadMore'. + # Notice that we re-use our fragment, and the shape of this query matches our fragment spec. + query ReplyListContainer3PaginationQuery( + $count: Int! + $cursor: Cursor + $orderBy: COMMENT_SORT! + $commentID: ID! + ) { + comment(id: $commentID) { + ...ReplyListContainer3_comment + @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) + } + } + `, + ReplyListContainer4 +); + +const ReplyListContainer2 = createReplyListContainer( + 2, + { + me: graphql` + fragment ReplyListContainer2_me on User { + ...ReplyListContainer3_me + ...CommentContainer_me + } + `, + asset: graphql` + fragment ReplyListContainer2_asset on Asset { + ...ReplyListContainer3_asset + ...CommentContainer_asset + } + `, + comment: graphql` + fragment ReplyListContainer2_comment on Comment + @argumentDefinitions( + count: { type: "Int!", defaultValue: 5 } + cursor: { type: "Cursor" } + orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC } + ) { + id + replies(first: $count, after: $cursor, orderBy: $orderBy) + @connection(key: "ReplyList_replies") { + edges { + node { + id + ...CommentContainer_comment + ...ReplyListContainer3_comment + } + } + } + } + `, + }, + graphql` + # Pagination query to be fetched upon calling 'loadMore'. + # Notice that we re-use our fragment, and the shape of this query matches our fragment spec. + query ReplyListContainer2PaginationQuery( + $count: Int! + $cursor: Cursor + $orderBy: COMMENT_SORT! + $commentID: ID! + ) { + comment(id: $commentID) { + ...ReplyListContainer2_comment + @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) + } + } + `, + ReplyListContainer3 +); + +const ReplyListContainer1 = createReplyListContainer( + 1, + { + me: graphql` + fragment ReplyListContainer1_me on User { + ...ReplyListContainer2_me + ...CommentContainer_me + } + `, + asset: graphql` + fragment ReplyListContainer1_asset on Asset { + ...ReplyListContainer2_asset + ...CommentContainer_asset + } + `, + comment: graphql` + fragment ReplyListContainer1_comment on Comment + @argumentDefinitions( + count: { type: "Int!", defaultValue: 5 } + cursor: { type: "Cursor" } + orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC } + ) { + id + replies(first: $count, after: $cursor, orderBy: $orderBy) + @connection(key: "ReplyList_replies") { + edges { + node { + id + ...CommentContainer_comment + ...ReplyListContainer2_comment + } + } + } + } + `, + }, + graphql` + # Pagination query to be fetched upon calling 'loadMore'. + # Notice that we re-use our fragment, and the shape of this query matches our fragment spec. + query ReplyListContainer1PaginationQuery( + $count: Int! + $cursor: Cursor + $orderBy: COMMENT_SORT! + $commentID: ID! + ) { + comment(id: $commentID) { + ...ReplyListContainer1_comment + @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) + } + } + `, + ReplyListContainer2 +); + +export default ReplyListContainer1; diff --git a/src/core/client/stream/tabs/comments/containers/StreamContainer.tsx b/src/core/client/stream/tabs/comments/containers/StreamContainer.tsx index 980d737b2..5ca2286bc 100644 --- a/src/core/client/stream/tabs/comments/containers/StreamContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/StreamContainer.tsx @@ -23,7 +23,7 @@ graphql` fragment StreamContainer_comment on Comment { id ...CommentContainer_comment - ...ReplyListContainer_comment + ...ReplyListContainer1_comment } `; @@ -95,12 +95,12 @@ const enhanced = withPaginationContainer< } } ...CommentContainer_asset - ...ReplyListContainer_asset + ...ReplyListContainer1_asset } `, me: graphql` fragment StreamContainer_me on User { - ...ReplyListContainer_me + ...ReplyListContainer1_me ...CommentContainer_me ...UserBoxContainer_me } @@ -135,7 +135,7 @@ const enhanced = withPaginationContainer< $count: Int! $cursor: Cursor $orderBy: COMMENT_SORT! - $assetID: ID! + $assetID: ID ) { asset(id: $assetID) { ...StreamContainer_asset diff --git a/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap b/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap index f6e7f50ba..db1219282 100644 --- a/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/containers/__snapshots__/CommentContainer.spec.tsx.snap @@ -24,6 +24,7 @@ exports[`renders body only 1`] = ` /> } + id="comment-comment-id" indentLevel={1} showEditedMarker={false} /> @@ -54,6 +55,7 @@ exports[`renders username and body 1`] = ` /> } + id="comment-comment-id" indentLevel={1} showEditedMarker={false} /> diff --git a/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap b/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap index e17a56740..ad14ab002 100644 --- a/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/containers/__snapshots__/ReplyListContainer.spec.tsx.snap @@ -2,6 +2,7 @@ exports[`renders correctly 1`] = ` = ({ - local: { commentID, assetID }, + local: { commentID, assetID, assetURL }, }) => ( query={graphql` - query PermalinkViewQuery($commentID: ID!, $assetID: ID!) { + query PermalinkViewQuery( + $commentID: ID! + $assetID: ID + $assetURL: String + ) { me { ...PermalinkViewContainer_me } - asset(id: $assetID) { + asset(id: $assetID, url: $assetURL) { ...PermalinkViewContainer_asset } comment(id: $commentID) { @@ -62,8 +66,9 @@ const PermalinkViewQuery: StatelessComponent = ({ } `} variables={{ - assetID: assetID!, commentID: commentID!, + assetID, + assetURL, }} render={render} /> @@ -74,6 +79,7 @@ const enhanced = withLocalStateContainer( fragment PermalinkViewQueryLocal on Local { assetID commentID + assetURL } ` )(PermalinkViewQuery); diff --git a/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx b/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx index ec61ec723..da7879f8e 100644 --- a/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx +++ b/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx @@ -38,21 +38,22 @@ export const render = ({ }; const StreamQuery: StatelessComponent = ({ - local: { assetID }, + local: { assetID, assetURL }, }) => ( query={graphql` - query StreamQuery($assetID: ID!) { + query StreamQuery($assetID: ID, $assetURL: String) { me { ...StreamContainer_me } - asset(id: $assetID) { + asset(id: $assetID, url: $assetURL) { ...StreamContainer_asset } } `} variables={{ - assetID: assetID!, + assetID, + assetURL, }} render={render} /> @@ -62,6 +63,7 @@ const enhanced = withLocalStateContainer( graphql` fragment StreamQueryLocal on Local { assetID + assetURL } ` )(StreamQuery); diff --git a/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap index 6a91b49e0..b843afa72 100644 --- a/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap @@ -191,36 +191,67 @@ exports[`cancel edit: edit canceled 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
+
+
+
-
+
+
-
-
- -
@@ -272,60 +277,65 @@ exports[`cancel edit: edit canceled 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
@@ -691,60 +701,65 @@ exports[`edit a comment: edit form 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
@@ -1110,60 +1125,65 @@ exports[`edit a comment: optimistic response 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
@@ -1364,36 +1384,67 @@ exports[`edit a comment: render stream 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
+
+
+
-
+
+
-
-
- -
@@ -1445,60 +1470,65 @@ exports[`edit a comment: render stream 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
@@ -1699,45 +1729,76 @@ exports[`edit a comment: server response 1`] = ` className="Indent-root" >
- - Markus -
- + Markus +
- ( - - Edited - - ) + +
+ ( + + Edited + + ) +
+
+ +
-
+
+
-
-
- -
@@ -1789,60 +1824,65 @@ exports[`edit a comment: server response 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
@@ -2043,36 +2083,67 @@ exports[`shows expiry message: edit form closed 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
+
+
+
-
+
+
-
-
- -
@@ -2124,60 +2169,65 @@ exports[`shows expiry message: edit form closed 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
@@ -2520,60 +2570,65 @@ exports[`shows expiry message: edit time expired 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
diff --git a/src/core/client/stream/test/comments/__snapshots__/loadMore.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/loadMore.spec.tsx.snap index c8496f77f..7a6a13b76 100644 --- a/src/core/client/stream/test/comments/__snapshots__/loadMore.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/loadMore.spec.tsx.snap @@ -152,60 +152,65 @@ exports[`loads more comments 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
@@ -217,60 +222,65 @@ exports[`loads more comments 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
@@ -282,60 +292,65 @@ exports[`loads more comments 1`] = ` className="Indent-root" >
- - Isabelle -
- + Isabelle + +
+ +
-
-
-
- + +
@@ -497,60 +512,65 @@ exports[`renders comment stream 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
@@ -562,60 +582,65 @@ exports[`renders comment stream 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
diff --git a/src/core/client/stream/test/comments/__snapshots__/permalinkView.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/permalinkView.spec.tsx.snap index 2857bd17a..7de038658 100644 --- a/src/core/client/stream/test/comments/__snapshots__/permalinkView.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/permalinkView.spec.tsx.snap @@ -27,60 +27,65 @@ exports[`renders permalink view 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
@@ -240,60 +245,65 @@ exports[`show all comments 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
diff --git a/src/core/client/stream/test/comments/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap index e3d7ca646..bd22c7e93 100644 --- a/src/core/client/stream/test/comments/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap @@ -184,60 +184,65 @@ exports[`show all comments 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
diff --git a/src/core/client/stream/test/comments/__snapshots__/postComment.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/postComment.spec.tsx.snap index 28eab8f9c..f52d50282 100644 --- a/src/core/client/stream/test/comments/__snapshots__/postComment.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/postComment.spec.tsx.snap @@ -185,36 +185,67 @@ exports[`post a comment: optimistic response 1`] = ` className="IndentedComment-blur Indent-root" >
- - Markus -
- + Markus + +
+ +
+
+
+
-
+
Hello world!", + } + } + /> +
+
+
+
+
+
+
Hello world!", - } - } - /> -
- +
+ + Markus + +
+ +
+
+
+
+
+ +
@@ -266,125 +341,65 @@ exports[`post a comment: optimistic response 1`] = ` className="Indent-root" >
- - Markus -
- + Lukas + +
+ +
-
-
-
- -
-
-
-
-
-
-
-
- - Lukas - -
- -
-
-
-
-
- + +
@@ -585,60 +600,65 @@ exports[`post a comment: server response 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
Hello world! (from server)", +
Hello world! (from server)", + } } - } - /> -
- + +
@@ -650,60 +670,65 @@ exports[`post a comment: server response 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
@@ -715,60 +740,65 @@ exports[`post a comment: server response 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
@@ -969,60 +999,65 @@ exports[`renders comment stream 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
@@ -1034,60 +1069,65 @@ exports[`renders comment stream 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
diff --git a/src/core/client/stream/test/comments/__snapshots__/postReply.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/postReply.spec.tsx.snap index b7546e97c..c1913d1ba 100644 --- a/src/core/client/stream/test/comments/__snapshots__/postReply.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/postReply.spec.tsx.snap @@ -191,60 +191,65 @@ exports[`post a reply: open reply form 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
@@ -383,60 +388,65 @@ exports[`post a reply: open reply form 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
@@ -637,60 +647,65 @@ exports[`post a reply: optimistic response 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
@@ -821,7 +836,101 @@ exports[`post a reply: optimistic response 1`] = ` role="log" >
+
+
+
+
+
+ + Markus + +
+ +
+
+
+ +
+
+
Hello world!", + } + } + /> +
+ +
+
+
+
+
+
+ +
+
+
- Markus + Lukas
-
- -
Hello world!", + "__html": "What's up?", } } /> @@ -880,7 +974,7 @@ exports[`post a reply: optimistic response 1`] = ` >
-
-
-
-
-
- - Lukas - -
- -
-
-
-
-
- -
-
-
-
@@ -1160,70 +1189,7 @@ exports[`post a reply: server response 1`] = ` className="Indent-root" >
-
-
- - Markus - -
- -
-
-
-
-
- -
-
-
-
-
Hello world! (from server)", + "__html": "Joining Too", } } /> @@ -1266,7 +1233,7 @@ exports[`post a reply: server response 1`] = ` >
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
Hello world! (from server)", + } + } + /> +
+ +
+
+
+
+
+
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
@@ -1546,60 +1594,65 @@ exports[`renders comment stream 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
@@ -1611,60 +1664,65 @@ exports[`renders comment stream 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
diff --git a/src/core/client/stream/test/comments/__snapshots__/renderReplies.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/renderReplies.spec.tsx.snap index 1ffdb59c7..2266711cd 100644 --- a/src/core/client/stream/test/comments/__snapshots__/renderReplies.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/renderReplies.spec.tsx.snap @@ -152,135 +152,7 @@ exports[`renders comment stream 1`] = ` className="Indent-root" >
-
-
- - Markus - -
- -
-
-
-
-
- -
-
-
-
-
-
-
-
-
- - Markus - -
- -
-
-
-
-
- -
-
-
-
-
+
+
+
+
- Lukas + Markus
@@ -375,7 +257,7 @@ exports[`renders comment stream 1`] = ` className="HTMLContent-root" dangerouslySetInnerHTML={ Object { - "__html": "What's up?", + "__html": "I like yoghurt", } } /> @@ -384,7 +266,7 @@ exports[`renders comment stream 1`] = ` >
+
+
+
+
+
+
+
+ + Markus + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Isabelle + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + Isabelle + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + Isabelle + +
+ +
+
+
+
+
+ +
+
+
+
+
+
diff --git a/src/core/client/stream/test/comments/__snapshots__/renderStream.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/renderStream.spec.tsx.snap index a6ed47452..bfe500ef4 100644 --- a/src/core/client/stream/test/comments/__snapshots__/renderStream.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/renderStream.spec.tsx.snap @@ -152,60 +152,65 @@ exports[`renders comment stream 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
@@ -217,60 +222,65 @@ exports[`renders comment stream 1`] = ` className="Indent-root" >
- - Lukas -
- + Lukas + +
+ +
-
-
-
- + +
diff --git a/src/core/client/stream/test/comments/__snapshots__/showAllReplies.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/showAllReplies.spec.tsx.snap index fe7fa13f6..27fc4c77a 100644 --- a/src/core/client/stream/test/comments/__snapshots__/showAllReplies.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/showAllReplies.spec.tsx.snap @@ -152,70 +152,7 @@ exports[`renders comment stream 1`] = ` className="Indent-root" >
-
-
- - Markus - -
- -
-
-
-
-
- -
-
-
-
-
- Lukas + Markus
@@ -249,7 +187,7 @@ exports[`renders comment stream 1`] = ` className="HTMLContent-root" dangerouslySetInnerHTML={ Object { - "__html": "What's up?", + "__html": "Joining Too", } } /> @@ -258,7 +196,7 @@ exports[`renders comment stream 1`] = ` >
+
+
- +
+
+
+
+ + Lukas + +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
@@ -454,60 +472,65 @@ exports[`show all replies 1`] = ` className="Indent-root" >
- - Markus -
- + Markus + +
+ +
-
-
-
- + +
@@ -517,125 +540,143 @@ exports[`show all replies 1`] = ` role="log" >
- - Lukas -
- + + Lukas + +
+ +
+
+
+
+
+
-
-
- -
- - Isabelle -
- + + Isabelle + +
+ +
+
+
+
+
+
-
-
- -
diff --git a/src/core/client/stream/test/comments/editComment.spec.tsx b/src/core/client/stream/test/comments/editComment.spec.tsx index 733f30eb2..67684bc6f 100644 --- a/src/core/client/stream/test/comments/editComment.spec.tsx +++ b/src/core/client/stream/test/comments/editComment.spec.tsx @@ -11,7 +11,10 @@ function createTestRenderer() { Query: { asset: createSinonStub( s => s.throws(), - s => s.withArgs(undefined, { id: assets[0].id }).returns(assets[0]) + s => + s + .withArgs(undefined, { id: assets[0].id, url: null }) + .returns(assets[0]) ), me: createSinonStub( s => s.throws(), diff --git a/src/core/client/stream/test/comments/loadMore.spec.tsx b/src/core/client/stream/test/comments/loadMore.spec.tsx index 404d3c702..d51ea60fa 100644 --- a/src/core/client/stream/test/comments/loadMore.spec.tsx +++ b/src/core/client/stream/test/comments/loadMore.spec.tsx @@ -1,4 +1,5 @@ import { ReactTestRenderer } from "react-test-renderer"; +import sinon from "sinon"; import { timeout } from "talk-common/utils"; import { createSinonStub } from "talk-framework/testHelpers"; @@ -55,7 +56,15 @@ beforeEach(() => { Query: { asset: createSinonStub( s => s.throws(), - s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub) + s => + s + .withArgs( + undefined, + sinon + .match({ id: assetStub.id, url: null }) + .or(sinon.match({ id: assetStub.id })) + ) + .returns(assetStub) ), }, }; diff --git a/src/core/client/stream/test/comments/permalinkView.spec.tsx b/src/core/client/stream/test/comments/permalinkView.spec.tsx index 0bbe3e260..180abbe0b 100644 --- a/src/core/client/stream/test/comments/permalinkView.spec.tsx +++ b/src/core/client/stream/test/comments/permalinkView.spec.tsx @@ -36,7 +36,10 @@ beforeEach(() => { ), asset: createSinonStub( s => s.throws(), - s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub) + s => + s + .withArgs(undefined, { id: assetStub.id, url: null }) + .returns(assetStub) ), }, }; diff --git a/src/core/client/stream/test/comments/permalinkViewCommentNotFound.spec.tsx b/src/core/client/stream/test/comments/permalinkViewCommentNotFound.spec.tsx index 9c4517351..6ff259e93 100644 --- a/src/core/client/stream/test/comments/permalinkViewCommentNotFound.spec.tsx +++ b/src/core/client/stream/test/comments/permalinkViewCommentNotFound.spec.tsx @@ -33,7 +33,10 @@ beforeEach(() => { comment: () => null, asset: createSinonStub( s => s.throws(), - s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub) + s => + s + .withArgs(undefined, { id: assetStub.id, url: null }) + .returns(assetStub) ), }, }; diff --git a/src/core/client/stream/test/comments/renderReplies.spec.tsx b/src/core/client/stream/test/comments/renderReplies.spec.tsx index 3c361740d..11ffcf000 100644 --- a/src/core/client/stream/test/comments/renderReplies.spec.tsx +++ b/src/core/client/stream/test/comments/renderReplies.spec.tsx @@ -3,7 +3,7 @@ import { ReactTestRenderer } from "react-test-renderer"; import { timeout } from "talk-common/utils"; import { createSinonStub } from "talk-framework/testHelpers"; -import { assetWithReplies } from "../fixtures"; +import { assetWithDeepReplies } from "../fixtures"; import create from "./create"; let testRenderer: ReactTestRenderer; @@ -14,8 +14,8 @@ beforeEach(() => { s => s.throws(), s => s - .withArgs(undefined, { id: assetWithReplies.id }) - .returns(assetWithReplies) + .withArgs(undefined, { id: assetWithDeepReplies.id, url: null }) + .returns(assetWithDeepReplies) ), }, }; @@ -25,7 +25,7 @@ beforeEach(() => { logNetwork: false, resolvers, initLocalState: localRecord => { - localRecord.setValue(assetWithReplies.id, "assetID"); + localRecord.setValue(assetWithDeepReplies.id, "assetID"); }, })); }); diff --git a/src/core/client/stream/test/comments/renderStream.spec.tsx b/src/core/client/stream/test/comments/renderStream.spec.tsx index 2778acb02..6a104ea6e 100644 --- a/src/core/client/stream/test/comments/renderStream.spec.tsx +++ b/src/core/client/stream/test/comments/renderStream.spec.tsx @@ -12,7 +12,10 @@ beforeEach(() => { Query: { asset: createSinonStub( s => s.throws(), - s => s.withArgs(undefined, { id: assets[0].id }).returns(assets[0]) + s => + s + .withArgs(undefined, { id: assets[0].id, url: null }) + .returns(assets[0]) ), }, }; diff --git a/src/core/client/stream/test/comments/showAllReplies.spec.tsx b/src/core/client/stream/test/comments/showAllReplies.spec.tsx index 547ac7b33..ac1c699e2 100644 --- a/src/core/client/stream/test/comments/showAllReplies.spec.tsx +++ b/src/core/client/stream/test/comments/showAllReplies.spec.tsx @@ -71,7 +71,10 @@ beforeEach(() => { ), asset: createSinonStub( s => s.throws(), - s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub) + s => + s + .withArgs(undefined, { id: assetStub.id, url: null }) + .returns(assetStub) ), }, }; diff --git a/src/core/client/stream/test/fixtures.ts b/src/core/client/stream/test/fixtures.ts index ec219fb67..47485cfa7 100644 --- a/src/core/client/stream/test/fixtures.ts +++ b/src/core/client/stream/test/fixtures.ts @@ -47,6 +47,39 @@ export const comments = [ editableUntil: "2018-07-06T18:14:30.000Z", }, }, + { + id: "comment-3", + author: users[2], + body: "Comment Body 3", + createdAt: "2018-07-06T18:14:00.000Z", + replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } }, + editing: { + edited: false, + editableUntil: "2018-07-06T18:14:30.000Z", + }, + }, + { + id: "comment-4", + author: users[2], + body: "Comment Body 4", + createdAt: "2018-07-06T18:14:00.000Z", + replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } }, + editing: { + edited: false, + editableUntil: "2018-07-06T18:14:30.000Z", + }, + }, + { + id: "comment-5", + author: users[2], + body: "Comment Body 5", + createdAt: "2018-07-06T18:14:00.000Z", + replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } }, + editing: { + edited: false, + editableUntil: "2018-07-06T18:14:30.000Z", + }, + }, ]; export const assets = [ @@ -73,8 +106,28 @@ export const commentWithReplies = { createdAt: "2018-07-06T18:24:00.000Z", replies: { edges: [ - { node: comments[0], cursor: comments[0].createdAt }, - { node: comments[1], cursor: comments[1].createdAt }, + { node: comments[3], cursor: comments[3].createdAt }, + { node: comments[4], cursor: comments[4].createdAt }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + editing: { + edited: false, + editableUntil: "2018-07-06T18:24:30.000Z", + }, +}; + +export const commentWithDeepReplies = { + id: "comment-with-deep-replies", + author: users[0], + body: "I like yoghurt", + createdAt: "2018-07-06T18:24:00.000Z", + replies: { + edges: [ + { node: commentWithReplies, cursor: commentWithReplies.createdAt }, + { node: comments[5], cursor: comments[5].createdAt }, ], pageInfo: { hasNextPage: false, @@ -100,3 +153,21 @@ export const assetWithReplies = { }, }, }; + +export const assetWithDeepReplies = { + id: "asset-with-deep-replies", + url: "http://localhost/assets/asset-with-replies", + isClosed: false, + comments: { + edges: [ + { node: comments[0], cursor: comments[0].createdAt }, + { + node: commentWithDeepReplies, + cursor: commentWithDeepReplies.createdAt, + }, + ], + pageInfo: { + hasNextPage: false, + }, + }, +}; diff --git a/src/core/client/test/setupTestFramework.ts b/src/core/client/test/setupTestFramework.ts new file mode 100644 index 000000000..2f7044378 --- /dev/null +++ b/src/core/client/test/setupTestFramework.ts @@ -0,0 +1,2 @@ +// Automatically unmock console. +import "jest-mock-console/dist/setupTestFramework"; diff --git a/src/core/server/app/middleware/passport/jwt.spec.ts b/src/core/server/app/middleware/passport/jwt.spec.ts index 3c25db7a4..6078b35f3 100644 --- a/src/core/server/app/middleware/passport/jwt.spec.ts +++ b/src/core/server/app/middleware/passport/jwt.spec.ts @@ -4,65 +4,34 @@ import { Config } from "talk-common/config"; import { createJWTSigningConfig, extractJWTFromRequest, - parseAuthHeader, } from "talk-server/app/middleware/passport/jwt"; import { Request } from "talk-server/types/express"; -describe("parseAuthHeader", () => { - it("parses valid headers", () => { - const parsed = { - scheme: "bearer", - value: "token", - }; - - expect(parseAuthHeader("Bearer token")).toEqual(parsed); - - expect(parseAuthHeader("bearer token")).toEqual(parsed); - - expect(parseAuthHeader("bearer token")).toEqual(parsed); - }); - - it("parses invalid headers", () => { - expect(parseAuthHeader("this-is-a-wrong-header")).toEqual(null); - expect(parseAuthHeader("bearerthis-is-a-wrong-header")).toEqual(null); - }); -}); - describe("extractJWTFromRequest", () => { it("extracts the token from header", () => { const req = { - get: sinon - .stub() - .withArgs("authorization") - .returns("Bearer token"), + headers: { + authorization: "Bearer token", + }, + url: "", }; expect(extractJWTFromRequest((req as any) as Request)).toEqual("token"); - expect(req.get.calledOnce).toBeTruthy(); - req.get.reset(); - req.get.returns(null); + delete req.headers.authorization; + expect(extractJWTFromRequest((req as any) as Request)).toEqual(null); - expect(req.get.calledOnce).toBeTruthy(); }); it("extracts the token from query string", () => { const req = { - get: sinon - .stub() - .withArgs("authorization") - .returns(null), - query: { access_token: "token" }, + url: "", }; + expect(extractJWTFromRequest((req as any) as Request)).toEqual(null); + + req.url = "https://talk.coralproject.net/api?access_token=token"; expect(extractJWTFromRequest((req as any) as Request)).toEqual("token"); - expect(req.get.calledOnce).toBeTruthy(); - - delete req.query.access_token; - - req.get.reset(); - expect(extractJWTFromRequest((req as any) as Request)).toEqual(null); - expect(req.get.calledOnce).toBeTruthy(); }); }); diff --git a/src/core/server/app/middleware/passport/jwt.ts b/src/core/server/app/middleware/passport/jwt.ts index 3736b6e14..0359ef450 100644 --- a/src/core/server/app/middleware/passport/jwt.ts +++ b/src/core/server/app/middleware/passport/jwt.ts @@ -2,41 +2,20 @@ import { Redis } from "ioredis"; import jwt, { SignOptions } from "jsonwebtoken"; import { Db } from "mongodb"; import { Strategy } from "passport-strategy"; +import { Bearer } from "permit"; import uuid from "uuid"; import { Config } from "talk-common/config"; import { retrieveUser, User } from "talk-server/models/user"; import { Request } from "talk-server/types/express"; -const authHeaderRegex = /(\S+)\s+(\S+)/; - -export function parseAuthHeader(header: string) { - const matches = header.match(authHeaderRegex); - if (!matches || matches.length < 3) { - return null; - } - - return { - scheme: matches[1].toLowerCase(), - value: matches[2], - }; -} - export function extractJWTFromRequest(req: Request) { - const header = req.get("authorization"); - if (header) { - const parts = parseAuthHeader(header); - if (parts && parts.scheme === "bearer") { - return parts.value; - } - } + const permit = new Bearer({ + basic: "password", + query: "access_token", + }); - const token: string | undefined | false = req.query && req.query.access_token; - if (token) { - return token; - } - - return null; + return permit.check(req) || null; } function generateJTIBlacklistKey(jti: string) { diff --git a/src/types/permit.d.ts b/src/types/permit.d.ts new file mode 100644 index 000000000..c24f13a6b --- /dev/null +++ b/src/types/permit.d.ts @@ -0,0 +1,31 @@ +// TODO: (wyattjoh) following https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29061 to merge then replace this with @types/permit. +declare module "permit" { + import { IncomingMessage, ServerResponse } from "http"; + + export interface PermitOptions { + scheme?: string; + proxy?: string; + realm?: string; + } + + export interface BearerOptions extends PermitOptions { + basic?: string; + header?: string; + query?: string; + } + + export class Permit { + constructor(options: PermitOptions); + check(req: IncomingMessage): void; + fail(res: ServerResponse): void; + } + + export class Bearer extends Permit { + constructor(options: BearerOptions); + check(req: IncomingMessage): string; + } + + export class Basic extends Permit { + check(req: IncomingMessage): [string, string]; + } +} \ No newline at end of file