Better Click To Tweet - Version 4.7

Version Description

  • added the ability to change the "via" addendum on a per-box basis using the new "username" shortcode attribute. The default behavior is (still) to go with the username you saved on the settings page.
  • (non-geek explanation of that first point) Now if you have a guest post by @KanyeWest, your Better Click To Tweet box can add "via @KanyeWest" automatically to your reader's tweets!
  • made some changes to the toolbar popup in the visual editor to facilitate the new "username" attribute, limiting confusion and causing much rejoicing.
  • Made unsuccessful attempt at getting Kanye West to guest post as the ultimate demonstration of the new feature.
  • Tested for compatibility with the upcoming WordPress 4.5, and I don't mean to sound arrogant, but it pretty much NAILS compatibility with 4.5.
  • Added a module that shows up when a user is using WordPress in a language for which there is incomplete (or non-existent) translations for this plugin. For users where there is a complete (+90%) translation, nothing will show up. But for users where the translation is incomplete, they'll be encouraged to help with the translation efforts!
Download this release

Release Info

Developer ben.meredith@gmail.com
Plugin Icon 128x128 Better Click To Tweet
Version 4.7
Comparing to
See all releases

Code changes from version 4.6.2 to 4.7

assets/tinymce/js/tinymce-bctt.js CHANGED
@@ -24,12 +24,19 @@
24
  checked: true,
25
  name: 'viamark',
26
  value: true,
27
- text: editor.getLang( 'bctt.viaExplainer', 'Add via @YourTwitterName to this tweet'),
28
  label: editor.getLang( 'bctt.viaPrompt', 'Include "via"?'),
 
 
 
 
 
 
 
29
  }
30
  ],
31
  width: 800,
32
- height: 120,
33
  onsubmit: function( e ) {
34
 
35
  // bail without tweet text
@@ -46,6 +53,9 @@
46
  // check for via
47
  if ( e.data.viamark === false ) {
48
  bcttBuild += ' via="no"';
 
 
 
49
  }
50
 
51
  // close it up
24
  checked: true,
25
  name: 'viamark',
26
  value: true,
27
+ text: editor.getLang( 'bctt.viaExplainer', 'Add the username below to this tweet'),
28
  label: editor.getLang( 'bctt.viaPrompt', 'Include "via"?'),
29
+ },
30
+ {
31
+ type: 'textbox',
32
+ name: 'username',
33
+ label: editor.getLang( 'bctt.usernameExplainer', 'Which Twitter username?'),
34
+ multiline: false,
35
+ value: editor.getLang( 'bctt.userPrePopulated', ''),
36
  }
37
  ],
38
  width: 800,
39
+ height: 180,
40
  onsubmit: function( e ) {
41
 
42
  // bail without tweet text
53
  // check for via
54
  if ( e.data.viamark === false ) {
55
  bcttBuild += ' via="no"';
56
+
57
+ } else {
58
+ bcttBuild += ' username="' + e.data.username + '"';
59
  }
60
 
61
  // close it up
assets/tinymce/languages/bctt-mce-locale.php CHANGED
@@ -16,8 +16,10 @@ $strings =
16
  toolTip : "' . esc_js( _x( 'Better Click To Tweet Shortcode Generator', 'Text that shows on mouseover for visual editor button', 'better-click-to-tweet' ) ) . '",
17
  windowTitle : "' . esc_js( _x( 'Better Click To Tweet Shortcode Generator', 'Text for title of the popup box when creating tweetable quote in the visual editor', 'better-click-to-tweet' ) ) . '",
18
  tweetableQuote : "' . esc_js( _x( 'Tweetable Quote', 'Text for label on input box on popup box in visual editor', 'better-click-to-tweet' ) ) . '",
19
- viaExplainer : "' . esc_js( _x( 'Add via @YourTwitterName to this tweet', 'Text explaining the checkbox on the visual editor', 'better-click-to-tweet' ) ) . '",
20
  viaPrompt : "' . esc_js( _x( 'Include via?', 'Checkbox label in visual editor', 'better-click-to-tweet' ) ) . '",
 
 
21
  }
