├── .browserslistrc ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.json ├── composer.json ├── composer.lock ├── dist ├── css │ ├── styles-bs.css │ ├── styles-bs.min.css │ ├── styles.css │ ├── styles.min.css │ ├── variables.css │ └── variables.min.css └── js │ ├── dispatchEvent.js │ ├── dispatchEvent.js.map │ ├── fetchData.js │ ├── fetchData.js.map │ ├── getTweetId.js │ ├── getTweetId.js.map │ ├── processTweets.js │ ├── processTweets.js.map │ ├── ready.js │ ├── ready.js.map │ ├── renderTweet.js │ ├── renderTweet.js.map │ ├── scripts.js │ └── scripts.js.map ├── gulpfile.js ├── images ├── assets │ ├── banner-1544x500.png │ ├── banner-772x250.png │ ├── icon-128x128.png │ ├── icon-256x256.png │ ├── screenshot-1.png │ └── screenshot-2.png ├── banner-1544x500.psd ├── blog │ ├── performance.png │ ├── test-demo │ │ ├── desktop-off.png │ │ ├── desktop-on.png │ │ ├── mobile-off.png │ │ └── mobile-on.png │ └── test-fourtonfish │ │ ├── desktop-off.png │ │ ├── desktop-on.png │ │ ├── mobile-off.png │ │ └── mobile-on.png └── thumbnail │ ├── tweet-embeds-bw-tint.png │ ├── tweet-embeds-bw.png │ └── tweet-embeds.png ├── includes ├── Cleanup.php ├── Database.php ├── Embed_Tweets.php ├── Enqueue_Assets.php ├── Helpers.php ├── Media_Proxy.php ├── Settings.php ├── Site_Info.php └── simple_html_dom.php ├── index.php ├── package-lock.json ├── package.json ├── readme.txt ├── src └── frontend │ ├── scripts │ ├── dispatchEvent.js │ ├── fetchData.js │ ├── getTweetId.js │ ├── processTweets.js │ ├── ready.js │ ├── renderTweet.js │ └── scripts.js │ └── styles │ ├── bootstrap │ ├── _alert.scss │ ├── _badge.scss │ ├── _breadcrumb.scss │ ├── _button-group.scss │ ├── _buttons.scss │ ├── _card.scss │ ├── _carousel.scss │ ├── _close.scss │ ├── _code.scss │ ├── _custom-forms.scss │ ├── _dropdown.scss │ ├── _forms.scss │ ├── _functions.scss │ ├── _grid.scss │ ├── _images.scss │ ├── _input-group.scss │ ├── _jumbotron.scss │ ├── _list-group.scss │ ├── _media.scss │ ├── _mixins.scss │ ├── _modal.scss │ ├── _nav.scss │ ├── _navbar.scss │ ├── _pagination.scss │ ├── _popover.scss │ ├── _print.scss │ ├── _progress.scss │ ├── _reboot.scss │ ├── _root.scss │ ├── _spinners.scss │ ├── _tables.scss │ ├── _toasts.scss │ ├── _tooltip.scss │ ├── _transitions.scss │ ├── _type.scss │ ├── _utilities.scss │ ├── _variables.scss │ ├── bootstrap-grid.scss │ ├── bootstrap-reboot.scss │ ├── bootstrap.scss │ ├── mixins │ │ ├── _alert.scss │ │ ├── _background-variant.scss │ │ ├── _badge.scss │ │ ├── _border-radius.scss │ │ ├── _box-shadow.scss │ │ ├── _breakpoints.scss │ │ ├── _buttons.scss │ │ ├── _caret.scss │ │ ├── _clearfix.scss │ │ ├── _deprecate.scss │ │ ├── _float.scss │ │ ├── _forms.scss │ │ ├── _gradients.scss │ │ ├── _grid-framework.scss │ │ ├── _grid.scss │ │ ├── _hover.scss │ │ ├── _image.scss │ │ ├── _list-group.scss │ │ ├── _lists.scss │ │ ├── _nav-divider.scss │ │ ├── _pagination.scss │ │ ├── _reset-text.scss │ │ ├── _resize.scss │ │ ├── _screen-reader.scss │ │ ├── _size.scss │ │ ├── _table-row.scss │ │ ├── _text-emphasis.scss │ │ ├── _text-hide.scss │ │ ├── _text-truncate.scss │ │ ├── _transition.scss │ │ └── _visibility.scss │ ├── utilities │ │ ├── _align.scss │ │ ├── _background.scss │ │ ├── _borders.scss │ │ ├── _clearfix.scss │ │ ├── _display.scss │ │ ├── _embed.scss │ │ ├── _flex.scss │ │ ├── _float.scss │ │ ├── _interactions.scss │ │ ├── _overflow.scss │ │ ├── _position.scss │ │ ├── _screenreaders.scss │ │ ├── _shadows.scss │ │ ├── _sizing.scss │ │ ├── _spacing.scss │ │ ├── _stretched-link.scss │ │ ├── _text.scss │ │ └── _visibility.scss │ └── vendor │ │ └── _rfs.scss │ ├── styles-bs.scss │ ├── styles.scss │ └── variables.scss └── tests └── .jshintrc /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.25% 2 | not dead 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | vendor 3 | media 4 | profile_images 5 | !src/frontend/styles/bootstrap/vendor 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Stefan Bohacek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > Twitter is a [nazi bar](https://www.upworthy.com/bartender-explains-why-he-swiftly-kicks-nazis-out-of-his-punk-bar-even-if-theyre-not-bothering-anyone) now and as a result, this project is no longer maintained. Consider [joining the fediverse](https://jointhefediverse.net/) instead, and check out my new [Fediverse Embeds](https://stefanbohacek.com/project/wordpress-plugin-for-fediverse-embeds/) plugin! 3 | 4 | 5 | ![Preview of multiple Tweets embedded with the Tweet Embeds plugin](./images/thumbnail/tweet-embeds-bw-tint.png) 6 | 7 | # Tweet Embeds 8 | 9 | Embed tweets without compromising your users' privacy and your site's performance. 10 | 11 | Learn more [on fourtonfish.com](https://fourtonfish.com/project/tweet-embeds-wordpress-plugin/). 12 | 13 | ## How to use 14 | 15 | 1. [Install the plugin.](https://wordpress.org/plugins/tembeds) 16 | 2. [Create a new Twitter app](https://developer.twitter.com/en/dashboard) and get your API keys. 17 | 3. Go to the plugin's settings page and add your Twitter API keys. 18 | 19 | If you don't provide the API keys, the plugin will still work, but some data will be missing (profile pictures, number of likes and retweets) and media (images, GIFs, videos) will not render. 20 | 21 | ## Technical details 22 | 23 | ### Process tweets manually 24 | 25 | If you need to process tweets that are added to the page dynamically, use the `ftfHelpers.processTweets()` function. Be sure to check if the function exists before using it to avoid errors in your script. 26 | 27 | 28 | ### Wait for tweets to be processed 29 | 30 | If you need to perform an action after all tweets on the page are processed, add a listener for the `tembeds_tweets_processed` event. 31 | 32 | ```js 33 | document.addEventListener('tembeds_tweets_processed', function(){ 34 | const tweets = document.querySelectorAll('.twitter-tweet'); 35 | console.log('tweets are ready', tweets); 36 | }); 37 | ``` 38 | 39 | Here's an example using jQuery. 40 | 41 | ```js 42 | $(document).on('tembeds_tweets_processed', function(){ 43 | const $tweets = $('.twitter-tweet'); 44 | console.log('tweets are ready', $tweets); 45 | }); 46 | ``` 47 | 48 | ## Development 49 | 50 | ```sh 51 | # install dependencies 52 | npm install 53 | # build front-end scripts and styles 54 | gulp 55 | # when adding new PHP classes inside `includes` 56 | composer dumpautoload -o 57 | ``` 58 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "useBuiltIns": "entry", 8 | "corejs": "3.22" 9 | } 10 | ], 11 | [ 12 | "minify", { 13 | "builtIns": false, 14 | "evaluate": false, 15 | "mangle": false 16 | } 17 | ] 18 | ], 19 | "sourceType": "module" 20 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stefan-bohacek/tembeds", 3 | "description": "Embed tweets without compromising your users' privacy and your site's performance.", 4 | "type": "wordpress-plugin", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "FTF_TEmbeds\\": "includes/" 9 | } 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Stefan Bohacek", 14 | "email": "stefan@stefanbohacek.com" 15 | } 16 | ], 17 | "require": {} 18 | } 19 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "7c3e089b83241b4ad1874f4859cf0d79", 8 | "packages": [], 9 | "packages-dev": [], 10 | "aliases": [], 11 | "minimum-stability": "stable", 12 | "stability-flags": [], 13 | "prefer-stable": false, 14 | "prefer-lowest": false, 15 | "platform": [], 16 | "platform-dev": [], 17 | "plugin-api-version": "2.1.0" 18 | } 19 | -------------------------------------------------------------------------------- /dist/css/styles.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | .twitter-tweet.twitter-tweet-rendered { 3 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif; 4 | max-width: 640px; } 5 | .twitter-tweet.twitter-tweet-rendered .card-body { 6 | line-height: 1.1rem; } 7 | .twitter-tweet.twitter-tweet-rendered .card-body .card-title, 8 | .twitter-tweet.twitter-tweet-rendered .card-body .card-subtitle, 9 | .twitter-tweet.twitter-tweet-rendered .card-body .stretched-link { 10 | font-size: 1rem; } 11 | .twitter-tweet.twitter-tweet-rendered .card-footer { 12 | border-top: none !important; 13 | background: #fff !important; } 14 | .twitter-tweet.twitter-tweet-rendered .card-footer a { 15 | text-decoration: none; } 16 | .twitter-tweet.twitter-tweet-rendered .card-footer a:hover { 17 | text-decoration: underline; } 18 | .twitter-tweet.twitter-tweet-rendered .tweet-author { 19 | line-height: 1.2; } 20 | .twitter-tweet.twitter-tweet-rendered .tweet-verified-user-badge { 21 | height: 1.16rem; 22 | fill: #1b95e0; 23 | margin-left: 3px; 24 | vertical-align: sub; } 25 | .twitter-tweet.twitter-tweet-rendered .tweet-body { 26 | white-space: pre-wrap; 27 | line-height: 1.4; } 28 | .twitter-tweet.twitter-tweet-rendered .tweet-body a { 29 | text-decoration: none; 30 | color: #1b95e0; } 31 | .twitter-tweet.twitter-tweet-rendered .tweet-body a:hover { 32 | text-decoration: underline; } 33 | .twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-poll-results img.emoji { 34 | margin-top: 0.5rem !important; } 35 | .twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-poll-results .progress-bar { 36 | background-color: lightskyblue; } 37 | .twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-video-placeholder { 38 | position: relative; } 39 | .twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-video-placeholder:after { 40 | content: '▶️'; 41 | display: block; 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | width: 100%; 46 | font-size: 8rem; 47 | line-height: 1rem; 48 | text-align: center; 49 | color: #fff; 50 | text-shadow: 5px 2px 10px rgba(0, 0, 0, 0.5); 51 | opacity: 0.8; 52 | -webkit-transition: opacity 0.2s; 53 | -o-transition: opacity 0.2s; 54 | transition: opacity 0.2s; } 55 | .twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-video-placeholder img { 56 | -webkit-filter: brightness(0.5); 57 | filter: brightness(0.5); 58 | -webkit-transition: -webkit-filter 0.2s; 59 | transition: -webkit-filter 0.2s; 60 | -o-transition: filter 0.2s; 61 | transition: filter 0.2s; 62 | transition: filter 0.2s, -webkit-filter 0.2s; } 63 | .twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-video-placeholder:hover:after { 64 | opacity: 1; 65 | -webkit-transition: opacity 0.2s; 66 | -o-transition: opacity 0.2s; 67 | transition: opacity 0.2s; } 68 | .twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-video-placeholder:hover img { 69 | -webkit-filter: brightness(0.6); 70 | filter: brightness(0.6); 71 | -webkit-transition: -webkit-filter 0.2s; 72 | transition: -webkit-filter 0.2s; 73 | -o-transition: filter 0.2s; 74 | transition: filter 0.2s; 75 | transition: filter 0.2s, -webkit-filter 0.2s; } 76 | .twitter-tweet.twitter-tweet-rendered .tweet-body .card { 77 | white-space: normal; 78 | margin-top: 2rem; } 79 | .twitter-tweet.twitter-tweet-rendered .tweet-media img { 80 | height: 260px !important; 81 | -o-object-fit: cover !important; 82 | object-fit: cover !important; } 83 | .twitter-tweet.twitter-tweet-rendered .tweet-media .col-lg-12 img { 84 | height: 360px !important; } 85 | .twitter-tweet.twitter-tweet-rendered .tweet-media[data-media-length] .col-lg-12 img { 86 | height: 260px !important; } 87 | .twitter-tweet.twitter-tweet-rendered .tweet-media[data-media-length="2"] [data-media-type="photo"] img { 88 | height: 320px !important; } 89 | .twitter-tweet.twitter-tweet-rendered .tweet-media[data-media-length="1"] [data-media-type="photo"] img { 90 | height: unset !important; 91 | -o-object-fit: contain !important; 92 | object-fit: contain !important; } 93 | .twitter-tweet.twitter-tweet-rendered .tweet-media [data-media-type="video"] .tweet-video-placeholder img { 94 | height: 360px !important; } 95 | .twitter-tweet.twitter-tweet-rendered .tweet-attachment-preview a { 96 | text-decoration: none !important; } 97 | .twitter-tweet.twitter-tweet-rendered .tweet-attachment-preview a:hover { 98 | text-decoration: underline !important; } 99 | .twitter-tweet.twitter-tweet-rendered .tweet-attachment-preview img.tweet-attachment-site-thumbnail { 100 | height: 300px !important; 101 | -o-object-fit: cover !important; 102 | object-fit: cover !important; } 103 | .twitter-tweet.twitter-tweet-rendered .tweet-icon { 104 | margin-right: 5px; } 105 | -------------------------------------------------------------------------------- /dist/css/styles.min.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";.twitter-tweet.twitter-tweet-rendered{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Ubuntu,"Helvetica Neue",sans-serif;max-width:640px}.twitter-tweet.twitter-tweet-rendered .card-body{line-height:1.1rem}.twitter-tweet.twitter-tweet-rendered .card-body .card-subtitle,.twitter-tweet.twitter-tweet-rendered .card-body .card-title,.twitter-tweet.twitter-tweet-rendered .card-body .stretched-link{font-size:1rem}.twitter-tweet.twitter-tweet-rendered .card-footer{border-top:none!important;background:#fff!important}.twitter-tweet.twitter-tweet-rendered .card-footer a{text-decoration:none}.twitter-tweet.twitter-tweet-rendered .card-footer a:hover{text-decoration:underline}.twitter-tweet.twitter-tweet-rendered .tweet-author{line-height:1.2}.twitter-tweet.twitter-tweet-rendered .tweet-verified-user-badge{height:1.16rem;fill:#1b95e0;margin-left:3px;vertical-align:sub}.twitter-tweet.twitter-tweet-rendered .tweet-body{white-space:pre-wrap;line-height:1.4}.twitter-tweet.twitter-tweet-rendered .tweet-body a{text-decoration:none;color:#1b95e0}.twitter-tweet.twitter-tweet-rendered .tweet-body a:hover{text-decoration:underline}.twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-poll-results img.emoji{margin-top:.5rem!important}.twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-poll-results .progress-bar{background-color:#87cefa}.twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-video-placeholder{position:relative}.twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-video-placeholder:after{content:'▶️';display:block;position:absolute;top:0;left:0;width:100%;font-size:8rem;line-height:1rem;text-align:center;color:#fff;text-shadow:5px 2px 10px rgba(0,0,0,.5);opacity:.8;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-video-placeholder img{-webkit-filter:brightness(.5);filter:brightness(.5);-webkit-transition:-webkit-filter .2s;-o-transition:filter .2s;transition:filter .2s;transition:filter .2s,-webkit-filter .2s}.twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-video-placeholder:hover:after{opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.twitter-tweet.twitter-tweet-rendered .tweet-body .tweet-video-placeholder:hover img{-webkit-filter:brightness(.6);filter:brightness(.6);-webkit-transition:-webkit-filter .2s;-o-transition:filter .2s;transition:filter .2s;transition:filter .2s,-webkit-filter .2s}.twitter-tweet.twitter-tweet-rendered .tweet-body .card{white-space:normal;margin-top:2rem}.twitter-tweet.twitter-tweet-rendered .tweet-media img{height:260px!important;-o-object-fit:cover!important;object-fit:cover!important}.twitter-tweet.twitter-tweet-rendered .tweet-media .col-lg-12 img{height:360px!important}.twitter-tweet.twitter-tweet-rendered .tweet-media[data-media-length] .col-lg-12 img{height:260px!important}.twitter-tweet.twitter-tweet-rendered .tweet-media[data-media-length="2"] [data-media-type=photo] img{height:320px!important}.twitter-tweet.twitter-tweet-rendered .tweet-media[data-media-length="1"] [data-media-type=photo] img{height:unset!important;-o-object-fit:contain!important;object-fit:contain!important}.twitter-tweet.twitter-tweet-rendered .tweet-media [data-media-type=video] .tweet-video-placeholder img{height:360px!important}.twitter-tweet.twitter-tweet-rendered .tweet-attachment-preview a{text-decoration:none!important}.twitter-tweet.twitter-tweet-rendered .tweet-attachment-preview a:hover{text-decoration:underline!important}.twitter-tweet.twitter-tweet-rendered .tweet-attachment-preview img.tweet-attachment-site-thumbnail{height:300px!important;-o-object-fit:cover!important;object-fit:cover!important}.twitter-tweet.twitter-tweet-rendered .tweet-icon{margin-right:5px} -------------------------------------------------------------------------------- /dist/css/variables.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/dist/css/variables.css -------------------------------------------------------------------------------- /dist/css/variables.min.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/dist/css/variables.min.css -------------------------------------------------------------------------------- /dist/js/dispatchEvent.js: -------------------------------------------------------------------------------- 1 | var dispatchEvent=function(eventName){var event=new Event(eventName);document.dispatchEvent(event)};export{dispatchEvent}; 2 | //# sourceMappingURL=dispatchEvent.js.map 3 | -------------------------------------------------------------------------------- /dist/js/dispatchEvent.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"dispatchEvent.js","names":["dispatchEvent","eventName","event","Event","document"],"sources":["dispatchEvent.js"],"sourcesContent":["const dispatchEvent = (eventName) => {\n const event = new Event(eventName);\n document.dispatchEvent(event);\n};\n\nexport { dispatchEvent };\n"],"mappings":"AAAA,GAAMA,cAAa,CAAG,SAACC,SAAS,CAAK,CACnC,GAAMC,MAAK,CAAG,GAAIC,MAAK,CAACF,SAAS,CAAC,CAClCG,QAAQ,CAACJ,aAAa,CAACE,KAAK,CAC9B,CAAC,CAED,OAASF,aAAa"} -------------------------------------------------------------------------------- /dist/js/fetchData.js: -------------------------------------------------------------------------------- 1 | var fetchData=function(data,cb,done){done=done||function(){/* noop */},fetch(window.ftf_aet.ajax_url,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded","Cache-Control":"no-cache"},body:new URLSearchParams(data)}).then(function(response){return response.json()}).then(function(response){// console.log('response', response); 2 | cb(response)}).catch(function(error){console.error("tembeds_error",error)}).then(done)};export{fetchData}; 3 | //# sourceMappingURL=fetchData.js.map 4 | -------------------------------------------------------------------------------- /dist/js/fetchData.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"fetchData.js","names":["fetchData","data","cb","done","fetch","window","ftf_aet","ajax_url","method","credentials","headers","body","URLSearchParams","then","response","json","catch","error","console"],"sources":["fetchData.js"],"sourcesContent":["const fetchData = (data, cb, done) => {\n done = done || function(){ /* noop */ }\n\n fetch(window.ftf_aet.ajax_url, {\n method: 'POST',\n credentials: 'same-origin',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'Cache-Control': 'no-cache',\n },\n body: new URLSearchParams(data) })\n .then((response) => response.json())\n .then((response) => {\n // console.log('response', response);\n cb(response);\n })\n .catch((error) => {\n console.error('tembeds_error', error);\n })\n .then(done); \n};\n\nexport { fetchData };\n"],"mappings":"AAAA,GAAMA,UAAS,CAAG,SAACC,IAAI,CAAEC,EAAE,CAAEC,IAAI,CAAK,CACpCA,IAAI,CAAGA,IAAI,EAAI,UAAU,CAAE,WAAY,CAEvCC,KAAK,CAACC,MAAM,CAACC,OAAO,CAACC,QAAQ,CAAE,CAC3BC,MAAM,CAAE,MAAM,CACdC,WAAW,CAAE,aAAa,CAC1BC,OAAO,CAAE,CACL,eAAgB,mCAAmC,CACnD,gBAAiB,UACrB,CAAC,CACDC,IAAI,CAAE,GAAIC,gBAAe,CAACX,IAAI,CAAE,CAAC,CAAC,CACjCY,IAAI,CAAC,SAACC,QAAQ,QAAKA,SAAQ,CAACC,IAAI,EAAE,EAAC,CACnCF,IAAI,CAAC,SAACC,QAAQ,CAAK,CAChB;AACAZ,EAAE,CAACY,QAAQ,CACf,CAAC,CAAC,CACDE,KAAK,CAAC,SAACC,KAAK,CAAK,CACdC,OAAO,CAACD,KAAK,CAAC,eAAe,CAAEA,KAAK,CACxC,CAAC,CAAC,CACDJ,IAAI,CAACV,IAAI,CAChB,CAAC,CAED,OAASH,SAAS"} -------------------------------------------------------------------------------- /dist/js/getTweetId.js: -------------------------------------------------------------------------------- 1 | var getTweetId=function(url){return url.match(/status\/(\d+)/g)[0].replace("status/","")};export{getTweetId}; 2 | //# sourceMappingURL=getTweetId.js.map 3 | -------------------------------------------------------------------------------- /dist/js/getTweetId.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"getTweetId.js","names":["getTweetId","url","match","replace"],"sources":["getTweetId.js"],"sourcesContent":["const getTweetId = (url) => url.match(/status\\/(\\d+)/g)[0].replace('status/', '');\n\nexport { getTweetId };\n"],"mappings":"AAAA,GAAMA,WAAU,CAAG,SAACC,GAAG,QAAKA,IAAG,CAACC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAACC,OAAO,CAAC,SAAS,CAAE,EAAE,CAAC,EAEjF,OAASH,UAAU"} -------------------------------------------------------------------------------- /dist/js/processTweets.js: -------------------------------------------------------------------------------- 1 | function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof Symbol&&o[Symbol.iterator]||o["@@iterator"];if(!it){if(Array.isArray(o)||(it=_unsupportedIterableToArray(o))||allowArrayLike&&o&&"number"==typeof o.length){it&&(o=it);var i=0,F=function(){};return{s:F,n:function n(){return i>=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function e(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function s(){it=it.call(o)},n:function n(){var step=it.next();return normalCompletion=step.done,step},e:function e(_e2){didErr=!0,err=_e2},f:function f(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _unsupportedIterableToArray(o,minLen){if(o){if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);return"Object"===n&&o.constructor&&(n=o.constructor.name),"Map"===n||"Set"===n?Array.from(o):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?_arrayLikeToArray(o,minLen):void 0}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=Array(len);i\"Preview")),urlAttachmentPreviewHTML+="
",urlAttachmentPreviewHTML+="

").concat(tmpAnchor.hostname,"

"),data.title&&(urlAttachmentPreviewHTML+="

".concat(data.title,"

")),data.description&&(urlAttachmentPreviewHTML+="

".concat(data.description,"

")),urlAttachmentPreviewHTML+="
",urlAttachmentPreview.innerHTML=urlAttachmentPreviewHTML,tweet.querySelector(".tweet-body-wrapper").appendChild(urlAttachmentPreview)}},function(){tweetsWithAttachmentCount--,0===tweetsWithAttachmentCount&&dispatchEvent("tembeds_tweets_processed")})};for(_iterator2.s();!(_step2=_iterator2.n()).done;)_loop()}catch(err){_iterator2.e(err)}finally{_iterator2.f()}}()});else{var _step3,_iterator3=_createForOfIteratorHelper(tweets);try{for(_iterator3.s();!(_step3=_iterator3.n()).done;){var tweet=_step3.value,tweetAttribution="",tweetDate="";if(tweet.childNodes&&tweet.childNodes.length){if(3===tweet.childNodes.length){tweetDate=tweet.childNodes[2].textContent;for(var currentNode,i=0;i {\n if (document.readyState != \"loading\") {\n fn();\n } else {\n document.addEventListener(\"DOMContentLoaded\", fn);\n }\n};\n\nexport { ready };\n"],"mappings":"AAAA,GAAMA,MAAK,CAAG,SAACC,EAAE,CAAK,CACO,SAAS,EAAhCC,QAAQ,CAACC,UAAuB,CAGlCD,QAAQ,CAACE,gBAAgB,CAAC,kBAAkB,CAAEH,EAAE,CAAC,CAFjDA,EAAE,EAIN,CAAC,CAED,OAASD,KAAK"} -------------------------------------------------------------------------------- /dist/js/scripts.js: -------------------------------------------------------------------------------- 1 | "use strict";import{ready}from"./ready.js";import{processTweets}from"./processTweets.js";ready(function(){processTweets()}); 2 | //# sourceMappingURL=scripts.js.map 3 | -------------------------------------------------------------------------------- /dist/js/scripts.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"scripts.js","names":["ready","processTweets"],"sources":["scripts.js"],"sourcesContent":["'use strict';\nimport { ready } from \"./ready.js\";\nimport { processTweets } from \"./processTweets.js\";\n\nready(function(){\n processTweets();\n});\n"],"mappings":"AAAA,YAAY,CACZ,OAASA,KAAK,KAAQ,YAAY,CAClC,OAASC,aAAa,KAAQ,oBAAoB,CAElDD,KAAK,CAAC,UAAU,CACdC,aAAa,EACf,CAAC,CAAC"} -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'), 2 | sass = require('gulp-sass'), 3 | less = require('gulp-less'), 4 | path = require('path'), 5 | streamify = require('gulp-streamify'), 6 | autoprefixer = require('gulp-autoprefixer'), 7 | minifycss = require('gulp-minify-css'), 8 | browserify = require('browserify'), 9 | babelify = require('babelify'), 10 | sourcemaps = require('gulp-sourcemaps'), 11 | babel = require('gulp-babel'), 12 | source = require('vinyl-source-stream'), 13 | gutil = require('gulp-util'), 14 | jshint = require('gulp-jshint'), 15 | stylish = require('jshint-stylish'), 16 | uglify = require('gulp-uglify'), 17 | minify = require('gulp-babel-minify'), 18 | rename = require('gulp-rename'), 19 | clean = require('gulp-clean'), 20 | concat = require('gulp-concat'), 21 | notify = require('gulp-notify'); 22 | 23 | function swallow_error(error) { 24 | console.log(error.toString()); 25 | this.emit('end'); 26 | } 27 | 28 | gulp.task('styles', function() { 29 | return gulp.src('src/frontend/styles/*.scss') 30 | .pipe(sass({ 31 | paths: [ path.join(__dirname, 'scss', 'includes') ] 32 | })) 33 | .on('error', swallow_error) 34 | .pipe(autoprefixer('last 3 version', 'android >= 3', { cascade: true })) 35 | .on('error', swallow_error) 36 | .pipe(gulp.dest('dist/css')) 37 | .pipe(rename({ suffix: '.min' })) 38 | .pipe(minifycss()) 39 | .on('error', swallow_error) 40 | .pipe(gulp.dest('dist/css')); 41 | }); 42 | 43 | gulp.task('scripts', function() { 44 | gulp.src('src/frontend/scripts/*.js') 45 | .pipe(sourcemaps.init()) 46 | .pipe(babel()) 47 | .on('error', swallow_error) 48 | .pipe(sourcemaps.write('.')) 49 | .pipe(gulp.dest('./dist/js')) 50 | }); 51 | 52 | gulp.task('jslint', function(){ 53 | return gulp.src([ 54 | './src/scripts/**/*.js' 55 | ]).pipe(jshint('tests/.jshintrc')) 56 | .on('error',gutil.noop) 57 | .pipe(jshint.reporter(stylish)) 58 | .on('error', swallow_error); 59 | }); 60 | 61 | gulp.task('clean', function() { 62 | return gulp.src(['dist/css', 'dist/js'], { read: false }) 63 | .pipe(clean()); 64 | }); 65 | 66 | gulp.task('watch', function() { 67 | gulp.watch('src/frontend/styles/**/*.*', ['styles']); 68 | gulp.watch('src/frontend/scripts/**/*.*', ['jslint', 'scripts']); 69 | }); 70 | 71 | gulp.task('default', ['clean'], function() { 72 | gulp.start('styles', 'jslint', 'scripts', 'watch'); 73 | }); 74 | -------------------------------------------------------------------------------- /images/assets/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/assets/banner-1544x500.png -------------------------------------------------------------------------------- /images/assets/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/assets/banner-772x250.png -------------------------------------------------------------------------------- /images/assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/assets/icon-128x128.png -------------------------------------------------------------------------------- /images/assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/assets/icon-256x256.png -------------------------------------------------------------------------------- /images/assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/assets/screenshot-1.png -------------------------------------------------------------------------------- /images/assets/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/assets/screenshot-2.png -------------------------------------------------------------------------------- /images/banner-1544x500.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/banner-1544x500.psd -------------------------------------------------------------------------------- /images/blog/performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/blog/performance.png -------------------------------------------------------------------------------- /images/blog/test-demo/desktop-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/blog/test-demo/desktop-off.png -------------------------------------------------------------------------------- /images/blog/test-demo/desktop-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/blog/test-demo/desktop-on.png -------------------------------------------------------------------------------- /images/blog/test-demo/mobile-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/blog/test-demo/mobile-off.png -------------------------------------------------------------------------------- /images/blog/test-demo/mobile-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/blog/test-demo/mobile-on.png -------------------------------------------------------------------------------- /images/blog/test-fourtonfish/desktop-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/blog/test-fourtonfish/desktop-off.png -------------------------------------------------------------------------------- /images/blog/test-fourtonfish/desktop-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/blog/test-fourtonfish/desktop-on.png -------------------------------------------------------------------------------- /images/blog/test-fourtonfish/mobile-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/blog/test-fourtonfish/mobile-off.png -------------------------------------------------------------------------------- /images/blog/test-fourtonfish/mobile-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/blog/test-fourtonfish/mobile-on.png -------------------------------------------------------------------------------- /images/thumbnail/tweet-embeds-bw-tint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/thumbnail/tweet-embeds-bw-tint.png -------------------------------------------------------------------------------- /images/thumbnail/tweet-embeds-bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/thumbnail/tweet-embeds-bw.png -------------------------------------------------------------------------------- /images/thumbnail/tweet-embeds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbohacek/tweet-embeds-wordpress-plugin/108e77759f49c19fb43cf6c412acf2a9174f97bc/images/thumbnail/tweet-embeds.png -------------------------------------------------------------------------------- /includes/Cleanup.php: -------------------------------------------------------------------------------- 1 | ', '', $content); 18 | $content = str_replace(''; 65 | return $tag; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /includes/Helpers.php: -------------------------------------------------------------------------------- 1 | archival_mode = get_option('ftf_alt_embed_tweet_archival_mode') === 'on' ? true : false; 16 | add_action('rest_api_init', array($this, 'register_media_proxy_endpoint')); 17 | add_action('wp_ajax_nopriv_ftf_media_proxy', array($this, 'media_proxy'), 1000); 18 | } 19 | 20 | public function register_media_proxy_endpoint(/* $_REQUEST */) { 21 | register_rest_route('ftf', 'proxy-media', array( 22 | 'methods' => \WP_REST_Server::READABLE, 23 | 'permission_callback' => '__return_true', 24 | 'callback' => array($this, 'proxy_media'), 25 | )); 26 | } 27 | 28 | public function proxy_media(\WP_REST_Request $request){ 29 | $url = $request['url']; 30 | 31 | if (strpos($url, 'profile_images')){ 32 | $folder_name = 'profile_images'; 33 | } else { 34 | $folder_name = 'media'; 35 | } 36 | 37 | if ($this->archival_mode){ 38 | $dir = plugin_dir_path(__FILE__) . "../$folder_name"; 39 | $file_name = basename($url); 40 | $file_path = "$dir/$file_name"; 41 | 42 | if (!is_dir($dir)) { 43 | mkdir($dir); 44 | } 45 | } 46 | 47 | if ($this->archival_mode && file_exists($file_path)){ 48 | 49 | Helpers::log_this('debug:proxy_media', array( 50 | 'url' => $url, 51 | 'file_name' => $file_name, 52 | 'file_path' => $file_path, 53 | 'file_exists' => 'true', 54 | )); 55 | 56 | $image_info = getimagesize($file_path); 57 | header("Content-type: {$image_info['mime']}"); 58 | echo file_get_contents($file_path); 59 | 60 | } else { 61 | $remote_response = wp_remote_get($url); 62 | 63 | Helpers::log_this('debug:proxy_media', array( 64 | 'url' => $url, 65 | 'file_name' => $file_name, 66 | 'remote_response' => $remote_response, 67 | )); 68 | 69 | if ($this->archival_mode){ 70 | file_put_contents($file_path, $remote_response['body']); 71 | } 72 | 73 | 74 | header('Content-Type: ' . $remote_response['headers']['content-type']); 75 | echo $remote_response['body']; 76 | } 77 | 78 | exit(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /includes/Site_Info.php: -------------------------------------------------------------------------------- 1 | find('meta[name="twitter:image"]'); 36 | 37 | if (!empty($meta_image)){ 38 | $image = $meta_image[0]->content; 39 | } else { 40 | $meta_image = $site_html->find('meta[property="og:image"]'); 41 | $image = $meta_image[0]->content; 42 | } 43 | 44 | $meta_title = $site_html->find('meta[name="title"]'); 45 | 46 | if (!empty($meta_title)){ 47 | $title = $meta_title[0]->content; 48 | } else { 49 | $meta_title = $site_html->find('meta[name="twitter:title"]'); 50 | 51 | if (!empty($meta_title)){ 52 | $title = $meta_title[0]->content; 53 | } else { 54 | $meta_title = $site_html->find('meta[property="og:title"]'); 55 | $title = $meta_title[0]->content; 56 | } 57 | } 58 | 59 | $meta_description = $site_html->find('meta[name="description"]'); 60 | 61 | if (!empty($meta_description)){ 62 | $description = $meta_description[0]->content; 63 | } else { 64 | $meta_description = $site_html->find('meta[name="twitter:title"]'); 65 | 66 | if (!empty($meta_description)){ 67 | $description = $meta_description[0]->content; 68 | } else { 69 | $meta_description = $site_html->find('meta[property="og:title"]'); 70 | $description = $meta_description[0]->content; 71 | } 72 | 73 | } 74 | 75 | $description = $meta_description[0]->content; 76 | } 77 | 78 | $site_data = array( 79 | 'url' => $site_url, 80 | 'image' => $image, 81 | 'title' => $title, 82 | 'description' => $description 83 | ); 84 | 85 | Helpers::log_this('debug:get_site_info', array( 86 | 'site_data' => $site_data, 87 | )); 88 | 89 | wp_cache_set($cache_key, $site_data, 'ftf_alt_embed_tweet', ($cache_expiration * MINUTE_IN_SECONDS)); 90 | } 91 | // error_log(print_r($site_data, true)); 92 | wp_send_json($site_data); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | { 2 | const event = new Event(eventName); 3 | document.dispatchEvent(event); 4 | }; 5 | 6 | export { dispatchEvent }; 7 | -------------------------------------------------------------------------------- /src/frontend/scripts/fetchData.js: -------------------------------------------------------------------------------- 1 | const fetchData = (data, cb, done) => { 2 | done = done || function(){ /* noop */ } 3 | 4 | fetch(window.ftf_aet.ajax_url, { 5 | method: 'POST', 6 | credentials: 'same-origin', 7 | headers: { 8 | 'Content-Type': 'application/x-www-form-urlencoded', 9 | 'Cache-Control': 'no-cache', 10 | }, 11 | body: new URLSearchParams(data) }) 12 | .then((response) => response.json()) 13 | .then((response) => { 14 | // console.log('response', response); 15 | cb(response); 16 | }) 17 | .catch((error) => { 18 | console.error('tembeds_error', error); 19 | }) 20 | .then(done); 21 | }; 22 | 23 | export { fetchData }; 24 | -------------------------------------------------------------------------------- /src/frontend/scripts/getTweetId.js: -------------------------------------------------------------------------------- 1 | const getTweetId = (url) => url.match(/status\/(\d+)/g)[0].replace('status/', ''); 2 | 3 | export { getTweetId }; 4 | -------------------------------------------------------------------------------- /src/frontend/scripts/processTweets.js: -------------------------------------------------------------------------------- 1 | import { fetchData } from "./fetchData.js"; 2 | import { getTweetId } from "./getTweetId.js"; 3 | import { renderTweet } from "./renderTweet.js"; 4 | import { dispatchEvent } from "./dispatchEvent.js"; 5 | 6 | const processTweets = (fn) => { 7 | const tweets = document.querySelectorAll('blockquote.twitter-tweet'); 8 | let tweetIds = []; 9 | 10 | for (const tweet of tweets) { 11 | const anchors = tweet.querySelectorAll('a'); 12 | const url = anchors[anchors.length - 1].href; 13 | const tweetId = getTweetId(url); 14 | tweetIds.push(tweetId); 15 | tweet.dataset.tweetId = tweetId; 16 | } 17 | 18 | // console.log('tweet IDs', tweetIds); 19 | 20 | if (tweetIds.length){ 21 | if (ftf_aet.config.use_api){ 22 | fetchData({ 23 | action: 'ftf_embed_tweet', 24 | tweet_ids: tweetIds.join(',') 25 | }, function(response){ 26 | if (response && response.length){ 27 | response.forEach(function(data){ 28 | renderTweet(data); 29 | }); 30 | 31 | const tweetsWithAttachment = document.querySelectorAll('[data-url-attachment-processed="false"]'); 32 | let tweetsWithAttachmentCount = tweetsWithAttachment.length; 33 | 34 | if (tweetsWithAttachmentCount === 0){ 35 | dispatchEvent('tembeds_tweets_processed'); 36 | } 37 | 38 | // console.log('tweetsWithAttachment', tweetsWithAttachment); 39 | 40 | for (const tweet of tweetsWithAttachment) { 41 | tweet.dataset.urlAttachmentProcessed = 'true'; 42 | 43 | if (tweet.dataset.urlAttachment.indexOf('twitter.com/') > -1){ 44 | console.log('rendering QT...', getTweetId(tweet.dataset.urlAttachment)); 45 | fetchData({ 46 | action: 'ftf_embed_tweet', 47 | tweet_ids: [getTweetId(tweet.dataset.urlAttachment)] 48 | }, function(response){ 49 | console.log(response); 50 | if (response && response.length){ 51 | response.forEach(function(data){ 52 | renderTweet(data, tweet); 53 | }); 54 | } 55 | }); 56 | } else { 57 | // noop 58 | } 59 | 60 | fetchData({ 61 | action: 'ftf_get_site_info', 62 | url: tweet.dataset.urlAttachment 63 | }, function(data){ 64 | if (data && data.image){ 65 | let urlAttachmentPreview = document.createElement('div'); 66 | urlAttachmentPreview.className = `tweet-attachment-preview card mt-4`; 67 | 68 | let tmpAnchor = document.createElement ('a'); 69 | tmpAnchor.href = tweet.dataset.urlAttachment; 70 | 71 | let urlAttachmentPreviewHTML = ''; 72 | console.log('debug:data.image', data.image); 73 | if (data.image){ 74 | urlAttachmentPreviewHTML += `Preview image for ${tweet.dataset.urlAttachment}`; 75 | } 76 | 77 | urlAttachmentPreviewHTML += `
`; 78 | urlAttachmentPreviewHTML += `

