summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/post.go29
-rw-r--r--api/post_test.go31
-rw-r--r--glide.lock10
-rw-r--r--glide.yaml4
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/.gitignore24
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/LICENSE22
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/README.md118
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/examples/advanced.go58
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/examples/simple.go27
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph.go329
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph_test.go131
-rw-r--r--webapp/actions/global_actions.jsx24
-rw-r--r--webapp/client/client.jsx10
-rw-r--r--webapp/components/post_view/components/post.jsx4
-rw-r--r--webapp/components/post_view/components/post_attachment_oembed.jsx108
-rw-r--r--webapp/components/post_view/components/post_attachment_opengraph.jsx212
-rw-r--r--webapp/components/post_view/components/post_body.jsx4
-rw-r--r--webapp/components/post_view/components/post_body_additional_content.jsx64
-rw-r--r--webapp/components/post_view/components/post_list.jsx16
-rw-r--r--webapp/components/post_view/components/providers.json376
-rw-r--r--webapp/sass/layout/_webhooks.scss20
-rw-r--r--webapp/stores/opengraph_store.jsx68
-rw-r--r--webapp/tests/utils_get_nearest_point.test.jsx35
-rw-r--r--webapp/utils/commons.jsx36
-rw-r--r--webapp/utils/constants.jsx3
-rw-r--r--webapp/utils/utils.jsx12
26 files changed, 1241 insertions, 534 deletions
diff --git a/api/post.go b/api/post.go
index 0e3ad2aa5..ba089ec4f 100644
--- a/api/post.go
+++ b/api/post.go
@@ -8,6 +8,7 @@ import (
"strconv"
l4g "github.com/alecthomas/log4go"
+ "github.com/dyatlov/go-opengraph/opengraph"
"github.com/gorilla/mux"
"github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
@@ -18,6 +19,8 @@ import (
func InitPost() {
l4g.Debug(utils.T("api.post.init.debug"))
+ BaseRoutes.ApiRoot.Handle("/get_opengraph_metadata", ApiUserRequired(getOpenGraphMetadata)).Methods("POST")
+
BaseRoutes.NeedTeam.Handle("/posts/search", ApiUserRequiredActivity(searchPosts, true)).Methods("POST")
BaseRoutes.NeedTeam.Handle("/posts/flagged/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getFlaggedPosts)).Methods("GET")
BaseRoutes.NeedTeam.Handle("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET")
@@ -649,3 +652,29 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.FileInfosToJson(infos)))
}
}
+
+func getOpenGraphMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.StringInterfaceFromJson(r.Body)
+ og := opengraph.NewOpenGraph()
+
+ res, err := http.Get(props["url"].(string))
+ if err != nil {
+ writeOpenGraphToResponse(w, og)
+ return
+ }
+
+ if err := og.ProcessHTML(res.Body); err != nil {
+ writeOpenGraphToResponse(w, og)
+ return
+ }
+
+ writeOpenGraphToResponse(w, og)
+}
+
+func writeOpenGraphToResponse(w http.ResponseWriter, og *opengraph.OpenGraph) {
+ ogJson, err := og.ToJSON()
+ if err != nil {
+ w.Write([]byte(`{"url": ""}`))
+ }
+ w.Write(ogJson)
+}
diff --git a/api/post_test.go b/api/post_test.go
index 4d3ee80b5..d382786cc 100644
--- a/api/post_test.go
+++ b/api/post_test.go
@@ -5,6 +5,7 @@ package api
import (
"encoding/json"
+ "fmt"
"net/http"
"net/http/httptest"
"net/url"
@@ -1298,3 +1299,33 @@ func TestGetPermalinkTmp(t *testing.T) {
t.Fatal("should not be empty")
}
}
+
+func TestGetOpenGraphMetadata(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/og-data/" {
+ fmt.Fprintln(w, `
+ <html><head><meta property="og:type" content="article" />
+ <meta property="og:title" content="Test Title" />
+ <meta property="og:url" content="http://example.com/" />
+ </head><body></body></html>
+ `)
+ } else if r.URL.Path == "/no-og-data/" {
+ fmt.Fprintln(w, `<html><head></head><body></body></html>`)
+ }
+ }))
+
+ for _, data := range [](map[string]string){{"path": "/og-data/", "title": "Test Title"}, {"path": "/no-og-data/", "title": ""}} {
+ res, err := Client.DoApiPost("/get_opengraph_metadata", fmt.Sprintf("{\"url\":\"%s\"}", ts.URL+data["path"]))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ogData := model.StringInterfaceFromJson(res.Body)
+ if strings.Compare(ogData["title"].(string), data["title"]) != 0 {
+ t.Fatal(fmt.Sprintf("OG data title mismatch for path \"%s\". Expected title: \"%s\". Actual title: \"%s\"", data["path"], data["title"], ogData["title"]))
+ }
+ }
+}
diff --git a/glide.lock b/glide.lock
index 35dfce6bb..d91c3c347 100644
--- a/glide.lock
+++ b/glide.lock
@@ -1,5 +1,5 @@
-hash: a8fe20af467bdf57b944108bb00f8b410d73ad13a2cc5616ae2c410d510e5d90
-updated: 2016-11-23T19:38:17.926223307-05:00
+hash: 67ac70374ac7d1acb02736f628409e406cb55f343a260ccf78c4f324ec2df45a
+updated: 2016-12-25T21:59:49.671116871+05:30
imports:
- name: github.com/alecthomas/log4go
version: e5dc62318d9bd58682f1dceb53a4b24e8253682f
@@ -11,6 +11,10 @@ imports:
version: 96977cbd42e27be71f9f731db6634123de7e861a
- name: github.com/disintegration/imaging
version: 5b7e22645c93e3f3911b36b7d66bf8799f3eddfd
+- name: github.com/dyatlov/go-opengraph
+ version: 41a3523719dfbe7e8f853fbd4061867543db5270
+ subpackages:
+ - opengraph
- name: github.com/go-gorp/gorp
version: 0c9bc0918534d133cedb439a24adc7cbe66e4a9d
- name: github.com/go-ldap/ldap
@@ -136,6 +140,8 @@ imports:
version: 4971afdc2f162e82d185353533d3cf16188a9f4e
subpackages:
- context
+ - html
+ - html/atom
- publicsuffix
- name: golang.org/x/sys
version: 30237cf4eefd639b184d1f2cb77a581ea0be8947
diff --git a/glide.yaml b/glide.yaml
index 8846dccdb..ae0560199 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -88,3 +88,7 @@ import:
- package: github.com/prometheus/procfs
- package: github.com/spf13/cobra
- package: github.com/spf13/pflag
+- package: github.com/dyatlov/go-opengraph
+ version: 41a3523719dfbe7e8f853fbd4061867543db5270
+ subpackages:
+ - opengraph
diff --git a/vendor/github.com/dyatlov/go-opengraph/.gitignore b/vendor/github.com/dyatlov/go-opengraph/.gitignore
new file mode 100644
index 000000000..daf913b1b
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/.gitignore
@@ -0,0 +1,24 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
diff --git a/vendor/github.com/dyatlov/go-opengraph/LICENSE b/vendor/github.com/dyatlov/go-opengraph/LICENSE
new file mode 100644
index 000000000..854759ad2
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Vitaly Dyatlov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/vendor/github.com/dyatlov/go-opengraph/README.md b/vendor/github.com/dyatlov/go-opengraph/README.md
new file mode 100644
index 000000000..8c8e00212
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/README.md
@@ -0,0 +1,118 @@
+Go OpenGraph
+===
+
+Parses given html data into Facebook OpenGraph structure.
+
+To download and install this package run:
+
+`go get github.com/dyatlov/go-opengraph/opengraph`
+
+Methods:
+
+ * `NewOpenGraph()` - create a new OpenGraph instance
+ * `ProcessHTML(buffer io.Reader) error` - process given html into underlying data structure
+ * `ProcessMeta(metaAttrs map[string]string)` - add data to the structure based on meta attributes
+ * `ToJSON() (string, error)` - return JSON representation of data or error
+ * `String() string` - return JSON representation of structure
+
+Source docs: http://godoc.org/github.com/dyatlov/go-opengraph/opengraph
+
+If you just need to parse an OpenGraph data from HTML then method `ProcessHTML` is your needed one.
+
+Example:
+
+```go
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/dyatlov/go-opengraph/opengraph"
+)
+
+func main() {
+ html := `<html><head><meta property="og:type" content="article" />
+ <meta property="og:title" content="WordPress 4.3 &quot;Billie&quot;" />
+ <meta property="og:url" content="https://wordpress.org/news/2015/08/billie/" /></head><body></body></html>`
+
+ og := opengraph.NewOpenGraph()
+ err := og.ProcessHTML(strings.NewReader(html))
+
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ fmt.Printf("Type: %s\n", og.Type)
+ fmt.Printf("Title: %s\n", og.Title)
+ fmt.Printf("URL: %s\n", og.URL)
+ fmt.Printf("String/JSON Representation: %s\n", og)
+}
+```
+
+If you have your own parsing engine and just need an intelligent OpenGraph parsing, then `ProcessMeta` is the method you need.
+While using this method you don't need to reparse your parsed html again, just feed it with meta atributes as they appear and OpenGraph will be built based on the data.
+
+Example:
+
+```go
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/dyatlov/go-opengraph/opengraph"
+ "golang.org/x/net/html"
+)
+
+func main() {
+ h := `<html><head><meta property="og:type" content="article" />
+ <meta property="og:title" content="WordPress 4.3 &quot;Billie&quot;" />
+ <meta property="og:url" content="https://wordpress.org/news/2015/08/billie/" /></head><body></body></html>`
+
+ og := opengraph.NewOpenGraph()
+
+ doc, err := html.Parse(strings.NewReader(h))
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ var parseHead func(*html.Node)
+ parseHead = func(n *html.Node) {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type == html.ElementNode && c.Data == "meta" {
+ m := make(map[string]string)
+ for _, a := range c.Attr {
+ m[a.Key] = a.Val
+ }
+
+ og.ProcessMeta(m)
+ }
+ }
+ }
+
+ var f func(*html.Node)
+ f = func(n *html.Node) {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type == html.ElementNode {
+ if c.Data == "head" {
+ parseHead(c)
+ continue
+ } else if c.Data == "body" { // OpenGraph is only in head, so we don't need body
+ break
+ }
+ }
+ f(c)
+ }
+ }
+ f(doc)
+
+ fmt.Printf("Type: %s\n", og.Type)
+ fmt.Printf("Title: %s\n", og.Title)
+ fmt.Printf("URL: %s\n", og.URL)
+ fmt.Printf("String/JSON Representation: %s\n", og)
+}
+```
diff --git a/vendor/github.com/dyatlov/go-opengraph/examples/advanced.go b/vendor/github.com/dyatlov/go-opengraph/examples/advanced.go
new file mode 100644
index 000000000..e24b821e7
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/examples/advanced.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/dyatlov/go-opengraph/opengraph"
+ "golang.org/x/net/html"
+)
+
+func main() {
+ h := `<html><head><meta property="og:type" content="article" />
+ <meta property="og:title" content="WordPress 4.3 &quot;Billie&quot;" />
+ <meta property="og:url" content="https://wordpress.org/news/2015/08/billie/" /></head><body></body></html>`
+
+ og := opengraph.NewOpenGraph()
+
+ doc, err := html.Parse(strings.NewReader(h))
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ var parseHead func(*html.Node)
+ parseHead = func(n *html.Node) {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type == html.ElementNode && c.Data == "meta" {
+ m := make(map[string]string)
+ for _, a := range c.Attr {
+ m[a.Key] = a.Val
+ }
+
+ og.ProcessMeta(m)
+ }
+ }
+ }
+
+ var f func(*html.Node)
+ f = func(n *html.Node) {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type == html.ElementNode {
+ if c.Data == "head" {
+ parseHead(c)
+ continue
+ } else if c.Data == "body" { // OpenGraph is only in head, so we don't need body
+ break
+ }
+ }
+ f(c)
+ }
+ }
+ f(doc)
+
+ fmt.Printf("Type: %s\n", og.Type)
+ fmt.Printf("Title: %s\n", og.Title)
+ fmt.Printf("URL: %s\n", og.URL)
+ fmt.Printf("String/JSON Representation: %s\n", og)
+}
diff --git a/vendor/github.com/dyatlov/go-opengraph/examples/simple.go b/vendor/github.com/dyatlov/go-opengraph/examples/simple.go
new file mode 100644
index 000000000..fa128cd43
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/examples/simple.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/dyatlov/go-opengraph/opengraph"
+)
+
+func main() {
+ html := `<html><head><meta property="og:type" content="article" />
+ <meta property="og:title" content="WordPress 4.3 &quot;Billie&quot;" />
+ <meta property="og:url" content="https://wordpress.org/news/2015/08/billie/" /></head><body></body></html>`
+
+ og := opengraph.NewOpenGraph()
+ err := og.ProcessHTML(strings.NewReader(html))
+
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ fmt.Printf("Type: %s\n", og.Type)
+ fmt.Printf("Title: %s\n", og.Title)
+ fmt.Printf("URL: %s\n", og.URL)
+ fmt.Printf("String/JSON Representation: %s\n", og)
+}
diff --git a/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph.go b/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph.go
new file mode 100644
index 000000000..5468d86bb
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph.go
@@ -0,0 +1,329 @@
+package opengraph
+
+import (
+ "encoding/json"
+ "io"
+ "strconv"
+ "time"
+
+ "golang.org/x/net/html"
+ "golang.org/x/net/html/atom"
+)
+
+// Image defines Open Graph Image type
+type Image struct {
+ URL string `json:"url"`
+ SecureURL string `json:"secure_url"`
+ Type string `json:"type"`
+ Width uint64 `json:"width"`
+ Height uint64 `json:"height"`
+}
+
+// Video defines Open Graph Video type
+type Video struct {
+ URL string `json:"url"`
+ SecureURL string `json:"secure_url"`
+ Type string `json:"type"`
+ Width uint64 `json:"width"`
+ Height uint64 `json:"height"`
+}
+
+// Audio defines Open Graph Audio Type
+type Audio struct {
+ URL string `json:"url"`
+ SecureURL string `json:"secure_url"`
+ Type string `json:"type"`
+}
+
+// Article contain Open Graph Article structure
+type Article struct {
+ PublishedTime *time.Time `json:"published_time"`
+ ModifiedTime *time.Time `json:"modified_time"`
+ ExpirationTime *time.Time `json:"expiration_time"`
+ Section string `json:"section"`
+ Tags []string `json:"tags"`
+ Authors []*Profile `json:"authors"`
+}
+
+// Profile contains Open Graph Profile structure
+type Profile struct {
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ Username string `json:"username"`
+ Gender string `json:"gender"`
+}
+
+// Book contains Open Graph Book structure
+type Book struct {
+ ISBN string `json:"isbn"`
+ ReleaseDate *time.Time `json:"release_date"`
+ Tags []string `json:"tags"`
+ Authors []*Profile `json:"authors"`
+}
+
+// OpenGraph contains facebook og data
+type OpenGraph struct {
+ isArticle bool
+ isBook bool
+ isProfile bool
+ Type string `json:"type"`
+ URL string `json:"url"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Determiner string `json:"determiner"`
+ SiteName string `json:"site_name"`
+ Locale string `json:"locale"`
+ LocalesAlternate []string `json:"locales_alternate"`
+ Images []*Image `json:"images"`
+ Audios []*Audio `json:"audios"`
+ Videos []*Video `json:"videos"`
+ Article *Article `json:"article,omitempty"`
+ Book *Book `json:"book,omitempty"`
+ Profile *Profile `json:"profile,omitempty"`
+}
+
+// NewOpenGraph returns new instance of Open Graph structure
+func NewOpenGraph() *OpenGraph {
+ return &OpenGraph{}
+}
+
+// ToJSON a simple wrapper around json.Marshal
+func (og *OpenGraph) ToJSON() ([]byte, error) {
+ return json.Marshal(og)
+}
+
+// String return json representation of structure, or error string
+func (og *OpenGraph) String() string {
+ data, err := og.ToJSON()
+
+ if err != nil {
+ return err.Error()
+ }
+
+ return string(data[:])
+}
+
+// ProcessHTML parses given html from Reader interface and fills up OpenGraph structure
+func (og *OpenGraph) ProcessHTML(buffer io.Reader) error {
+ z := html.NewTokenizer(buffer)
+ for {
+ tt := z.Next()
+ switch tt {
+ case html.ErrorToken:
+ if z.Err() == io.EOF {
+ return nil
+ }
+ return z.Err()
+ case html.StartTagToken, html.SelfClosingTagToken, html.EndTagToken:
+ name, hasAttr := z.TagName()
+ if atom.Lookup(name) == atom.Body {
+ return nil // OpenGraph is only in head, so we don't need body
+ }
+ if atom.Lookup(name) != atom.Meta || !hasAttr {
+ continue
+ }
+ m := make(map[string]string)
+ var key, val []byte
+ for hasAttr {
+ key, val, hasAttr = z.TagAttr()
+ m[atom.String(key)] = string(val)
+ }
+ og.ProcessMeta(m)
+ }
+ }
+ return nil
+}
+
+// ProcessMeta processes meta attributes and adds them to Open Graph structure if they are suitable for that
+func (og *OpenGraph) ProcessMeta(metaAttrs map[string]string) {
+ switch metaAttrs["property"] {
+ case "og:description":
+ og.Description = metaAttrs["content"]
+ case "og:type":
+ og.Type = metaAttrs["content"]
+ switch og.Type {
+ case "article":
+ og.isArticle = true
+ case "book":
+ og.isBook = true
+ case "profile":
+ og.isProfile = true
+ }
+ case "og:title":
+ og.Title = metaAttrs["content"]
+ case "og:url":
+ og.URL = metaAttrs["content"]
+ case "og:determiner":
+ og.Determiner = metaAttrs["content"]
+ case "og:site_name":
+ og.SiteName = metaAttrs["content"]
+ case "og:locale":
+ og.Locale = metaAttrs["content"]
+ case "og:locale:alternate":
+ og.LocalesAlternate = append(og.LocalesAlternate, metaAttrs["content"])
+ case "og:image":
+ og.Images = append(og.Images, &Image{URL: metaAttrs["content"]})
+ case "og:image:url":
+ if len(og.Images) > 0 {
+ og.Images[len(og.Images)-1].URL = metaAttrs["content"]
+ }
+ case "og:image:secure_url":
+ if len(og.Images) > 0 {
+ og.Images[len(og.Images)-1].SecureURL = metaAttrs["content"]
+ }
+ case "og:image:type":
+ if len(og.Images) > 0 {
+ og.Images[len(og.Images)-1].Type = metaAttrs["content"]
+ }
+ case "og:image:width":
+ if len(og.Images) > 0 {
+ w, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
+ if err == nil {
+ og.Images[len(og.Images)-1].Width = w
+ }
+ }
+ case "og:image:height":
+ if len(og.Images) > 0 {
+ h, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
+ if err == nil {
+ og.Images[len(og.Images)-1].Height = h
+ }
+ }
+ case "og:video":
+ og.Videos = append(og.Videos, &Video{URL: metaAttrs["content"]})
+ case "og:video:url":
+ if len(og.Videos) > 0 {
+ og.Videos[len(og.Videos)-1].URL = metaAttrs["content"]
+ }
+ case "og:video:secure_url":
+ if len(og.Videos) > 0 {
+ og.Videos[len(og.Videos)-1].SecureURL = metaAttrs["content"]
+ }
+ case "og:video:type":
+ if len(og.Videos) > 0 {
+ og.Videos[len(og.Videos)-1].Type = metaAttrs["content"]
+ }
+ case "og:video:width":
+ if len(og.Videos) > 0 {
+ w, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
+ if err == nil {
+ og.Videos[len(og.Videos)-1].Width = w
+ }
+ }
+ case "og:video:height":
+ if len(og.Videos) > 0 {
+ h, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
+ if err == nil {
+ og.Videos[len(og.Videos)-1].Height = h
+ }
+ }
+ default:
+ if og.isArticle {
+ og.processArticleMeta(metaAttrs)
+ } else if og.isBook {
+ og.processBookMeta(metaAttrs)
+ } else if og.isProfile {
+ og.processProfileMeta(metaAttrs)
+ }
+ }
+}
+
+func (og *OpenGraph) processArticleMeta(metaAttrs map[string]string) {
+ if og.Article == nil {
+ og.Article = &Article{}
+ }
+ switch metaAttrs["property"] {
+ case "article:published_time":
+ t, err := time.Parse(time.RFC3339, metaAttrs["content"])
+ if err == nil {
+ og.Article.PublishedTime = &t
+ }
+ case "article:modified_time":
+ t, err := time.Parse(time.RFC3339, metaAttrs["content"])
+ if err == nil {
+ og.Article.ModifiedTime = &t
+ }
+ case "article:expiration_time":
+ t, err := time.Parse(time.RFC3339, metaAttrs["content"])
+ if err == nil {
+ og.Article.ExpirationTime = &t
+ }
+ case "article:secttion":
+ og.Article.Section = metaAttrs["content"]
+ case "article:tag":
+ og.Article.Tags = append(og.Article.Tags, metaAttrs["content"])
+ case "article:author:first_name":
+ if len(og.Article.Authors) == 0 {
+ og.Article.Authors = append(og.Article.Authors, &Profile{})
+ }
+ og.Article.Authors[len(og.Article.Authors)-1].FirstName = metaAttrs["content"]
+ case "article:author:last_name":
+ if len(og.Article.Authors) == 0 {
+ og.Article.Authors = append(og.Article.Authors, &Profile{})
+ }
+ og.Article.Authors[len(og.Article.Authors)-1].LastName = metaAttrs["content"]
+ case "article:author:username":
+ if len(og.Article.Authors) == 0 {
+ og.Article.Authors = append(og.Article.Authors, &Profile{})
+ }
+ og.Article.Authors[len(og.Article.Authors)-1].Username = metaAttrs["content"]
+ case "article:author:gender":
+ if len(og.Article.Authors) == 0 {
+ og.Article.Authors = append(og.Article.Authors, &Profile{})
+ }
+ og.Article.Authors[len(og.Article.Authors)-1].Gender = metaAttrs["content"]
+ }
+}
+
+func (og *OpenGraph) processBookMeta(metaAttrs map[string]string) {
+ if og.Book == nil {
+ og.Book = &Book{}
+ }
+ switch metaAttrs["property"] {
+ case "book:release_date":
+ t, err := time.Parse(time.RFC3339, metaAttrs["content"])
+ if err == nil {
+ og.Book.ReleaseDate = &t
+ }
+ case "book:isbn":
+ og.Book.ISBN = metaAttrs["content"]
+ case "book:tag":
+ og.Book.Tags = append(og.Book.Tags, metaAttrs["content"])
+ case "book:author:first_name":
+ if len(og.Book.Authors) == 0 {
+ og.Book.Authors = append(og.Book.Authors, &Profile{})
+ }
+ og.Book.Authors[len(og.Book.Authors)-1].FirstName = metaAttrs["content"]
+ case "book:author:last_name":
+ if len(og.Book.Authors) == 0 {
+ og.Book.Authors = append(og.Book.Authors, &Profile{})
+ }
+ og.Book.Authors[len(og.Book.Authors)-1].LastName = metaAttrs["content"]
+ case "book:author:username":
+ if len(og.Book.Authors) == 0 {
+ og.Book.Authors = append(og.Book.Authors, &Profile{})
+ }
+ og.Book.Authors[len(og.Book.Authors)-1].Username = metaAttrs["content"]
+ case "book:author:gender":
+ if len(og.Book.Authors) == 0 {
+ og.Book.Authors = append(og.Book.Authors, &Profile{})
+ }
+ og.Book.Authors[len(og.Book.Authors)-1].Gender = metaAttrs["content"]
+ }
+}
+
+func (og *OpenGraph) processProfileMeta(metaAttrs map[string]string) {
+ if og.Profile == nil {
+ og.Profile = &Profile{}
+ }
+ switch metaAttrs["property"] {
+ case "profile:first_name":
+ og.Profile.FirstName = metaAttrs["content"]
+ case "profile:last_name":
+ og.Profile.LastName = metaAttrs["content"]
+ case "profile:username":
+ og.Profile.Username = metaAttrs["content"]
+ case "profile:gender":
+ og.Profile.Gender = metaAttrs["content"]
+ }
+}
diff --git a/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph_test.go b/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph_test.go
new file mode 100644
index 000000000..6af7f25d2
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph_test.go
@@ -0,0 +1,131 @@
+package opengraph_test
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/dyatlov/go-opengraph/opengraph"
+)
+
+const html = `
+ <!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US">
+<head profile="http://gmpg.org/xfn/11">
+<meta charset="utf-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>WordPress &#8250; WordPress 4.3 &#8220;Billie&#8221;</title>
+
+<!-- Jetpack Open Graph Tags -->
+<meta property="og:type" content="article" />
+<meta property="og:title" content="WordPress 4.3 &quot;Billie&quot;" />
+<meta property="og:url" content="https://wordpress.org/news/2015/08/billie/" />
+<meta property="og:description" content="Version 4.3 of WordPress, named &quot;Billie&quot; in honor of jazz singer Billie Holiday, is available for download or update in your WordPress dashboard. New features in 4.3 make it even easier to format y..." />
+<meta property="article:published_time" content="2015-08-18T19:12:38+00:00" />
+<meta property="article:modified_time" content="2015-08-19T13:10:24+00:00" />
+<meta property="og:site_name" content="WordPress News" />
+<meta property="og:image" content="https://www.gravatar.com/avatar/2370ea5912750f4cb0f3c51ae1cbca55?d=mm&amp;s=180&amp;r=G" />
+<meta property="og:locale" content="en_US" />
+<meta name="twitter:site" content="@WordPress" />
+<meta name="twitter:card" content="summary" />
+<meta name="twitter:creator" content="@WordPress" />
+ `
+
+func BenchmarkOpenGraph_ProcessHTML(b *testing.B) {
+ og := opengraph.NewOpenGraph()
+ b.ReportAllocs()
+ b.SetBytes(int64(len(html)))
+ for i := 0; i < b.N; i++ {
+ if err := og.ProcessHTML(strings.NewReader(html)); err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestOpenGraphProcessHTML(t *testing.T) {
+ og := opengraph.NewOpenGraph()
+ err := og.ProcessHTML(strings.NewReader(html))
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if og.Type != "article" {
+ t.Error("type parsed incorrectly")
+ }
+
+ if len(og.Title) == 0 {
+ t.Error("title parsed incorrectly")
+ }
+
+ if len(og.URL) == 0 {
+ t.Error("url parsed incorrectly")
+ }
+
+ if len(og.Description) == 0 {
+ t.Error("description parsed incorrectly")
+ }
+
+ if len(og.Images) == 0 {
+ t.Error("images parsed incorrectly")
+ } else {
+ if len(og.Images[0].URL) == 0 {
+ t.Error("image url parsed incorrectly")
+ }
+ }
+
+ if len(og.Locale) == 0 {
+ t.Error("locale parsed incorrectly")
+ }
+
+ if len(og.SiteName) == 0 {
+ t.Error("site name parsed incorrectly")
+ }
+
+ if og.Article == nil {
+ t.Error("articles parsed incorrectly")
+ } else {
+ ev, _ := time.Parse(time.RFC3339, "2015-08-18T19:12:38+00:00")
+ if !og.Article.PublishedTime.Equal(ev) {
+ t.Error("article published time parsed incorrectly")
+ }
+ }
+}
+
+func TestOpenGraphProcessMeta(t *testing.T) {
+ og := opengraph.NewOpenGraph()
+
+ og.ProcessMeta(map[string]string{"property": "og:type", "content": "book"})
+
+ if og.Type != "book" {
+ t.Error("wrong og:type processing")
+ }
+
+ og.ProcessMeta(map[string]string{"property": "book:isbn", "content": "123456"})
+
+ if og.Book == nil {
+ t.Error("wrong book type processing")
+ } else {
+ if og.Book.ISBN != "123456" {
+ t.Error("wrong book isbn processing")
+ }
+ }
+
+ og.ProcessMeta(map[string]string{"property": "article:section", "content": "testsection"})
+
+ if og.Article != nil {
+ t.Error("article processed when it should not be")
+ }
+
+ og.ProcessMeta(map[string]string{"property": "book:author:first_name", "content": "John"})
+
+ if og.Book != nil {
+ if len(og.Book.Authors) == 0 {
+ t.Error("book author was not processed")
+ } else {
+ if og.Book.Authors[0].FirstName != "John" {
+ t.Error("author first name was processed incorrectly")
+ }
+ }
+ }
+}
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx
index 4cfcaa6cb..23e19f22f 100644
--- a/webapp/actions/global_actions.jsx
+++ b/webapp/actions/global_actions.jsx
@@ -596,3 +596,27 @@ export function redirectUserToDefaultTeam() {
browserHistory.push('/select_team');
}
}
+
+requestOpenGraphMetadata.openGraphMetadataOnGoingRequests = {}; // Format: {<url>: true}
+export function requestOpenGraphMetadata(url) {
+ const onself = requestOpenGraphMetadata;
+
+ if (!onself.openGraphMetadataOnGoingRequests[url]) {
+ onself.openGraphMetadataOnGoingRequests[url] = true;
+
+ Client.getOpenGraphMetadata(url,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIVED_OPEN_GRAPH_METADATA,
+ url,
+ data
+ });
+ delete onself.openGraphMetadataOnGoingRequests[url];
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getOpenGraphMetadata');
+ delete onself.openGraphMetadataOnGoingRequests[url];
+ }
+ );
+ }
+}
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx
index 639f2da2e..9f1bc926d 100644
--- a/webapp/client/client.jsx
+++ b/webapp/client/client.jsx
@@ -1767,6 +1767,16 @@ export default class Client {
end(this.handleResponse.bind(this, 'getFileInfosForPost', success, error));
}
+ getOpenGraphMetadata(url, success, error) {
+ request.
+ post(`${this.getBaseRoute()}/get_opengraph_metadata`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ send({url}).
+ end(this.handleResponse.bind(this, 'getOpenGraphMetadata', success, error));
+ }
+
// Routes for Files
uploadFile(file, filename, channelId, clientId, success, error) {
diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx
index 8ba3438a0..896002a6c 100644
--- a/webapp/components/post_view/components/post.jsx
+++ b/webapp/components/post_view/components/post.jsx
@@ -289,6 +289,7 @@ export default class Post extends React.Component {
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed}
isCommentMention={this.props.isCommentMention}
+ childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
/>
</div>
</div>
@@ -317,5 +318,6 @@ Post.propTypes = {
useMilitaryTime: React.PropTypes.bool.isRequired,
isFlagged: React.PropTypes.bool,
status: React.PropTypes.string,
- isBusy: React.PropTypes.bool
+ isBusy: React.PropTypes.bool,
+ childComponentDidUpdateFunction: React.PropTypes.func
};
diff --git a/webapp/components/post_view/components/post_attachment_oembed.jsx b/webapp/components/post_view/components/post_attachment_oembed.jsx
deleted file mode 100644
index 359c7cc35..000000000
--- a/webapp/components/post_view/components/post_attachment_oembed.jsx
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import React from 'react';
-
-export default class PostAttachmentOEmbed extends React.Component {
- constructor(props) {
- super(props);
- this.fetchData = this.fetchData.bind(this);
-
- this.isLoading = false;
- }
-
- componentWillMount() {
- this.setState({data: {}});
- }
-
- componentWillReceiveProps(nextProps) {
- if (nextProps.link !== this.props.link) {
- this.isLoading = false;
- this.fetchData(nextProps.link);
- }
- }
-
- componentDidMount() {
- this.fetchData(this.props.link);
- }
-
- fetchData(link) {
- if (!this.isLoading) {
- this.isLoading = true;
- let url = 'https://noembed.com/embed?nowrap=on';
- url += '&url=' + encodeURIComponent(link);
- url += '&maxheight=' + this.props.provider.height;
- return $.ajax({
- url,
- dataType: 'jsonp',
- success: (result) => {
- this.isLoading = false;
- if (result.error) {
- this.setState({data: {}});
- } else {
- this.setState({data: result});
- }
- },
- error: () => {
- this.setState({data: {}});
- }
- });
- }
- return null;
- }
-
- render() {
- let data = {};
- let content;
- if ($.isEmptyObject(this.state.data)) {
- content = <div style={{height: this.props.provider.height}}/>;
- } else {
- data = this.state.data;
- content = (
- <div
- style={{height: this.props.provider.height}}
- dangerouslySetInnerHTML={{__html: data.html}}
- />
- );
- }
-
- return (
- <div
- className='attachment attachment--oembed'
- ref='attachment'
- >
- <div className='attachment__content'>
- <div
- className={'clearfix attachment__container'}
- >
- <h1
- className='attachment__title'
- >
- <a
- className='attachment__title-link'
- href={data.url}
- target='_blank'
- rel='noopener noreferrer'
- >
- {data.title}
- </a>
- </h1>
- <div >
- <div
- className={'attachment__body attachment__body--no_thumb'}
- >
- {content}
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
-
-PostAttachmentOEmbed.propTypes = {
- link: React.PropTypes.string.isRequired,
- provider: React.PropTypes.object.isRequired
-};
diff --git a/webapp/components/post_view/components/post_attachment_opengraph.jsx b/webapp/components/post_view/components/post_attachment_opengraph.jsx
new file mode 100644
index 000000000..20beaed51
--- /dev/null
+++ b/webapp/components/post_view/components/post_attachment_opengraph.jsx
@@ -0,0 +1,212 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import OpenGraphStore from 'stores/opengraph_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+import * as CommonUtils from 'utils/commons.jsx';
+import {requestOpenGraphMetadata} from 'actions/global_actions.jsx';
+
+export default class PostAttachmentOpenGraph extends React.Component {
+ constructor(props) {
+ super(props);
+ this.imageDimentions = { // Image dimentions in pixels.
+ height: 150,
+ width: 150
+ };
+ this.maxDescriptionLength = 300;
+ this.descriptionEllipsis = '...';
+ this.fetchData = this.fetchData.bind(this);
+ this.onOpenGraphMetadataChange = this.onOpenGraphMetadataChange.bind(this);
+ this.toggleImageVisibility = this.toggleImageVisibility.bind(this);
+ this.onImageLoad = this.onImageLoad.bind(this);
+ }
+
+ componentWillMount() {
+ this.setState({
+ data: {},
+ imageLoaded: false,
+ imageVisible: this.props.previewCollapsed.startsWith('false')
+ });
+ this.fetchData(this.props.link);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.setState({imageVisible: nextProps.previewCollapsed.startsWith('false')});
+ if (!Utils.areObjectsEqual(nextProps.link, this.props.link)) {
+ this.fetchData(nextProps.link);
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (nextState.imageVisible !== this.state.imageVisible) {
+ return true;
+ }
+ if (nextState.imageLoaded !== this.state.imageLoaded) {
+ return true;
+ }
+ if (!Utils.areObjectsEqual(nextState.data, this.state.data)) {
+ return true;
+ }
+ return false;
+ }
+
+ componentDidMount() {
+ OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange);
+ }
+
+ componentDidUpdate() {
+ if (this.props.childComponentDidUpdateFunction) {
+ this.props.childComponentDidUpdateFunction();
+ }
+ }
+
+ componentWillUnmount() {
+ OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange);
+ }
+
+ onOpenGraphMetadataChange(url) {
+ if (url === this.props.link) {
+ this.fetchData(url);
+ }
+ }
+
+ fetchData(url) {
+ const data = OpenGraphStore.getOgInfo(url);
+ this.setState({data, imageLoaded: false});
+ if (Utils.isEmptyObject(data)) {
+ requestOpenGraphMetadata(url);
+ }
+ }
+
+ getBestImageUrl() {
+ const nearestPointData = CommonUtils.getNearestPoint(this.imageDimentions, this.state.data.images, 'width', 'height');
+
+ const bestImage = nearestPointData.nearestPoint;
+ const bestImageLte = nearestPointData.nearestPointLte; // Best image <= 150px height and width
+
+ let finalBestImage;
+
+ if (
+ !Utils.isEmptyObject(bestImageLte) &&
+ bestImageLte.height <= this.imageDimentions.height &&
+ bestImageLte.width <= this.imageDimentions.width
+ ) {
+ finalBestImage = bestImageLte;
+ } else {
+ finalBestImage = bestImage;
+ }
+
+ return finalBestImage.secure_url || finalBestImage.url;
+ }
+
+ toggleImageVisibility() {
+ this.setState({imageVisible: !this.state.imageVisible});
+ }
+
+ onImageLoad() {
+ this.setState({imageLoaded: true});
+ }
+
+ loadImage(src) {
+ const img = new Image();
+ img.onload = this.onImageLoad;
+ img.src = src;
+ }
+
+ imageToggleAnchoreTag(imageUrl) {
+ if (imageUrl) {
+ return (
+ <a
+ className={'post__embed-visibility'}
+ data-expanded={this.state.imageVisible}
+ aria-label='Toggle Embed Visibility'
+ onClick={this.toggleImageVisibility}
+ />
+ );
+ }
+ return null;
+ }
+
+ imageTag(imageUrl) {
+ if (imageUrl && this.state.imageVisible) {
+ return (
+ <img
+ className={this.state.imageLoaded ? 'attachment__image' : 'attachment__image loading'}
+ src={this.state.imageLoaded ? imageUrl : null}
+ />
+ );
+ }
+ return null;
+ }
+
+ render() {
+ if (Utils.isEmptyObject(this.state.data) || Utils.isEmptyObject(this.state.data.description)) {
+ return null;
+ }
+
+ const data = this.state.data;
+ const imageUrl = this.getBestImageUrl();
+ var description = data.description;
+
+ if (description.length > this.maxDescriptionLength) {
+ description = description.substring(0, this.maxDescriptionLength - this.descriptionEllipsis.length) + this.descriptionEllipsis;
+ }
+
+ if (imageUrl && this.state.imageVisible) {
+ this.loadImage(imageUrl);
+ }
+
+ return (
+ <div
+ className='attachment attachment--oembed'
+ ref='attachment'
+ >
+ <div className='attachment__content'>
+ <div
+ className={'clearfix attachment__container'}
+ >
+ <span className='sitename'>{data.site_name}</span>
+ <h1
+ className='attachment__title has-link'
+ >
+ <a
+ className='attachment__title-link'
+ href={data.url || this.props.link}
+ target='_blank'
+ rel='noopener noreferrer'
+ title={data.title || data.url || this.props.link}
+ >
+ {data.title || data.url || this.props.link}
+ </a>
+ </h1>
+ <div >
+ <div
+ className={'attachment__body attachment__body--no_thumb'}
+ >
+ <div>
+ <div>
+ {description} &nbsp;
+ {this.imageToggleAnchoreTag(imageUrl)}
+ </div>
+ {this.imageTag(imageUrl)}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+PostAttachmentOpenGraph.defaultProps = {
+ previewCollapsed: 'false'
+};
+
+PostAttachmentOpenGraph.propTypes = {
+ link: React.PropTypes.string.isRequired,
+ childComponentDidUpdateFunction: React.PropTypes.func,
+ previewCollapsed: React.PropTypes.string
+};
diff --git a/webapp/components/post_view/components/post_body.jsx b/webapp/components/post_view/components/post_body.jsx
index 60e682e8d..10c24aab2 100644
--- a/webapp/components/post_view/components/post_body.jsx
+++ b/webapp/components/post_view/components/post_body.jsx
@@ -188,6 +188,7 @@ export default class PostBody extends React.Component {
message={messageWrapper}
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed}
+ childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
/>
);
}
@@ -221,5 +222,6 @@ PostBody.propTypes = {
handleCommentClick: React.PropTypes.func.isRequired,
compactDisplay: React.PropTypes.bool,
previewCollapsed: React.PropTypes.string,
- isCommentMention: React.PropTypes.bool
+ isCommentMention: React.PropTypes.bool,
+ childComponentDidUpdateFunction: React.PropTypes.func
};
diff --git a/webapp/components/post_view/components/post_body_additional_content.jsx b/webapp/components/post_view/components/post_body_additional_content.jsx
index e6c1f3b06..cad618de0 100644
--- a/webapp/components/post_view/components/post_body_additional_content.jsx
+++ b/webapp/components/post_view/components/post_body_additional_content.jsx
@@ -2,12 +2,11 @@
// See License.txt for license information.
import PostAttachmentList from './post_attachment_list.jsx';
-import PostAttachmentOEmbed from './post_attachment_oembed.jsx';
+import PostAttachmentOpenGraph from './post_attachment_opengraph.jsx';
import PostImage from './post_image.jsx';
import YoutubeVideo from 'components/youtube_video.jsx';
import Constants from 'utils/constants.jsx';
-import OEmbedProviders from './providers.json';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
@@ -17,7 +16,6 @@ export default class PostBodyAdditionalContent extends React.Component {
super(props);
this.getSlackAttachment = this.getSlackAttachment.bind(this);
- this.getOEmbedProvider = this.getOEmbedProvider.bind(this);
this.generateToggleableEmbed = this.generateToggleableEmbed.bind(this);
this.generateStaticEmbed = this.generateStaticEmbed.bind(this);
this.toggleEmbedVisibility = this.toggleEmbedVisibility.bind(this);
@@ -72,18 +70,6 @@ export default class PostBodyAdditionalContent extends React.Component {
);
}
- getOEmbedProvider(link) {
- for (let i = 0; i < OEmbedProviders.length; i++) {
- for (let j = 0; j < OEmbedProviders[i].patterns.length; j++) {
- if (link.match(OEmbedProviders[i].patterns[j])) {
- return OEmbedProviders[i];
- }
- }
- }
-
- return null;
- }
-
isLinkImage(link) {
const regex = /.+\/(.+\.(?:jpg|gif|bmp|png|jpeg))(?:\?.*)?$/i;
const match = link.match(regex);
@@ -152,38 +138,20 @@ export default class PostBodyAdditionalContent extends React.Component {
}
const link = Utils.extractFirstLink(this.props.post.message);
- if (!link) {
- return null;
- }
-
- if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) {
- const provider = this.getOEmbedProvider(link);
-
- if (provider) {
- return (
- <PostAttachmentOEmbed
- provider={provider}
- link={link}
- />
- );
- }
+ if (link && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) {
+ return (
+ <PostAttachmentOpenGraph
+ link={link}
+ childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
+ previewCollapsed={this.props.previewCollapsed}
+ />
+ );
}
return null;
}
render() {
- const staticEmbed = this.generateStaticEmbed();
-
- if (staticEmbed) {
- return (
- <div>
- {this.props.message}
- {staticEmbed}
- </div>
- );
- }
-
if (this.isLinkToggleable() && !this.state.linkLoadError) {
const messageWithToggle = [];
@@ -224,6 +192,17 @@ export default class PostBodyAdditionalContent extends React.Component {
);
}
+ const staticEmbed = this.generateStaticEmbed();
+
+ if (staticEmbed) {
+ return (
+ <div>
+ {this.props.message}
+ {staticEmbed}
+ </div>
+ );
+ }
+
return this.props.message;
}
}
@@ -235,5 +214,6 @@ PostBodyAdditionalContent.propTypes = {
post: React.PropTypes.object.isRequired,
message: React.PropTypes.element.isRequired,
compactDisplay: React.PropTypes.bool,
- previewCollapsed: React.PropTypes.string
+ previewCollapsed: React.PropTypes.string,
+ childComponentDidUpdateFunction: React.PropTypes.func
};
diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx
index e3724b688..7550db348 100644
--- a/webapp/components/post_view/components/post_list.jsx
+++ b/webapp/components/post_view/components/post_list.jsx
@@ -45,6 +45,7 @@ export default class PostList extends React.Component {
this.scrollToBottom = this.scrollToBottom.bind(this);
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.childComponentDidUpdate = this.childComponentDidUpdate.bind(this);
this.jumpToPostNode = null;
this.wasAtBottom = true;
@@ -347,6 +348,7 @@ export default class PostList extends React.Component {
isFlagged={isFlagged}
status={status}
isBusy={this.props.isBusy}
+ childComponentDidUpdateFunction={this.childComponentDidUpdate}
/>
);
@@ -492,6 +494,12 @@ export default class PostList extends React.Component {
);
}
+ checkAndUpdateScrolling() {
+ if (this.props.postList != null && this.refs.postlist) {
+ this.updateScrolling();
+ }
+ }
+
componentDidMount() {
if (this.props.postList != null) {
this.updateScrolling();
@@ -509,9 +517,11 @@ export default class PostList extends React.Component {
}
componentDidUpdate() {
- if (this.props.postList != null && this.refs.postlist) {
- this.updateScrolling();
- }
+ this.checkAndUpdateScrolling();
+ }
+
+ childComponentDidUpdate() {
+ this.checkAndUpdateScrolling();
}
render() {
diff --git a/webapp/components/post_view/components/providers.json b/webapp/components/post_view/components/providers.json
deleted file mode 100644
index b5899c225..000000000
--- a/webapp/components/post_view/components/providers.json
+++ /dev/null
@@ -1,376 +0,0 @@
-[
- {
- "patterns": [
- "http://(?:www\\.)?xkcd\\.com/\\d+/?"
- ],
- "name": "XKCD",
- "height": 110
- },
- {
- "patterns": [
- "https?://soundcloud.com/.*/.*"
- ],
- "name": "SoundCloud",
- "height": 140
- },
- {
- "patterns": [
- "https?://(?:www\\.)?flickr\\.com/.*",
- "https?://flic\\.kr/p/[a-zA-Z0-9]+"
- ],
- "name": "Flickr",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.ted\\.com/talks/.+\\.html"
- ],
- "name": "TED",
- "height": 110
- },
- {
- "patterns": [
- "http://(?:www\\.)?theverge\\.com/\\d{4}/\\d{1,2}/\\d{1,2}/\\d+/[^/]+/?$"
- ],
- "name": "The Verge",
- "height": 110
- },
- {
- "patterns": [
- "http://.*\\.viddler\\.com/.*"
- ],
- "name": "Viddler",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www\\.)?avclub\\.com/article/[^/]+/?$"
- ],
- "name": "The AV Club",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www\\.)?wired\\.com/([^/]+/)?\\d+/\\d+/[^/]+/?$"
- ],
- "name": "Wired",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.theonion\\.com/articles/[^/]+/?"
- ],
- "name": "The Onion",
- "height": 110
- },
- {
- "patterns": [
- "http://yfrog\\.com/[0-9a-zA-Z]+/?$"
- ],
- "name": "YFrog",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.duffelblog\\.com/\\d{4}/\\d{1,2}/[^/]+/?$"
- ],
- "name": "The Duffel Blog",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.clickhole\\.com/article/[^/]+/?"
- ],
- "name": "Clickhole",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www.)?skitch.com/([^/]+)/[^/]+/.+",
- "http://skit.ch/[^/]+"
- ],
- "name": "Skitch",
- "height": 110
- },
- {
- "patterns": [
- "https?://(alpha|posts|photos)\\.app\\.net/.*"
- ],
- "name": "ADN",
- "height": 110
- },
- {
- "patterns": [
- "https?://gist\\.github\\.com/(?:[-0-9a-zA-Z]+/)?([0-9a-fA-f]+)"
- ],
- "name": "Gist",
- "height": 110
- },
- {
- "patterns": [
- "https?://www\\.(dropbox\\.com/s/.+\\.(?:jpg|png|gif))",
- "https?://db\\.tt/[a-zA-Z0-9]+"
- ],
- "name": "Dropbox",
- "height": 110
- },
- {
- "patterns": [
- "https?://[^\\.]+\\.wikipedia\\.org/wiki/(?!Talk:)[^#]+(?:#(.+))?"
- ],
- "name": "Wikipedia",
- "height": 110
- },
- {
- "patterns": [
- "http://www.traileraddict.com/trailer/[^/]+/trailer"
- ],
- "name": "TrailerAddict",
- "height": 110
- },
- {
- "patterns": [
- "http://lockerz\\.com/[sd]/\\d+"
- ],
- "name": "Lockerz",
- "height": 110
- },
- {
- "patterns": [
- "http://gifuk\\.com/s/[0-9a-f]{16}"
- ],
- "name": "GIFUK",
- "height": 110
- },
- {
- "patterns": [
- "http://trailers\\.apple\\.com/trailers/[^/]+/[^/]+"
- ],
- "name": "iTunes Movie Trailers",
- "height": 110
- },
- {
- "patterns": [
- "http://gfycat\\.com/([a-zA-Z]+)"
- ],
- "name": "Gfycat",
- "height": 110
- },
- {
- "patterns": [
- "http://bash\\.org/\\?(\\d+)"
- ],
- "name": "Bash.org",
- "height": 110
- },
- {
- "patterns": [
- "http://arstechnica\\.com/[^/]+/\\d+/\\d+/[^/]+/?$"
- ],
- "name": "Ars Technica",
- "height": 110
- },
- {
- "patterns": [
- "http://imgur\\.com/gallery/[0-9a-zA-Z]+"
- ],
- "name": "Imgur",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.asciiartfarts\\.com/[0-9]+\\.html"
- ],
- "name": "ASCII Art Farts",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.monoprice\\.com/products/product\\.asp\\?.*p_id=\\d+"
- ],
- "name": "Monoprice",
- "height": 110
- },
- {
- "patterns": [
- "http://boingboing\\.net/\\d{4}/\\d{2}/\\d{2}/[^/]+\\.html"
- ],
- "name": "Boing Boing",
- "height": 110
- },
- {
- "patterns": [
- "https?://github\\.com/([^/]+)/([^/]+)/commit/(.+)",
- "http://git\\.io/[_0-9a-zA-Z]+"
- ],
- "name": "Github Commit",
- "height": 110
- },
- {
- "patterns": [
- "https?://open\\.spotify\\.com/(track|album)/([0-9a-zA-Z]{22})"
- ],
- "name": "Spotify",
- "height": 110
- },
- {
- "patterns": [
- "https?://path\\.com/p/([0-9a-zA-Z]+)$"
- ],
- "name": "Path",
- "height": 110
- },
- {
- "patterns": [
- "http://www.funnyordie.com/videos/[^/]+/.+"
- ],
- "name": "Funny or Die",
- "height": 110
- },
- {
- "patterns": [
- "http://(?:www\\.)?twitpic\\.com/([^/]+)"
- ],
- "name": "Twitpic",
- "height": 110
- },
- {
- "patterns": [
- "https?://www\\.giantbomb\\.com/videos/[^/]+/\\d+-\\d+/?"
- ],
- "name": "GiantBomb",
- "height": 110
- },
- {
- "patterns": [
- "http://(?:www\\.)?beeradvocate\\.com/beer/profile/\\d+/\\d+"
- ],
- "name": "Beer Advocate",
- "height": 110
- },
- {
- "patterns": [
- "http://(?:www\\.)?imdb.com/title/(tt\\d+)"
- ],
- "name": "IMDB",
- "height": 110
- },
- {
- "patterns": [
- "http://cl\\.ly/(?:image/)?[0-9a-zA-Z]+/?$"
- ],
- "name": "CloudApp",
- "height": 110
- },
- {
- "patterns": [
- "http://clyp\\.it/.*"
- ],
- "name": "Clyp",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.hulu\\.com/watch/.*"
- ],
- "name": "Hulu",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www|mobile\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/?$",
- "https?://t\\.co/[a-zA-Z0-9]+"
- ],
- "name": "Twitter",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www\\.)?vimeo\\.com/.+"
- ],
- "name": "Vimeo",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.amazon\\.com/(?:.+/)?[gd]p/(?:product/)?(?:tags-on-product/)?([a-zA-Z0-9]+)",
- "http://amzn\\.com/([^/]+)"
- ],
- "name": "Amazon",
- "height": 110
- },
- {
- "patterns": [
- "http://qik\\.com/video/.*"
- ],
- "name": "Qik",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.rdio\\.com/artist/[^/]+/album/[^/]+/?",
- "http://www\\.rdio\\.com/artist/[^/]+/album/[^/]+/track/[^/]+/?",
- "http://www\\.rdio\\.com/people/[^/]+/playlists/\\d+/[^/]+"
- ],
- "name": "Rdio",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.slideshare\\.net/.*/.*"
- ],
- "name": "SlideShare",
- "height": 110
- },
- {
- "patterns": [
- "http://imgur\\.com/([0-9a-zA-Z]+)$"
- ],
- "name": "Imgur",
- "height": 110
- },
- {
- "patterns": [
- "https?://instagr(?:\\.am|am\\.com)/p/.+"
- ],
- "name": "Instagram",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.twitlonger\\.com/show/[a-zA-Z0-9]+",
- "http://tl\\.gd/[^/]+"
- ],
- "name": "Twitlonger",
- "height": 110
- },
- {
- "patterns": [
- "https?://vine.co/v/[a-zA-Z0-9]+"
- ],
- "name": "Vine",
- "height": 490
- },
- {
- "patterns": [
- "http://www\\.urbandictionary\\.com/define\\.php\\?term=.+"
- ],
- "name": "Urban Dictionary",
- "height": 110
- },
- {
- "patterns": [
- "http://picplz\\.com/user/[^/]+/pic/[^/]+"
- ],
- "name": "Picplz",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/photo/\\d+(?:/large|/)?$",
- "https?://pic\\.twitter\\.com/.+"
- ],
- "name": "Twitter",
- "height": 110
- }
-]
diff --git a/webapp/sass/layout/_webhooks.scss b/webapp/sass/layout/_webhooks.scss
index 99a82f00e..904c50ccc 100644
--- a/webapp/sass/layout/_webhooks.scss
+++ b/webapp/sass/layout/_webhooks.scss
@@ -68,6 +68,9 @@
&.attachment__container--danger {
border-left-color: #e40303;
}
+ .sitename {
+ color: #A3A3A3;
+ }
}
.attachment__body {
@@ -80,6 +83,14 @@
&.attachment__body--no_thumb {
width: 100%;
}
+ .attachment__image {
+ margin-bottom: 0;
+ max-height: 150px;
+ max-width: 150px;
+ &.loading {
+ height: 150px;
+ }
+ }
}
.attachment__text p:last-of-type {
@@ -103,6 +114,13 @@
line-height: 18px;
margin: 5px 0;
padding: 0;
+
+ &.has-link {
+ color: #2f81b7;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
}
.attachment-link-more {
@@ -144,4 +162,4 @@
}
}
}
-} \ No newline at end of file
+}
diff --git a/webapp/stores/opengraph_store.jsx b/webapp/stores/opengraph_store.jsx
new file mode 100644
index 000000000..4ad156df0
--- /dev/null
+++ b/webapp/stores/opengraph_store.jsx
@@ -0,0 +1,68 @@
+import EventEmitter from 'events';
+
+import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
+import Constants from 'utils/constants.jsx';
+
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'change';
+const URL_DATA_CHANGE_EVENT = 'url_data_change';
+
+class OpenGraphStoreClass extends EventEmitter {
+ constructor() {
+ super();
+ this.ogDataObject = {}; // Format: {<url>: <data-object>}
+ }
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ emitUrlDataChange(url) {
+ this.emit(URL_DATA_CHANGE_EVENT, url);
+ }
+
+ addUrlDataChangeListener(callback) {
+ this.on(URL_DATA_CHANGE_EVENT, callback);
+ }
+
+ removeUrlDataChangeListener(callback) {
+ this.removeListener(URL_DATA_CHANGE_EVENT, callback);
+ }
+
+ storeOgInfo(url, ogInfo) {
+ this.ogDataObject[url] = ogInfo;
+ }
+
+ getOgInfo(url) {
+ return this.ogDataObject[url];
+ }
+}
+
+var OpenGraphStore = new OpenGraphStoreClass();
+
+// Not expecting more that `Constants.POST_CHUNK_SIZE` post previews rendered at a time
+OpenGraphStore.setMaxListeners(Constants.POST_CHUNK_SIZE);
+
+OpenGraphStore.dispatchToken = AppDispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECIVED_OPEN_GRAPH_METADATA:
+ OpenGraphStore.storeOgInfo(action.url, action.data);
+ OpenGraphStore.emitUrlDataChange(action.url);
+ OpenGraphStore.emitChange();
+ break;
+ default:
+ }
+});
+
+export default OpenGraphStore;
diff --git a/webapp/tests/utils_get_nearest_point.test.jsx b/webapp/tests/utils_get_nearest_point.test.jsx
new file mode 100644
index 000000000..b0b0a2e0e
--- /dev/null
+++ b/webapp/tests/utils_get_nearest_point.test.jsx
@@ -0,0 +1,35 @@
+import assert from 'assert';
+import * as CommonUtils from 'utils/commons.jsx';
+
+describe('CommonUtils.getNearestPoint', function() {
+ this.timeout(10000);
+ it('should return nearest point', function() {
+ for (const data of [
+ {
+ points: [{x: 30, y: 40}, {x: 50, y: 50}, {x: 100, y: 2}, {x: 500, y: 200}, {x: 110, y: 20}, {x: 10, y: 20}],
+ pivotPoint: {x: 10, y: 20},
+ nearestPoint: {x: 10, y: 20},
+ nearestPointLte: {x: 10, y: 20}
+ },
+ {
+ points: [{x: 50, y: 50}, {x: 100, y: 2}, {x: 500, y: 200}, {x: 110, y: 20}, {x: 100, y: 90}, {x: 30, y: 40}],
+ pivotPoint: {x: 10, y: 20},
+ nearestPoint: {x: 30, y: 40},
+ nearestPointLte: {}
+ },
+ {
+ points: [{x: 50, y: 50}, {x: 1, y: 1}, {x: 15, y: 25}, {x: 100, y: 2}, {x: 500, y: 200}, {x: 110, y: 20}],
+ pivotPoint: {x: 10, y: 20},
+ nearestPoint: {x: 15, y: 25},
+ nearestPointLte: {x: 1, y: 1}
+ }
+ ]) {
+ const nearestPointData = CommonUtils.getNearestPoint(data.pivotPoint, data.points);
+
+ assert.equal(nearestPointData.nearestPoint.x, data.nearestPoint.x);
+ assert.equal(nearestPointData.nearestPoint.y, data.nearestPoint.y);
+ assert.equal(nearestPointData.nearestPointLte.x, data.nearestPointLte.x);
+ assert.equal(nearestPointData.nearestPointLte.y, data.nearestPointLte.y);
+ }
+ });
+});
diff --git a/webapp/utils/commons.jsx b/webapp/utils/commons.jsx
new file mode 100644
index 000000000..1888869dc
--- /dev/null
+++ b/webapp/utils/commons.jsx
@@ -0,0 +1,36 @@
+export function getDistanceBW2Points(point1, point2, xAttr = 'x', yAttr = 'y') {
+ return Math.sqrt(Math.pow(point1[xAttr] - point2[xAttr], 2) + Math.pow(point1[yAttr] - point2[yAttr], 2));
+}
+
+/**
+ * Funtion to return nearest point of given pivot point.
+ * It return two points one nearest and other nearest but having both coorditanes smaller than the given point's coordinates.
+ */
+export function getNearestPoint(pivotPoint, points, xAttr = 'x', yAttr = 'y') {
+ var nearestPoint = {};
+ var nearestPointLte = {}; // Nearest point smaller than or equal to point
+ for (const point of points) {
+ if (typeof nearestPoint[xAttr] === 'undefined' || typeof nearestPoint[yAttr] === 'undefined') {
+ nearestPoint = point;
+ } else if (getDistanceBW2Points(point, pivotPoint, xAttr, yAttr) < getDistanceBW2Points(nearestPoint, pivotPoint, xAttr, yAttr)) {
+ // Check for bestImage
+ nearestPoint = point;
+ }
+
+ if (typeof nearestPointLte[xAttr] === 'undefined' || typeof nearestPointLte[yAttr] === 'undefined') {
+ if (point[xAttr] <= pivotPoint[xAttr] && point[yAttr] <= pivotPoint[yAttr]) {
+ nearestPointLte = point;
+ }
+ } else if (
+ // Check for bestImageLte
+ getDistanceBW2Points(point, pivotPoint, xAttr, yAttr) < getDistanceBW2Points(nearestPointLte, pivotPoint, xAttr, yAttr) &&
+ point[xAttr] <= pivotPoint[xAttr] && point[yAttr] <= pivotPoint[yAttr]
+ ) {
+ nearestPointLte = point;
+ }
+ }
+ return {
+ nearestPoint,
+ nearestPointLte
+ };
+}
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 6377f27f2..b1c188d89 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -146,6 +146,9 @@ export const ActionTypes = keyMirror({
RECEIVED_LOCALE: null,
+ UPDATE_OPEN_GRAPH_METADATA: null,
+ RECIVED_OPEN_GRAPH_METADATA: null,
+
SHOW_SEARCH: null,
USER_TYPING: null,
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index 9654ff605..a0aecbdb3 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -1324,3 +1324,15 @@ export function handleFormattedTextClick(e) {
browserHistory.push('/' + TeamStore.getCurrent().name + '/channels/' + channelMentionAttribute.value);
}
}
+
+export function isEmptyObject(object) {
+ if (!object) {
+ return true;
+ }
+
+ if (Object.keys(object).length === 0) {
+ return true;
+ }
+
+ return false;
+}