22
  );
23
  ';
16
  toolTip : "' . esc_js( _x( 'Better Click To Tweet Shortcode Generator', 'Text that shows on mouseover for visual editor button', 'better-click-to-tweet' ) ) . '",
17
  windowTitle : "' . esc_js( _x( 'Better Click To Tweet Shortcode Generator', 'Text for title of the popup box when creating tweetable quote in the visual editor', 'better-click-to-tweet' ) ) . '",
18
  tweetableQuote : "' . esc_js( _x( 'Tweetable Quote', 'Text for label on input box on popup box in visual editor', 'better-click-to-tweet' ) ) . '",
19
+ viaExplainer : "' . esc_js( _x( 'Add the username below to this tweet', 'Text explaining the checkbox on the visual editor', 'better-click-to-tweet' ) ) . '",
20
  viaPrompt : "' . esc_js( _x( 'Include via?', 'Checkbox label in visual editor', 'better-click-to-tweet' ) ) . '",
21
+ usernameExplainer : "' . esc_js( _x( 'Which Twitter username?', 'Help text for label in visual editor', 'better-click-to-tweet' ) ) . '",
22
+ userPrePopulated : "' . esc_js( get_option( 'bctt-twitter-handle' ) ) . '",
23
  }
24
  );
25
  ';
bctt_options.php CHANGED
@@ -8,6 +8,20 @@ add_filter( 'tiny_mce_version', 'refresh_mce' );
8
  // Add button to visual editor
9
  include dirname( __FILE__ ) . '/assets/tinymce/bctt-tinymce.php';
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  // Add Settings Link
12
  add_action( 'admin_menu', 'bctt_admin_menu' );
13
 