${ tmpAnchor.hostname }

`; 79 | 80 | if (data.title){ 81 | urlAttachmentPreviewHTML += `

${ data.title }

`; 82 | } 83 | 84 | if (data.description){ 85 | urlAttachmentPreviewHTML += `

${ data.description }

`; 86 | } 87 | 88 | urlAttachmentPreviewHTML += `
`; 89 | 90 | urlAttachmentPreview.innerHTML = urlAttachmentPreviewHTML; 91 | tweet.querySelector('.tweet-body-wrapper').appendChild(urlAttachmentPreview); 92 | } 93 | 94 | }, function(){ 95 | tweetsWithAttachmentCount--; 96 | // console.log('tweetsWithAttachmentCount', tweetsWithAttachmentCount); 97 | if (tweetsWithAttachmentCount === 0){ 98 | dispatchEvent('tembeds_tweets_processed'); 99 | } 100 | }); 101 | } 102 | } 103 | 104 | }); 105 | } else { 106 | 107 | for (const tweet of tweets) { 108 | // console.log('debug:tweet', tweet); 109 | // console.log('debug:childNodes', tweet.childNodes); 110 | 111 | let tweetAttribution = '', tweetDate = ''; 112 | 113 | if (tweet.childNodes && tweet.childNodes.length){ 114 | if (tweet.childNodes.length === 3){ 115 | tweetDate = tweet.childNodes[2].textContent; 116 | for (let i = 0; i < tweet.childNodes.length; i++){ 117 | let currentNode = tweet.childNodes[i]; 118 | if (currentNode.nodeName === '#text') { 119 | tweetAttribution = currentNode.nodeValue; 120 | break; 121 | } 122 | } 123 | } else { 124 | tweetAttribution = tweet.childNodes[tweet.childNodes.length - 2].innerHTML; 125 | let tweetDateEl = document.createElement('div'); 126 | tweetDateEl.innerHTML = tweetAttribution; 127 | tweetDate = tweetDateEl.querySelector('a').textContent; 128 | } 129 | 130 | const usernames = tweetAttribution.match(/@\w+/gi); 131 | let name = '', username = ''; 132 | // console.log('debug:tweetDate', tweetDate); 133 | 134 | if (usernames && usernames[0]){ 135 | username = usernames[0]; 136 | const names = tweetAttribution.split(username); 137 | // console.log('debug:names', usernames); 138 | 139 | if (names && names[0]){ 140 | name = names[0].replace('— ', '').replace(' (', ''); 141 | } 142 | } 143 | 144 | renderTweet({ 145 | 'created_at': tweetDate, 146 | 'text': tweet.querySelector('p').innerHTML, 147 | 'id': tweet.dataset.tweetId, 148 | // 'author_id': '', 149 | 'users': [ 150 | { 151 | 'name': name, 152 | 'username': username.replace(/^@/, ''), 153 | // 'id': '', 154 | // 'profile_image_url': '', 155 | // 'verified': false 156 | } 157 | ] 158 | }); 159 | } 160 | } 161 | } 162 | } 163 | }; 164 | 165 | export { processTweets }; 166 | -------------------------------------------------------------------------------- /src/frontend/scripts/ready.js: -------------------------------------------------------------------------------- 1 | const ready = (fn) => { 2 | if (document.readyState != "loading") { 3 | fn(); 4 | } else { 5 | document.addEventListener("DOMContentLoaded", fn); 6 | } 7 | }; 8 | 9 | export { ready }; 10 | -------------------------------------------------------------------------------- /src/frontend/scripts/scripts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { ready } from "./ready.js"; 3 | import { processTweets } from "./processTweets.js"; 4 | 5 | ready(function(){ 6 | processTweets(); 7 | }); 8 | -------------------------------------------------------------------------------- /src/frontend/styles/bootstrap/_alert.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Base styles 3 | // 4 | 5 | .alert { 6 | position: relative; 7 | padding: $alert-padding-y $alert-padding-x; 8 | margin-bottom: $alert-margin-bottom; 9 | border: $alert-border-width solid transparent; 10 | @include border-radius($alert-border-radius); 11 | } 12 | 13 | // Headings for larger alerts 14 | .alert-heading { 15 | // Specified to prevent conflicts of changing $headings-color 16 | color: inherit; 17 | } 18 | 19 | // Provide class for links that match alerts 20 | .alert-link { 21 | font-weight: $alert-link-font-weight; 22 | } 23 | 24 | 25 | // Dismissible alerts 26 | // 27 | // Expand the right padding and account for the close button's positioning. 28 | 29 | .alert-dismissible { 30 | padding-right: $close-font-size + $alert-padding-x * 2; 31 | 32 | // Adjust close link position 33 | .close { 34 | position: absolute; 35 | top: 0; 36 | right: 0; 37 | z-index: 2; 38 | padding: $alert-padding-y $alert-padding-x; 39 | color: inherit; 40 | } 41 | } 42 | 43 | 44 | // Alternate styles 45 | // 46 | // Generate contextual modifier classes for colorizing the alert. 47 | 48 | @each $color, $value in $theme-colors { 49 | .alert-#{$color} { 50 | @include alert-variant(theme-color-level($color, $alert-bg-level), theme-color-level($color, $alert-border-level), theme-color-level($color, $alert-color-level)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/frontend/styles/bootstrap/_badge.scss: -------------------------------------------------------------------------------- 1 | // Base class 2 | // 3 | // Requires one of the contextual, color modifier classes for `color` and 4 | // `background-color`. 5 | 6 | .badge { 7 | display: inline-block; 8 | padding: $badge-padding-y $badge-padding-x; 9 | @include font-size($badge-font-size); 10 | font-weight: $badge-font-weight; 11 | line-height: 1; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | @include border-radius($badge-border-radius); 16 | @include transition($badge-transition); 17 | 18 | @at-root a#{&} { 19 | @include hover-focus() { 20 | text-decoration: none; 21 | } 22 | } 23 | 24 | // Empty badges collapse automatically 25 | &:empty { 26 | display: none; 27 | } 28 | } 29 | 30 | // Quick fix for badges in buttons 31 | .btn .badge { 32 | position: relative; 33 | top: -1px; 34 | } 35 | 36 | // Pill badges 37 | // 38 | // Make them extra rounded with a modifier to replace v3's badges. 39 | 40 | .badge-pill { 41 | padding-right: $badge-pill-padding-x; 42 | padding-left: $badge-pill-padding-x; 43 | @include border-radius($badge-pill-border-radius); 44 | } 45 | 46 | // Colors 47 | // 48 | // Contextual variations (linked badges get darker on :hover). 49 | 50 | @each $color, $value in $theme-colors { 51 | .badge-#{$color} { 52 | @include badge-variant($value); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/frontend/styles/bootstrap/_breadcrumb.scss: -------------------------------------------------------------------------------- 1 | .breadcrumb { 2 | display: flex; 3 | flex-wrap: wrap; 4 | padding: $breadcrumb-padding-y $breadcrumb-padding-x; 5 | margin-bottom: $breadcrumb-margin-bottom; 6 | @include font-size($breadcrumb-font-size); 7 | list-style: none; 8 | background-color: $breadcrumb-bg; 9 | @include border-radius($breadcrumb-border-radius); 10 | } 11 | 12 | .breadcrumb-item { 13 | display: flex; 14 | 15 | // The separator between breadcrumbs (by default, a forward-slash: "/") 16 | + .breadcrumb-item { 17 | padding-left: $breadcrumb-item-padding; 18 | 19 | &::before { 20 | display: inline-block; // Suppress underlining of the separator in modern browsers 21 | padding-right: $breadcrumb-item-padding; 22 | color: $breadcrumb-divider-color; 23 | content: escape-svg($breadcrumb-divider); 24 | } 25 | } 26 | 27 | // IE9-11 hack to properly handle hyperlink underlines for breadcrumbs built 28 | // without `