@@ -45,6 +59,7 @@ function bctt_settings_page() {
45
  _e( 'Better Click To Tweet — a plugin by Ben Meredith', 'better-click-to-tweet' ); ?></h2>
46
 
47
  <hr/>
 
48
  <div id="bctt_admin" class="metabox-holder has-right-sidebar">
49
  <div class="inner-sidebar">
50
  <div id="normal-sortables" class="meta-box-sortables ui-sortable">
@@ -158,10 +173,11 @@ function bctt_settings_page() {
158
  <br class="clear"/>
159
  <em><?php $url = 'https://www.wpsteward.com';
160
  $link = sprintf( __( 'An open source plugin by <a href=%s>Ben Meredith</a>', 'better-click-to-tweet' ), esc_url( $url ) );
161
- echo $link; ?></em>
162
  </form>
163
  </div>
164
  </div>
 
165
  </div>
166
  </div>
167
  </div>
8
  // Add button to visual editor
9
  include dirname( __FILE__ ) . '/assets/tinymce/bctt-tinymce.php';
10
 
11
+ // instantiate i18n encouragement module
12
+ $bctt_i18n = new yoast_i18n(
13
+ array(
14
+ 'textdomain' => 'better-click-to-tweet',
15
+ 'project_slug' => '/wp-plugins/better-click-to-tweet/stable',
16
+ 'plugin_name' => 'Better Click To Tweet',
17
+ 'hook' => 'bctt_settings_top',
18
+ 'glotpress_url' => 'https://translate.wordpress.org/',
19
+ 'glotpress_name' => 'Translating WordPress',
20
+ 'glotpress_logo' => 'https://plugins.svn.wordpress.org/better-click-to-tweet/assets/icon-256x256.png',
21
+ 'register_url ' => 'https://translate.wordpress.org/projects/wp-plugins/better-click-to-tweet/',
22
+ )
23
+ );
24
+
25
  // Add Settings Link
26
  add_action( 'admin_menu', 'bctt_admin_menu' );
27
 
59
  _e( 'Better Click To Tweet — a plugin by Ben Meredith', 'better-click-to-tweet' ); ?></h2>
60
 
61
  <hr/>
62
+ <?php do_action( 'bctt_settings_top' ); ?>
63
  <div id="bctt_admin" class="metabox-holder has-right-sidebar">
64
  <div class="inner-sidebar">
65
  <div id="normal-sortables" class="meta-box-sortables ui-sortable">
173
  <br class="clear"/>
174
  <em><?php $url = 'https://www.wpsteward.com';
175
  $link = sprintf( __( 'An open source plugin by <a href=%s>Ben Meredith</a>', 'better-click-to-tweet' ), esc_url( $url ) );
176
+ echo $link; ?></em>
177
  </form>
178
  </div>
179
  </div>
180
+
181
  </div>
182
  </div>
183
  </div>
better-click-to-tweet.php CHANGED
@@ -2,16 +2,18 @@
2
  /*
3
  Plugin Name: Better Click To Tweet
4
  Description: Add Click to Tweet boxes simply and elegantly to your posts or pages. All the features of a premium plugin, for FREE!
5
- Version: 4.6.2
6
  Author: Ben Meredith
7
  Author URI: https://www.wpsteward.com
8
  Plugin URI: https://wordpress.org/plugins/better-click-to-tweet/
9
  License: GPL2
10
  Text Domain: better-click-to-tweet
11
  */
 
12
  include 'bctt_options.php';
13
  include 'bctt-i18n.php';
14
 
 
15
  defined( 'ABSPATH' ) or die( "No soup for you. You leave now." );
16
 
17
  /*
@@ -83,11 +85,21 @@ function bctt_shortcode( $atts ) {
83
  extract( shortcode_atts( array(
84
  'tweet' => '',
85
  'via' => 'yes',
 
86
  'url' => 'yes',
87
  'nofollow' => 'no',
88
  ), $atts ) );
89
-
90
- $handle = get_option( 'bctt-twitter-handle' );
 
 
 
 
 
 
 
 
 
91
 
92
  if ( function_exists( 'mb_internal_encoding' ) ) {
93
 
@@ -108,7 +120,7 @@ function bctt_shortcode( $atts ) {
108
  $handle_code = '';
109
 
110
  }
111
-
112
  if ( $via != 'yes' ) {
113
 
114
  $handle = '';
2
  /*
3
  Plugin Name: Better Click To Tweet
4
  Description: Add Click to Tweet boxes simply and elegantly to your posts or pages. All the features of a premium plugin, for FREE!
5
+ Version: 4.7
6
  Author: Ben Meredith
7
  Author URI: https://www.wpsteward.com
8
  Plugin URI: https://wordpress.org/plugins/better-click-to-tweet/
9
  License: GPL2
10
  Text Domain: better-click-to-tweet
11
  */
12
+ include 'i18n-module.php';
13
  include 'bctt_options.php';
14
  include 'bctt-i18n.php';
15
 
16
+
17
  defined( 'ABSPATH' ) or die( "No soup for you. You leave now." );
18
 
19
  /*
85
  extract( shortcode_atts( array(
86
  'tweet' => '',
87
  'via' => 'yes',
88
+ 'username' => 'not-a-real-user',
89
  'url' => 'yes',
90
  'nofollow' => 'no',
91
  ), $atts ) );
92
+
93
+ //since 4.7: adds option to add in a per-box username to the tweet
94
+ if ( $username != 'not-a-real-user' ) {
95
+
96
+ $handle = $username;
97
+
98
+ } else {
99
+
100
+ $handle = get_option( 'bctt-twitter-handle' );
101
+
102
+ }
103
 
104
  if ( function_exists( 'mb_internal_encoding' ) ) {
105
 
120
  $handle_code = '';
121
 
122
  }
123
+
124
  if ( $via != 'yes' ) {
125
 
126
  $handle = '';
i18n-module.php ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * This class defines a promo box and checks your translation site's API for stats about it, then shows them to the user.
5
+ */
6
+ class yoast_i18n {
7
+
8
+ /**
9
+ * Your translation site's logo
10
+ *
11
+ * @var string
12
+ */
13
+ private $glotpress_logo;
14
+
15
+ /**
16
+ * Your translation site's name
17
+ *
18
+ * @var string
19
+ */
20
+ private $glotpress_name;
21
+
22
+ /**
23
+ * Your translation site's URL
24
+ *
25
+ * @var string
26
+ */
27
+ private $glotpress_url;
28
+
29
+ /**
30
+ * Hook where you want to show the promo box
31
+ *
32
+ * @var string
33
+ */
34
+ private $hook;
35
+
36
+ /**
37
+ * Will contain the site's locale
38
+ *
39
+ * @access private
40
+ * @var string
41
+ */
42
+ private $locale;
43
+
44
+ /**
45
+ * Will contain the locale's name, obtained from yoru translation site
46
+ *
47
+ * @access private
48
+ * @var string
49
+ */
50
+ private $locale_name;
51
+
52
+ /**
53
+ * Will contain the percentage translated for the plugin translation project in the locale
54
+ *
55
+ * @access private
56
+ * @var int
57
+ */
58
+ private $percent_translated;
59
+
60
+ /**
61
+ * Name of your plugin
62
+ *
63
+ * @var string
64
+ */
65
+ private $plugin_name;
66
+
67
+ /**
68
+ * Project slug for the project on your translation site
69
+ *
70
+ * @var string
71
+ */
72
+ private $project_slug;
73
+
74
+ /**
75
+ * URL to point to for registration links
76
+ *
77
+ * @var string
78
+ */
79
+ private $register_url;
80
+
81
+ /**
82
+ * Your plugins textdomain
83
+ *
84
+ * @var string
85
+ */
86
+ private $textdomain;
87
+
88
+ /**
89
+ * Indicates whether there's a translation available at all.
90
+ *
91
+ * @access private
92
+ * @var bool
93
+ */
94
+ private $translation_exists;
95
+
96
+ /**
97
+ * Indicates whether the translation's loaded.
98
+ *
99
+ * @access private
100
+ * @var bool
101
+ */
102
+ private $translation_loaded;
103
+
104
+ /**
105
+ * Class constructor
106
+ *
107
+ * @param array $args Contains the settings for the class.
108
+ */
109
+ public function __construct( $args ) {
110
+ if ( ! is_admin() ) {
111
+ return;
112
+ }
113
+
114
+ $this->locale = get_locale();
115
+ if ( 'en_US' === $this->locale ) {
116
+ return;
117
+ }
118
+
119
+ $this->init( $args );
120
+
121
+ if ( ! $this->hide_promo() ) {
122
+ add_action( $this->hook, array( $this, 'promo' ) );
123
+ }
124
+ }
125
+
126
+ /**
127
+ * This is where you decide where to display the messages and where you set the plugin specific variables.
128
+ *
129
+ * @access private
130
+ *
131
+ * @param array $args
132
+ */
133
+ private function init( $args ) {
134
+ foreach ( $args as $key => $arg ) {
135
+ $this->$key = $arg;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Check whether the promo should be hidden or not
141
+ *
142
+ * @access private
143
+ *
144
+ * @return bool
145
+ */
146
+ private function hide_promo() {
147
+ $hide_promo = get_transient( 'yoast_i18n_' . $this->project_slug . '_promo_hide' );
148
+ if ( ! $hide_promo ) {
149
+ if ( filter_input( INPUT_GET, 'remove_i18n_promo', FILTER_VALIDATE_INT ) === 1 ) {
150
+ // No expiration time, so this would normally not expire, but it wouldn't be copied to other sites etc.
151
+ set_transient( 'yoast_i18n_' . $this->project_slug . '_promo_hide', true );
152
+ $hide_promo = true;
153
+ }
154
+ }
155
+
156
+ return $hide_promo;
157
+ }
158
+
159
+ /**
160
+ * Generates a promo message
161
+ *
162
+ * @access private
163
+ *
164
+ * @return bool|string $message
165
+ */
166
+ private function promo_message() {
167
+ $message = false;
168
+
169
+ if ( $this->translation_exists && $this->translation_loaded && $this->percent_translated < 90 ) {
170
+ $message = __( 'As you can see, there is a translation of this plugin in %1$s. This translation is currently %3$d%% complete. We need your help to make it complete and to fix any errors. Please register at %4$s to help complete the translation to %1$s!', 'better-click-to-tweet' );
171
+ } else if ( ! $this->translation_loaded && $this->translation_exists ) {
172
+ $message = __( 'You\'re using WordPress in %1$s. While %2$s has been translated to %1$s for %3$d%%, it\'s not been shipped with the plugin yet. You can help! Register at %4$s to help complete the translation to %1$s!', 'better-click-to-tweet' );
173
+ } else if ( ! $this->translation_exists ) {
174
+ $message = __( 'You\'re using WordPress in a language we don\'t support yet. We\'d love for %2$s to be translated in that language too, but unfortunately, it isn\'t right now. You can change that! Register at %4$s to help translate it!', 'better-click-to-tweet' );
175
+ }
176
+
177
+ $registration_link = sprintf( '<a href="https://translate.wordpress.org/projects/wp-plugins/better-click-to-tweet/">%1$s</a>', esc_html( $this->glotpress_name ) );
178
+ $message = sprintf( $message, esc_html( $this->locale_name ), esc_html( $this->plugin_name ), $this->percent_translated, $registration_link );
179
+
180
+ return $message;
181
+ }
182
+
183
+ /**
184
+ * Outputs a promo box
185
+ */
186
+ public function promo() {
187
+ $this->translation_details();
188
+
189
+ $message = $this->promo_message();
190
+
191
+ if ( $message ) {
192
+ echo '<div id="i18n_promo_box" style="border:1px solid #ccc;background-color:#fff;padding:1em 2em;max-width:100%;min-height:220px;">';
193
+ echo '<a href="' . esc_url( add_query_arg( array( 'remove_i18n_promo' => '1' ) ) ) . '" style="color:#333;text-decoration:none;font-weight:bold;font-size:16px;border:1px solid #ccc;padding:1px 4px;" class="alignright">X</a>';
194
+ if ( isset( $this->glotpress_logo ) && '' != $this->glotpress_logo ) {
195
+ echo '<a href="https://translate.wordpress.org/projects/wp-plugins/better-click-to-tweet/"><img style="float: right;margin: 15px 5px 5px 5px;padding: 0 1em;width: 200px;" src="' . $this->glotpress_logo . '" alt="' . $this->glotpress_name . '"/></a>';
196
+ }
197
+ echo '<h2>' . sprintf( __( 'Translation of %s', 'better-click-to-tweet' ), $this->plugin_name ) . '</h2>';
198
+
199
+ echo '<p>' . $message . '</p>';
200
+ echo '<p><a href="https://translate.wordpress.org/projects/wp-plugins/better-click-to-tweet/">' . __( 'Register now &raquo;', 'better-click-to-tweet' ) . '</a></p>';
201
+ echo '</div>';
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Try to find the transient for the translation set or retrieve them.
207
+ *
208
+ * @access private
209
+ *
210
+ * @return object|null
211
+ */
212
+ private function find_or_initialize_translation_details() {
213
+ $set = get_transient( 'yoast_i18n_' . $this->project_slug . '_' . $this->locale );
214
+
215
+ if ( ! $set ) {
216
+ $set = $this->retrieve_translation_details();
217
+ set_transient( 'yoast_i18n_' . $this->project_slug . '_' . $this->locale, $set, DAY_IN_SECONDS );
218
+ }
219
+
220
+ return $set;
221
+ }
222
+
223
+ /**
224
+ * Try to get translation details from cache, otherwise retrieve them, then parse them.
225
+ *
226
+ * @access private
227
+ */
228
+ private function translation_details() {
229
+ $set = $this->find_or_initialize_translation_details();
230
+
231
+ $this->translation_exists = ! is_null( $set );
232
+ $this->translation_loaded = is_textdomain_loaded( 'better-click-to-tweet' );
233
+
234
+ $this->parse_translation_set( $set );
235
+ }
236
+
237
+ /**
238
+ * Retrieve the translation details from Yoast Translate
239
+ *
240
+ * @access private
241
+ *
242
+ * @return object|null
243
+ */
244
+ private function retrieve_translation_details() {
245
+ $api_url = trailingslashit( $this->glotpress_url ) . 'api/projects/' . $this->project_slug;
246
+
247
+ $resp = wp_remote_get( $api_url );
248
+ $body = wp_remote_retrieve_body( $resp );
249
+ unset( $resp );
250
+
251
+ if ( $body ) {
252
+ $body = json_decode( $body );
253
+ foreach ( $body->translation_sets as $set ) {
254
+ if ( $this->locale == $set->wp_locale ) {
255
+ return $set;
256
+ }
257
+ }
258
+ }
259
+
260
+ return null;
261
+ }
262
+
263
+ /**
264
+ * Set the needed private variables based on the results from Yoast Translate
265
+ *
266
+ * @param object $set The translation set
267
+ *
268
+ * @access private
269
+ */
270
+ private function parse_translation_set( $set ) {
271
+ if ( $this->translation_exists && is_object( $set ) ) {
272
+ $this->locale_name = $set->name;
273
+ $this->percent_translated = $set->percent_translated;
274
+ } else {
275
+ $this->locale_name = '';
276
+ $this->percent_translated = '';
277
+ }
278
+ }
279
+
280
+ }
readme.txt CHANGED
@@ -3,8 +3,8 @@ Contributors: ben.meredith@gmail.com
3
  Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HDSGWRJYFQQNJ
4
  Tags: click to tweet, twitter, tweet,
5
  Requires at least: 3.8
6
- Tested up to: 4.4
7
- Stable tag: 4.6.2
8
  License: GPLv2 or later
9
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
10
 
@@ -109,6 +109,14 @@ I want to maximize the usefulness of this plugin by translating it into multiple
109
 
110
  == Changelog ==
111
 
 
 
 
 
 
 
 
 
112
  = 4.6.2 =
113
  * Removed extra (old and unused) js file.
114
  * changed some back-end links to go to my new page, www.wpsteward.com
3
  Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HDSGWRJYFQQNJ
4
  Tags: click to tweet, twitter, tweet,
5
  Requires at least: 3.8
6
+ Tested up to: 4.5
7
+ Stable tag: 4.7
8
  License: GPLv2 or later
9
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
10
 
109
 
110
  == Changelog ==
111
 
112
+ = 4.7 =
113
+ * added the ability to change the "via" addendum on a per-box basis using the new "username" shortcode attribute. The default behavior is (still) to go with the username you saved on the settings page.
114
+ * (non-geek explanation of that first point) Now if you have a guest post by @KanyeWest, your Better Click To Tweet box can add "via @KanyeWest" automatically to your reader's tweets!
115
+ * made some changes to the toolbar popup in the visual editor to facilitate the new "username" attribute, limiting confusion and causing much rejoicing.
116
+ * Made unsuccessful attempt at getting Kanye West to guest post as the ultimate demonstration of the new feature.
117
+ * Tested for compatibility with the upcoming WordPress 4.5, and I don't mean to sound arrogant, but it pretty much NAILS compatibility with 4.5.
118
+ * Added a module that shows up when a user is using WordPress in a language for which there is incomplete (or non-existent) translations for this plugin. For users where there is a complete (+90%) translation, nothing will show up. But for users where the translation is incomplete, they'll be encouraged to help with the translation efforts!
119
+
120
  = 4.6.2 =
121
  * Removed extra (old and unused) js file.
122
  * changed some back-end links to go to my new page, www.wpsteward.com