Jump to content
View in the app

A better way to browse. Learn more.

Invision Community

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Invision Community Blog

The IPS Social Suite stores skin and language (and some other stuff) in xml files which are imported into the database at installation and upgrade. The reason we do it this way is so of course, you can export skins and languages and install them on other sites or distribute them via the IPS Marketplace.

I'm not the biggest fan of PHP's XML handling at the best of times (it would seem whoever wrote the SimpleXML class and I would disagree on the definition of "simple") and had already changed some of the places we use XML to use JSON instead, but these skins and languages are particularly difficult because they can get huge. Handling these large files, especially when combined with sub-par servers can lead to memory exhaustion or maximum execution time timeouts. Our current method was taking both too much memory, and too much time in a single HTTP request.

So we had to come up with something better. At first we considered splitting the XML file into several, but that would mean either requiring people installing a new skin/language to upload multiple files (which we deemed not acceptable) or compressing them in some way (which is another of PHP's weaknesses, and would have just created another problem). We also researched different markup languages, however found similar problems with everything. We also had to make sure that any solution didn't step on the toes of any of our other 4.0 goals, in particular we want to completely eliminate writing cache files to disk so as to better support cluster environments.

The solution was two-fold. To resolve the memory problem, we decided to use the XMLReader and XMLWriter classes - these are non-cached, forward-only classes for reading and writing XML. Rather than store the document in memory, you can only read/write one node at a time, and can only move forward, not backwards. Even with this approach though, we needed to account for the fact that importing is an intensive operation anyway, and one that needs to be staggered. To accommodate this, we wrote a simple AJAX-based "redirector", which continuously fires HTTP requests at a PHP controller until the controller reports the import is finished (it displays a fancy progress bar to the user while this is happening).



We decided to make the AJAX redirector a helper class so that it can be used elsewhere and by third-party developers (rather than our current approach in some areas of making the user sit through loads of physical redirects). The code is really simple:

IPSOutput::i()->output = new IPSHelpersMultipleRedirect( /* Query string which will take us back to this controller */ 'app=foo&module=bar&controller=baz&do=process', /* Code to run for each HTTP request */ function( $data ) { // If we're not done yet... return array( $data, // Data to get passed back to this function, "Processing...", // Message to show (via AJAX) to the user 50, // [Optional] Number between 1 and 100 to indicate progess for progress bar ); // If we're done return NULL; }, /* Code to run when we're done */ function() { // Code that runs when we're done } );
  • 8,942 views
We have a few updates to our services to share with you.

Community in the Cloud

For over 11 years IPS has provided hosting services for clients that want a turn-key approach to their online community. Over time we have become more and more focused on community hosting solutions so it seemed like a good time to drop the older "hosting" term and adopt a new name for our service: Community in the Cloud. Granted it's the buzzword of the day but we were in the cloud before the cloud was a term :smile:.

Right now it's all that you had before but presented in a much easier to understand format. Check out our new information page: http://www.invisionpower.com/cloud-pricing

This name and presentation change is just step one. We will soon be increasing our storage quotas and have some other great changes on the way!

New Support Package

We often get clients who are looking for a higher level of support beyond just tickets. They want training, schedule upgrade service, consultations, and more. Of course offering that level of support is intensive and in the past we have always custom-quoted such services. Now to streamline we we have created a new Premium Support package that includes:

Implementation
Scheduled installation time Initial training & consultation by phone or live chat Post-deployment best practices training Custom migration from other platforms* Custom skin design* Custom single sign on (SSO)*

SupportSame business day ticket response Scheduled upgrade times Custom skin upgrades between versions* Security updates applied before public release

Monthly MaintenanceLogs checked for signs of problems Advise and schedule if upgrades are available Database maintenance Settings reviewed for optimal performance Best practices reviews

* Custom services may incur additional fees

The new Premium Support package is $500 every 6 months and is available for purchase or upgrade today. If you have any questions feel free to email [email protected] and we will be happy to help.

Transfer Promotion

If you are interested in moving to IPS Community in the Cloud we are offering a promotion that should make now the best time to make the switch.

From now until 1 September 2013 we will offer free transfers and free conversions. This means if you are already using IPS Community Suite on your own servers but want to switch to the CiC we will move your data for you. It also means that if you are using a different community software provider and are ready to upgrade to CiC we will both transfer your data and convert it using one of our pre-made converters.
  • 12,273 views
Charts and graphs are an essential tool in modern web applications. The API we use for displaying charts and graphs in IP.Board presently is something we wrote in-house during IP.Board 2.x - it uses the PHP GD library to generate an image representing a chart. At the time, it was pretty amazing, but as times have changed a number of libraries for generating much more visually appealing and interactive graphs have emerged. For IPS Social Suite 4.0 we've decided to retire our GD-based charting library and embrace something a bit more modern and familiar for developers to work with.

After looking at a number of different solutions, we decided to use Google Charts (although wrapped with a simple gateway PHP class). Google Charts look great, have great features, are commonly used (so we figure any third-party developers who want to add charts to their apps will be able to do so easily) and is a service provided for free with no API limitations.


I'll show you an example of how simple it is to create a chart in IPS Social Suite 4.0. Let's look at the code I might use to show a graph of the number of members registered over time:

/* Init Chart */ $chart = new IPSHelpersChart; /* Specify headers */ $chart->addHeader( "Date", 'date' ); $chart->addHeader( "Members", 'number' ); /* Add Rows */ $stmt = IPSDb::i()->build( array( 'select' => "COUNT(*) AS count, DATE_FORMAT( FROM_UNIXTIME( joined ), '%Y-%m-%d' ) as joined_date", 'from' => 'core_members', 'group' => 'joined_date', 'order' => 'joined DESC', ) ); $stmt->execute(); while ( $row = $stmt->fetch() ) { $chart->addRow( array( new IPSDateTime( $row['joined_date'] ), $row['count'] ) ); } /* Output */ IPSOutput::i()->output = $chart->render( 'LineChart', array( 'title' => "Registrations", ) );
As you can see, the code is extremely simple to understand and use. This is what the output looks like:

By hovering over any point I'll see a tooltip with the value at that point.

All of the chart types and options available in Google Charts are available to us. Let's make some changes to make the lines curved, get rid of the legend and show titles on each axis - I just change the last line of the code to:

IPSOutput::i()->output = $chart->render( 'LineChart', array( 'curveType' => 'function', 'hAxis' => array( 'title' => 'Date', ), 'legend' => array( 'position' => 'none', ), 'title' => "Registrations", 'vAxis' => array( 'title' => 'Number of registrations', ) ) );
And now my chart looks like this:

  • 12,347 views
Javascript is a key component of front-end web development - it's essential for a modern web app to provide a good user experience, and javascript is central to enabling that. Getting it right in 4.0 has been one of our goals from the start.

Problems

To begin, let's summarize some of the issues with javascript in the current 3.x series:
Lack of file organization (a single JS directory that contains dozens of unrelated script files) Different script types are bundled together (interface widgets, routes and full applications all in one place) Lack of modularity (each JS file is pretty much a floating island in how it might implement functionality, with no formalized structure) Simple things requiring code (how many times have you had to write half a dozen lines of JS just to launch a popup?) New dom nodes aren't initialized automatically (load some new HTML via ajax, and your popups won't work without manually hooking them up)


Resolving these problems informed the javascript framework that we've built for 4.0. It all ultimately comes down to organizing code better and making the lives of developers easier (ours and yours!).

The solution

Our solution has been to build a framework which is modularized and heavily event-driven. In most cases, modules never call each other directly, but instead communicate with events which may (or may not) be responded to by another module. More on this later.

The framework breaks down into four types of module:
Widgets - interface widgets like popups, tooltips and menus Utility modules - things like cookie handling or URL manipulation Controllers - modules which work on a particular dom node. More on these later. Models - modules which handle data. In the vast majority of cases this is simply fetching data from the server.

There's also 'interfaces', which are third party scripts like CKEditor, jQuery plugins and so forth. They aren't part of the framework, so I won't discuss them here.


The groundwork

Before getting to specific types of modules, we needed to lay the groundwork. Javascript 4.0 is modularized, with only a single global variable (ips) being created in the page. All other scripts are defined as modules, whether they are interface widgets, utilities or anything else. A module is defined as a function which returns an object containing public methods (the revealing module pattern, if you're interested). Here's an example module:
;( function($, _, undefined){ "use strict"; ips.createModule('ips.myModule', function () { // Private methods var _privateMethod = function () { }; // Functions that become public methods var init = function () { }, publicMethod = function () { }; // Expose public methods return { init: init, publicMethod: publicMethod } }); }(jQuery, _));
This pattern works well for our purpose, because it enables a module to contain private methods for doing internal work, while exposing only those methods which should be public to the outside world.

This example module could then be used like so:

ips.myModule.publicMethod();
So this keeps everything neatly organized, and ensures variables don't leak into the global scope (which we want to avoid at all costs). When the document is ready, the module is automatically initialized (though you can also have functions that execute before DOMReady if necessary).


Interface widgets

It's fair to say that interface widgets make up a large proportion of the JS used in our software - a web app such as ours has an intrinsic need for popups, menus, tooltips and so on. As mentioned above though, the big hinderance in 3.x is that these widgets have to be created manually (or, in a few simple cases, a special classname is added to an element to initialize it). This feels unnecessary when all the developer wants to do is show a simple widget that is otherwise standard.

To alleviate this hassle, interface widgets in 4.0 all support a data API. What this means is that any widget can be created simply by adding some parameters to an HTML element, and specifying some options. Need a dialog box that loads a remote page? Simply do:

<a href='...' data-ipsDialog data-ipsDialog-title='My dialog' data-ipsDialog-url='http://...'>Click me to open a dialog</a>
Or if you need a hovercard, just do:

<a href='...' data-ipsHover>This will launch a hovercard</a>
We already have around two dozen widgets built, covering everything from dialogs, menus and tooltips, to keyboard navigation, tab bars and autocomplete - all supporting initialization with data attributes.

Building your own widgets is easy - they are built as a module, and then simply define some settings to be accepted. The widget module does the rest. They can either be initialized when they're first seen in the dom (which you'd want for something like an image slider widget), or when an event occurs (such as hovering on a link, in the case of hovercards). Whenever new content is loaded into the page, widgets will be found and initialized automatically, too.

Most widgets emit events when certain things occur - when we get to Controllers, you'll see why that is useful.


Utilities

Utilities are simple modules that don't need much discussion. They simply provide methods which do something useful - for example, fetch/set a cookie, write to the user's local browser database, or handle timestamps.


Controllers

Controllers are the meat of the application. Whereas interface widgets are used (and reused) as dumb tools on a page, controllers provide the logic for particular elements, sections and pages. They would, for example, handle the interactions in the topic listing, or the interactions with a post. Notice the word interaction - controllers are specifically designed to deal with events on the page. In fact, that's almost all they do!

Controllers are initialized by specifying the controller name on an element, like so:

<div id='topic_list' data-controller='forums.topicList'> </div>
This div becomes the controller's scope. The controller can manipulate content inside the div, watch for events, and so on.

Controllers, in general, should be as specific and succinct - so simply specifying a page-wide controller then handling everything inside it is discouraged. If we take the topic list in forum view as an example:

<div id='topic_list' data-controller='forums.topicList'> <ul> <li data-controller='forums.topicRow'> ... </li> <li data-controller='forums.topicRow'> ... </li> <li data-controller='forums.topicRow'> ... </li> </ul> </div>
Each topic row might specify the forums.topicRow controller which handles locking, pinning, or marking that topic. The topic list itself might specify the forums.topicList controller, which handles sorting and loading more topics. By doing it this way, controllers become responsible only for a specific portion of the functionality, which keeps them lean and simple.


Controllers are entirely decoupled and cannot reference each other - which is by design, given that a controller is only interested and responsible for its own scope. To communicate, events are used. A controller can trigger events on the page, which other controllers, widgets and models might respond to (and in turn emit their own events).

Continuing the example above, let's assume one of our topic rows is being deleted. The forums.topicRow controller handles removing the HTML from the DOM, but it doesn't care what happens after that - it's not its responsibility. However, it emits a deletedTopic event to let the page know. The forums.topicList controller sees this event, and because it does care, it loads a new topic entry into the list. By using events like this, we can build interfaces that respond fluidly to user interactions while still maintaining separation of concerns.

So, how does a controller deal with events? Because we're using jQuery, event handling in controllers piggy-backs with the on and trigger methods. In the controller's initialize method (which is specifically for setting up event handlers), you simply do:

this.on( 'menuItemSelected', '#menuid', this.handleMenuClick );
Usually when setting up events in an object using jQuery, you need to use $.proxy to properly control the scope of this, but in controllers, this is handled for you automatically - you just specify the method name.

Notice the event we're observing here - menuItemSelected. This is an event that the ui.menu widget emits, and it illustrates how widgets and controllers can interact. Controllers can watch for events from widgets, then do something with the information given, all without ever directly referring to each other.

Triggering an event is similar:

this.trigger( 'doSomething', { color: 'yellow', size: 'big' });
This is the same syntax as jQuery's own trigger, except that the controller will ensure the parameters object is passed between different event handlers in the same chain. This will hopefully be clearer when you get your hands on it.


Models

Models are quite similar to controllers (they also use the special on and trigger methods), but their only purpose is to handle data. By decoupling data handling from controllers, we can centralize data getting/setting so that any controller can use it.

Let's say we have a user model, which handles data for the current user. It might have event handlers for adding a friend, for example, so when it sees the event addFriend, it handles it appropriately. Let's also assume we have a controller on each post in a topic, there's three posts, and that the controllers are observing the click event on the 'add friend' button. Here's the sequence of events:

(controller1) click 'add friend button'
(controller1) emit 'addFriend' event
(user model) adds a friend via ajax
(user model) emits 'addedFriend' event
(controller1) updates friend icon
(controller2) updates friend icon
(controller3) updates friend icon

Even though it was controller1 that requested that the model adds a friend, all controllers respond to the event the model emits and updates the friend icon in its own post. This again shows the power of using events as the primary communication system - anyone can respond, and the caller doesn't have to deal with maintaining associations.



Conclusion

So that's about it - the new JS framework in IPS4. Hopefully this in-depth post has covered everything you need to know at this stage. You'll be pleased to know that most of the framework and widgets are already documented, and that will be available when IPS4 hits beta.

Do note that everything covered here is subject to change or removal, as usual in our development blogs.

If you have any questions, feel free to ask!


  • 14,075 views
Sending email is an important part of almost any web software available today. Social Suite applications have varying needs for sending email. Examples include allowing users to confirm their email address during registration, advising users when there are updates regarding content they are following, and bulk mailing your member base to alert them to something important regarding your site. While functionally the email handler has not significantly changed in 4.0, the process has been vastly simplified, allowing you to send more consistent emails with far less code than in the past.

Building and sending emails

When sending email in the Social Suite, there are two primary types of email you might need to send: an email based on a pre-existing template that you swap out variables in, and a completely custom email where you define the body specifically for the current email being sent. Both of these scenarios are accounted for. First, let's take a look at the simpler and more common scenario where you send an email based on a template.

IPSEmail::buildFromTemplate( 'core', 'admin_spammer', array( $this ) )->send( IPSSettings::i()->email_in );
Here, we are building an email from a template ("admin_spammer" in the "core" application, more on that in a moment), passing the current object in for purposes of replacing out variables in the template, and then calling the send method to actually send the email, passing the recipient to it. The buildFromTemplate() method is a factory method that will create a new object of type IPSEmail. When calling the send() method, you can optionally pass a single email address to send the email too, an array of email addresses to send the email to, or a single instance of IPSMember or an array of IPSMember instances to send the email to. The library takes care of the rest.


The other type of email you may need to send is a completely custom email where you define the entire body without using a template. Bulk mails are a good example of this type of email. You can easily send these types of emails as well.

$email = IPSEmail::buildFromContent( $subject, $htmlBody, $plaintextBody ); $email->from = "[email protected]"; $email->send( "[email protected]" );
Here we call the factory method buildFromContent(), passing in the email subject, the HTML email body, and the plain text email body. You can optionally omit the plain text email body and it will be generated for you automatically.

We then specify the from address (which defaults to the suite's outgoing email address), and finally we call the send() method, passing in an email address.

While it is not recommended, you can create an email object without specifying either email body parameter, and specify these later if you have a need.


Further control and options

Beyond what is outlined above, you have fine-grain control over the details of the email being sent. The following example outlines more of the possible values you can specify when necessary

$email = IPSEmail::buildFromContent( "Hello world" ); $email->from = "[email protected]"; $email->fromName = "John Doe"; $email->subject = "Changed my mind to hello planet"; $email->useWrapper = FALSE; $email->headers['X-Mailer'] = "My cool app"; $email->headers['Priority'] = 3; $email->setBody( array( 'html' => "<html><body><b>My email</b></body></html>", 'plain' => "My email" ) ); $email->send( array( "[email protected]", "[email protected]" ), array( "[email protected]" ), array( "[email protected]" ) );
Most of the above should be self-explanatory. Just a few quick notes:
You can set the $useWrapper property to FALSE if you do not wish for the email to be wrapped in the standard default HTML and plaintext email wrappers. The wrappers primarily define the HTML structure (doctype, html tag, etc.) and also allow for automatic definition of the unsubscribe link. You can call the $unsubscribe property to manually define an unsubscribe link and the text to display. Alternatively, you can call the "buildUnsubscribe()" method (see below) to do this automatically and consistently. There is no point in doing this if you do not use the wrapper, however, which is why it is omitted in the example above (in this case you should manually include an unsubscribe link if appropriate). The first parameter passed to the send() method is the to addresses. The second is the CC addresses, and the third is the BCC addresses, if necessary.



Automatically handling an unsubscribe link

Links to handle unsubscribing from emails can be accommodated automatically with emails sent through the email handler. For bulk emails, the following method is called
$email->buildUnsubscribe( 'bulk', IPSMember::loggedIn() );
The first parameter is the 'type', which is 'bulk' in this case. The second parameter when specifying an unsubscribe link for a bulk email should be the member who is being emailed, as their personal details are used to generate a unique key allowing one-click unsubscribing.

When sending notification emails, you still call buildUnsubscribe() with the following parameters

$email->buildUnsubscribe( 'notification', 'my_notification_key' );
Instead of a one-click unsubscribe link being included, a link pointing to the user's notification preferences area will be included instead, highlighting the notification preference that caused the email to be sent.

Unsubscribe links are only automatically included in the outgoing email if you (1) call this method, and (2) use the default wrappers.


Email templates

While we will touch on this further in a future blog entry, email templates are now defined in a similar manner to HTML templates, and there is one template for the HTML version of the email and one template for the plaintext version of the email. This allows us to better tailor the email being sent dependent upon the content-type in order to provide the nicest experience for the user. The email subject is a language string based upon the email template name and is determined automatically by the email library when buildFromTemplate() is called.


Wrapping Up

While the email library still provides plenty of control to accomodate almost any scenario encountered within the Social Suite, for day to day needs the interface to send emails has been vastly simplified in order to provide a more consistent experience for both the user and the developers integrating applications within the Suite. We hope you find these small changes will make your jobs easier, without losing the control you have presently.
  • 5,770 views
I'd like to introduce two new areas we've been working on. These new areas are designed to support our developer community, while making it easier for our clients to get their custom projects taken care of.

Projects
http://community.invisionpower.com/resources/projects

The first new area is Projects. When you have a custom project for which you need a developer/designer, this new area will allow you to gather responses from developers interested in working with you.

Post your project details, choose an approximate budget and date, if applicable, and developers can then register their interest in the project. From there, you can contact developers to help you decide which to go with.

Projects you post will be open for responses for 30 days, after which they'll be closed. If you agree to work with a developer before that time, you can mark it as completed by clicking the Accept link next to the chosen developer's response. You'll be notified when new developers respond to your project, too.

IPS won't be involved in the communication between you and a developer in any way, so it's up to you to agree project details and exchange payment, if necessary, before work starts.

We hope this will become a handy tool to help match up customers and developers on custom projects. In time we'll add more features, such as the ability to review customers/developers when a project is complete.


Developer Profiles
http://community.invisionpower.com/resources/developers

The second new area is Developer Profiles. This new area gives developers a place to present themselves to potential customers. When a developer creates a profile, we'll automatically build a page for them that pulls in their Marketplace information, and gives them a way of highlighting one of their contributed files and a place to write about themselves.

As a customer, you can browse the listings to check out the developers in our community. If you've worked with a developer or just like their files, you can Recommend them by clicking the link on their page.

If a developer with a profile responds to a Project, we'll link their name to their developer profile so that you can find out more about them.



We hope these two new features will help foster more growth in the development community, making it easier for customers to find developers, and giving developers a central place to find potential work.

Check them out - and if you have any feedback, please do feel free to share it. We've already made many changes based on the feedback from our preview to developers, so now it's your turn :smile:

  • 9,056 views
Our proprietary Spam Service which was launched in 2009 has been a very popular feature for IP.Board license holders. This service (which is included at no additional cost for all active license holders) is queried during account registration on your IP.Board installation and will respond to the query with a flag to indicate the likelihood that the registration might be a spammer. You can control how your site should react to the various responses the service may return in the admin control panel, and combined with other anti-spam tools in IP.Board you can help prevent spam registrations from occurring on your site.

While the system is constantly "learning" and blocking new spam signups, we have performed some updates to the service recently that we feel will help the system respond even quicker and more reliably to spam account registrations.


First, Some Stats

For those interested, we have some interesting statistics to share with you.

To date, the spam service has responded to over 58 million requests! The service has responded to almost 163,000 requests in the last 24 hours alone, and the service continues to respond to between 5,000 and 10,000 more requests day over day.

A total of over 23 million user registrations have been blocked (i.e. a status code of 3 or 4 was returned by the spam service) to date. That's 23 million spammers we've helped you prevent from disrupting your community.


Quicker Responses

With the recent updates we performed, the system will more quickly respond to new spammer accounts than it did previously. It is important that the system do not treat a single report of a spammer as a permanent block on that account of course, however we identified several areas where the algorithms used could be tweaked to more quickly identify potential spammers and have performed these changes. Additionally, by use of decaying flags (treating newer reports with a higher priority than older accounts), the system can more quickly respond to new spammer threats.


Project Honeypot Integration

We have integrated our spam service with the popular Project Honeypot service. This means that all account registrations are checked through Project Honeypot, and the threat score that is returned from this service is used to help determine the likelihood of the IP address being associated with a spammer.


Stop Forum Spam Integration

In addition to integration with Project Honeypot, we have integrated the IPS Spam Service with Stop Forum Spam. Our spam service will check the Stop Forum Spam email address and IP address databases and use any information found here to help weight and score the likelihood that the registration is coming from a spammer.

It is important to note that we do not rely directly on the Stop Forum Spam data to determine a spammer status, but are instead using the data from this service to help weight the overall score based on all of the flags we have available.


Enterprise Spam Mitigation

If you are in a load balanced or cloud environment, you may wish to take advantage of this new offering which allows calls to the spam service from multiple origins using the same license key. Additionally, this service allows greater control over your spam mitigation service including weighting algorithm preferences and customized blacklists and whitelists. This addon service is available for $100/6 months. For more information, please contact our sales department.



Collectively we feel the changes we have made to the service will benefit all IPS customers who are making use of our IPS Spam Service. We hope these changes help your community fight the threat of spam more rigorously and more reliably than ever before.
  • 13,891 views
To round up our previous blog entries on the post editor in IPS Social Suite 4.0, there's just a few extra features not previously mentioned to show off.



@mentions

@mentions are a common feature on social media sites like Twitter and Facebook. If you type an @ symbol and then start typing the name of a friend, an autocomplete menu shows so you can quickly then click on the user and they'll receive a notification that they've been mentioned. In 4.0 you can do exactly this to mention any user.




Automatic Saving

Currently, when you're typing a post, every 2 minutes the content of the post is saved, so that if you accidentally navigate away from the page, your post content can be recovered. The content is saved by making an AJAX request.

In 4.0, we've rewritten this to use HTML5 web storage. This unloads this work to the browser, meaning no call needs to be made to the server. Because this is much more efficient, the save can be done much more frequently (every few seconds). This makes the autosave feature much more useful.

In addition, we've expanded the feature to support attachments. So if you've uploaded files, these too will be automatically recovered. Essentially if you're in the middle of typing a post and you refresh the page, everything will reappear exactly as you left it.


HTML Posting

If you allow some users (like administrators) to post arbitrary HTML, they will see an additional "Source" button on the editor. When clicked, this will show them the raw HTML for the post and they can manipulate it here


  • 18,705 views
Introduction

Joining my previous entries about content and uploading features in post editor in IPS Social Suite 4.0, I'd like to take you through the customisation features on the editor.


Toolbar layout

The buttons that appear on the toolbar are completely customisable in 4.0 and you can set different layouts for desktop, tablet and mobile (so that you don't show more buttons than the device can show).

This is what the management screen looks like:

(This is an unfinished design - the tabs won't be be like that in the final version.)

To move a button you just drag and drop. The buttons on the right allow you to add more rows or separators.


Clicking on a button brings up a dialog where you can adjust where and to whom it shows:



Adding Buttons

There are two ways to add a button to the editor.

The easiest way is to install a CKEditor plugin. CKEditor has loads of plugins, and installing is as easy as uploading the zip file from their site. Here's a screenshot of the symbol plugin being used:


The second way is similar to how custom BBCode currently works, you specify the HTML code to be added when the user clicks on the button. Manually created buttons can optionally have a dialog popup to ask for an option.


Design

Just as you can install CKEditor plugins by uploading the zip file, you can do exactly the same with CKEditor skins to change the design of the editor.
You then simply set for each skin on your community which CKEditor skin to use for it.


BBCode

Though no features in IPS4 insert BBCode-style tags into the editor (like is currently done for attachments, etc.) users can still type BBCode into the editor and it will work fine.

We've rewritten how BBCode is parsed to be much more secure and reliable and produce more standards-compliant HTML (for those who are interested, it parses the post content into a DOM Document and examines only the text nodes for BBCode tags, then either splits the nodes surrounding it and inserts one for block-level elements, or wraps all subsequent text nodes in the appropriate formatting element until the end BBCode is found).

The benefit to this is that there now no longer needs to be a "BBCode mode" - you can type BBCode straight into the editor, even complicated stuff like lists spanning multiple lines, and it comes out looking great.

The downside to this approach is that custom BBCodes can no longer be added through the Admin CP. However, as mentioned above, we now have the ability to add custom buttons to the editor which work in a much more intuitive way, and can do everything that custom BBCodes could and more. For those who really want to be able to add the ability for custom BBCode, we've isolated the method that returns the supported BBCode (and information needed to parse them) into a specific method so that custom BBCode can be added with a very simple hook specific to that purpose.


Conclusion

There's still one more blog entry to go in our series on the editor. To finish up I'll be showing off some cool special features including how you can post using regular HTML.
  • 24,592 views
Introduction

In the last blog entry I introduced some of the features in the post editor in IPS Social Suite 4.0. In this blog entry I'd like to show you the uploading features in the editor.


Using the "Image" and "Attachment" dialogs

Along the bottom of the editor there are two buttons that deal with uploading files: image and attachments. Both present a dialog which looks like this:

We decided to keep both an images and an attachments dialog as users wanting to insert an image will naturally look for the "Image" button - if however, you upload an image to the attachments dialog, it will work completely as expected.

The upload panel here is based on HTML5 which supports drag and drop uploading, if your browser doesn't support this, it will use Flash, Silverlight or Google Gears if you have any of those installed, and if not it will fallback to a HTML4 & JavaScript implementation (none of these support drag and drop, but instead you click the "Choose Files" button just as you do now - the label in the box will change to reflect this).

Uploaded files then show below the box (images will get a preview), and you can click on any to add them into the editor, or click the "Insert All" button. When you insert an attachment into the editor, it displays either the image if it's an image, or a link if it's anything else, just as it will actually appear in the post (rather than the current "[attach=XXX]" tag).

You can also of course delete the attachment, which will automatically remove it from the editor if you've already inserted it.

Video Demonstration


Quick drag-and-drop

In addition to interacting with the panels, if you're using a supported browser, you can drag and drop straight into the editor. It will automatically figure out whether the uploaded file(s) are images or other files and add them to the appropriate panel automatically.

Video Demonstration


Image URLs

In the image panel, there is an additional "From URL" tab which allows you to insert an image from a URL, as you type the URL a preview is shown, and you can optionally link to the image.

Video Demonstration


My Files

In IP.Board currently, there is a "My Media" button which allows you to insert content submitted either in other posts or elsewhere in the community (images in IP.Gallery or files in IP.Downloads for example) into the editor. In 4.0, this feature is found in the images and attachments dialogs.

Just with normal attachments, the content is inserted as it will be shown rather than the current "[sharedmedia=XXX]" tag.


Conclusion

Please let us know what you think of the uploading features in the comments. Remember though that we're only half way through our series on the 4.0 editor. In my next blog entry I'll be talking about customising the editor and the place of BBCode.
  • 18,729 views
Introduction

The post editor is undoubtably one of the most frequently used features of the IPS Social Suite as it's the way users submit content to your community and functionality has evolved dramatically from the early days of forum software which consisted of a plain textbox in which users would type BBCode into the feature-rich WYSIWG (What You See Is What You Get) editors prevalent on the web today.

For the IPS Social Suite 4.0 we really wanted to focus on making the editor as good as it can be: feature-rich, intuitive to users and highly customisable.

Over the next 4 blog entries I'm going to cover the functionality of the editor and related features (attachments, emoticons, etc.) and the customisation options that will be available in 4.0. The theme of each blog entry will be: Here's a screenshot of what it looks like (by default, on a desktop, more on that later): Quotes A feature added to IP.Board in the 3.x series was "visual quotes" - when adding a quote the box shows directly in the editor as it will show in the post, rather than as just normal text with a quote BBCode wrapped around. In 4.0 we've rewritten how this works to use a CSS3 based solution rather than JavaScript to keep the citation header attached to the quote, which makes the feature much more reliable and easy to use when splitting quote boxes, moving the cursor before or after the quote box or dealing with embedded quotes. Code We've also rewritten how code can be inserted into the editor to be more reliable. Adding code now initially brings up an editor which supports syntax highlighting: And after inserting, you'll see the code exactly how it will be displayed after posting and you can interact with it exactly how you'd expect: Spoilers Spoilers are now also visual (though of course they're not true WYSIWYG as you need to be able to see what you're typing) and a built-in feature enabled by default. When you click the spoiler button you'll be given a darkened box in which to type the content: The spoilers display in the post as the same coloured box with a "click to view" message. When clicked, the dark box fades away revealing the content underneath. Emoticons We've completely overhauled emoticon management in 4.0. You can now create multiple "groups", drag and drop to reorder and quickly upload loads of emoticons at once. When you click the emoticon button in the editor, you'll see a popup with an overview of all the groups. If your community only uses the default emoticons, this is big enough to show all the emoticons, however, if you like to install lots of emoticons, you can select any group from the dropdown menu to show all emoticons in that group, or use the search bar to find a particular emoticon. In addition, your most recently used emoticons will show right at the top. Embedded Media In 3.0 we introduced a media BBCode tag. By wrapping a URL to media such as YouTube or Vimeo videos in media BBCode tags, the correct embed code would automatically be worked out and embedded into the post. In later versions, we added automatic embedding support, so just pasting the URL into the post would cause the video to be embedded. This still works exactly the same in 4.0 with a couple improvements: Previews If you type the URL to a supported media in the "Link" dialog, a preview will be shown in the dialog - you can choose to insert the embedded media or just a regular link. If you insert the embedded media, it will show directly in the editor. oEmbed Currently URLs which gets picked up for media embedding and the output they produce are managed in the Admin CP. The idea was that this would allow admins to add support for additional services, however, it has caused some problems when for example, YouTube has added new URL formats or changed their embed code and they don't work on most communities until we release an update to change the default record for YouTube. is an API which allows you to make a call to the provider giving them the URL and they respond with the best embed code to use, if that URL can be embedded. In 4.0, if a URL for YouTube, Flickr, Vimeo, College Humor or Hulu is inserted, we make a call to their oEmbed provider to obtain the embed code, so it's always up to date. Conclusion This is just the first in our series of blog entries on the editor in IPS4. Please do let us know what you think in the comments, but remember there's much more to come!
[*]Content [*]Uploads [*]Customisation and BBCode [*]Special features



































Video Demonstration





Video Demonstration












Video Demonstration






oEmbed




  • 24,577 views
The following applications are available for beta testing:
IP.Board 3.4.5 IP.Blog 2.6.3 IP.Content 2.3.6 IP.Chat 1.4.4 IP.Downloads 2.5.4 IP.Gallery 5.0.5 IP.Nexus 1.5.8 IP.Calendar 3.3.4


This round of maintenance updates represents updates to all of our applications. At this point in time, there are zero open bug reports in our tracker (that are not flagged to be resolved in a future major version). We would like to encourage all interested users to perform as much testing of these apps as possible.

We will be upgrading our company forums early this coming week.

Please report any bugs you find with the beta to our bug tracker.

Please pay particular attention to the following areas:The editor, both within IP.Board (topics/posts) and in other areas (such as IP.Content articles, IP.Blog entries, etc.). Pay attention both to the editor itself (i.e. when typing out and formatting your post, and toggling the editor back and forth) and the final post that is submitted. Rebuilding posts, specifically upon upgrading from an older version of the software. Pay attention specifically to quotes to be sure they display correctly. Any IP.Nexus functionality that has to do with grouped renewals. Cancelling packages where renewals are grouped, reactivating those packages, changing those packages, etc.


If you find a bug, please be certain to report it to the bug tracker. When doing so, be sure to include as much information as possible that will allow us to reproduce the issue, including what browser you are using, what version of PHP is running on your server, whether this was an upgrade or a fresh installation, and so on. Screenshots are often helpful.
As with all beta releases from IPS, these releases are not supported by our technicians until the official final releases are publicly available in our client center. Please do not upgrade your live installation using these betas, as you may find no path between these builds and the final releases that we put out. We recommended, instead, to create a copy of your live board as a test installation, and upgrade your test installation instead.

All customers with active licenses can download the betas at: http://community.inv...ower.com/qa.php

Thanks in advance! We look forward to your feedback.
  • 113,116 views
At IPS not only do we create market leading products, we also use them heavily ourselves. In doing so we often identify problems and needs in the software very early on. Two of the minor changes we've introduced with Nexus 1.5.8 are a direct result of management and support staff feedback during their use of the product internally.

Separating Outgoing Email Addresses

In the next release of IP.Nexus we've introduced the ability to set up separate outgoing email addresses for billing and support related notifications. these in turn are also separate from the default suite outgoing address. What this means in practice is that you can set up your community with a "noreply@" address where replies are not desirable and you want to avoid having to deal with bounced emails. Your billing notifications could be set up from "billing@" and ticket notifications from "support@". Replies can then be received via email and directed to the appropriate departments automatically.

Account Credit Increase Item

Businesses often have a primary contact and a separate billing contact. For accounting purposes, it is easier for them to add 12 months worth of account credit to the main account. Currently, in order to do this an invoice needs to be generated for a miscellaneous charge. The alternate contact has to then log in and pay this invoice then notify us that they've made payment. Finally, we have to look up the invoice and then manually add the credit to the account.

In IP.Nexus 1.5.8 we have added a new invoice item type in the ACP.



When an invoice containing this item type is paid, the customer's account credit will be automatically increased without further intervention from a staff member.

These types of real world usage scenarios and feedback are vital when developing all of the products in the IPS Suite. Your feedback as well as our internal usage help us to build products based on what our customers actually require rather than what an isolated group of developers may think they require.

Do you have suggestions for any other other workflow improvements such as these? However small or seemingly trivial you think they may be we are always interested in hearing ways in which we can make these tasks that little bit easier.

  • 6,828 views
We are pleased to announce an update to our popular converter application and with it support for conversions from vBulletin 5 Connect.

This release has been particularly focused on converter stability and performance improvements. At the time of writing there are 0 open bug reports in the bug tracker.

Performance Improvements

Whilst data integrity will always be our number one priority we do often review and apply performance improvements where appropriate.

In a previous release we introduced a feature that would significantly reduce the time taken when selecting data from the source software but this was only applied to a select few products. We are happy to report this change has now been applied across all converters. For the technically curious, this eliminates the need to use expensive LIMIT clauses with high offsets when converting forums with large amounts of posts.

Additionally we identified a few areas that would benefit from extra indexes particularly in the area of looking up relational content.

Software Specific Improvements

Support for vBulletin5 Connect is now available and the following items are converted - Permissions, Groups, Members, Passwords, Forums, Topics, Posts, Attachments, BBCode, Profile Fields, Emoticons, Moderators, Friends, Ignored Users, Reputation, Ranks, Warn Logs

The Ning converter has been greatly overhauled and now works much more reliably.

Considering switching to IPS?

Now is a great time to switch to IPS. We have a full community suite of products and a great resource community in the Marketplace. For a limited time through May 15 you can use the coupon code SWITCH at checkout to take 10% off your order. Feel free to email [email protected] with questions or post in our pre-sales forum to get feedback from other clients.

  • 6,015 views
The IPS Marketplace is the place to go for plugins, skins, language packs, full applications, and other resources provided by the IPS community. Some resources are free and some have a small fee. It's a great way to find ways to personalize your community and expand its functions.


Some updates...

I wanted to share some general statistics on the Marketplace (yes, I'm channeling Apple here). Yesterday we reached a great milestone:

Since its inception, we have paid out over $250,000 to contributors. Yes over a quarter million dollars has been paid out to those that sell resources in the Marketplace!

What's even more exciting is the growth we are seeing. In fact over 40% of that total payout was done just this year! Because I love any excuse to play with Excel here's a chart showing growth trend:



We also now have over 500 individual contributors that are providing both free and paid resources in the Marketplace.

Improvements coming soon

We are in the process of doing some cleaning up to make resources easier to find. Expect to see new categories based on what things do (moderation tools, promotion, utilities, etc.) rather than what they are (hook, mod, app, etc.) which we think will make finding resources for your community even easier.


A new ability to allow us to feature more than one resource at a time has already been launched.

Take a moment to browse the IPS Marketplace and see if there are any resources that would benefit your community. If you find one don't forget to thank the contributor!


  • 9,583 views
IPS is always looking for ways to assist our clients in monetization and promotion of their community traffic. After we saw so many clients successfully using VigLink on their community we decided to bundle it directly in the AdminCP under the Community Enhancements section. Since then many clients have reported success in using their service. Many clients have also since looked into other monetization options and have started to really grow.

Information from VigLink:



If you want to give it a try simply visit your AdminCP and click Community Enhancements to turn VigLink on and start earning. IPS does benefit from this relationship however we do not take any of your commission of course.
  • 11,906 views
Introduction

I previously wrote a blog entry about building tables in IPS Social Suite 4.0. Similar to tables, we also have trees. Trees in many ways look and behave similarly to tables, but can be distinguished mainly by the fact that trees show a collection of objects in a fixed order (often, though not always the order can be changed by the administrator) whereas tables can show data sorted however you like at the time. An example of trees would be the list of forums in IP.Board.

Trees vary quite significantly in their individual implementations, for example:
Objects in a tree often have parent/child relationships. Sometimes this relationship is between the same type of object (for example, forums in IP.Board or categories in IP.Downloads), sometimes the relationship is between different types of object (for example, applications and modules, where modules are always the child of applications) and sometimes it's a mix of both (for example, packages in IP.Nexus are always children of package groups, but package groups may or may not be children of each other). Objects in a tree can usually, but not always be reordered by the administrator. Objects in a tree can sometimes be enabled and disabled without being deleted, for example applications and modules. Sometimes individual objects can't when the rest can (for example, you can't disable the "Core" application). Trees usually have controls for adding, editing, assigning permissions, duplicating and deleting objects, though a subset of these controls, or additional controls may be available (for example, you can't edit the permissions on a package in IP.Nexus, but you can run a purchase report, which you can't do in any other trees).


For IPS Social Suite 4.0, I wanted to create a central class to create trees - currently we duplicate a lot of functionality, and all our trees display differently (in some places, quite radically so), but without loosing any of the flexibility necessary given the various differences in each implementation. Because trees are both more complicated and flexible than tables, the method for creating one might seem complicated - however, when you compare it to having to write every part manually (including all the JavaScript) as was the case in IP.Board 3, you'll be shaving hours off of development time.

There are two ways you can create trees in IPS Social Suite 4.0. The most common way is when each item in the table has a record in the database. There are occasions when this isn't the case (for example, the developer center displays trees created from JSON objects), and the Tree class can handle these, but in this blog entry I'm going to show you the more common method.

Just like I did for tables, I'm going to take you through how I programmed a real-world example, specifically the tree for custom profile fields.



Creating the classes

Custom profile fields are arranged into groups, so I'm going to start by creating a tree that shows the groups. To do this, I need to create a class for the custom profile field groups, I'll set this class to extend IPSNodeModel which is an abstract class that provides most of the functionality we need. IPSNodeModel in turn extends a class called IPSPatternsActiveRecord which provides functionality to make a class an Active Record (by that I mean, an object of the class corresponds to a record in the database).

In this class I need to define a few variables - I'll explain them below, but this is the code I'll write:
/** * Custom Profile Field Group Node */ class _Group extends IPSNodeModel { /** * @brief [ActiveRecord] Multiton Store */ protected static $multitons; /** * @brief [ActiveRecord] Default Values */ protected static $defaultValues = NULL; /** * @brief [ActiveRecord] Database Table */ public static $databaseTable = 'core_pfields_groups'; /** * @brief [ActiveRecord] Database Prefix */ public static $databasePrefix = 'pf_group_'; /** * @brief [ActiveRecord] ID Database Column */ public static $databaseColumnId = 'id'; /** * @brief [Node] Node Title */ public static $nodeTitle = 'profile_fields'; }
$multitons and $defaultValues are required by the IPSPatternsActiveRecord class. We don't need to do anything with them other than declare them. $databaseTable tells the IPSPatternsActiveRecord class what database table the records we need are in. $databasePrefix tells the IPSPatternsActiveRecord class the prefix used on all database columns. This isn't necessary, but since all the columns in my table start with "pf_group_", putting it here will save me typing it out every time. $databaseColumnId tells the IPSPatternsActiveRecord class which column contains the primary key. $nodeTitle tells the IPSNodeModel class what language string to use as the title on my page that shows the tree.



Now I need to create a controller to display my tree - the developer center will set up the structure of this for me, then I just fill in the name of the class I just created:
namespace IPScoremodulesadminmembersettings; /** * Profile Fields and Settings */ class _profiles extends IPSNodeController { /** * Node Class */ protected $nodeClass = 'IPScoreProfileFieldsGroup'; }
Now I have a page which looks like this:





Customising the rows

You'll notice I now have two rows (because I have two rows in my database), but both are blank. This is because the IPSNodeModel class doesn't know what to use for the record title. Let's fix that by adding a method to our class:

/** * [Node] Get Node Title * * @return string */ protected function get__title() { $key = "core_pfieldgroups_{$this->id}"; return IPSLang::i()->$key; }
That code might seem a bit confusing, but note:
$this->id gets the value of the "pf_group_id" column in the database. This is because the IPSPatternsActiveRecord class provides us with a __get method for retrieving the database row values. This is handy if we want to be able to modify the value returned for whatever reason, as we can override that method. We're retrieving the value for a language key rather than some kind of title field in the database. This is because in IPS Social Suite 4, data like this will be translatable, so if you have more than one language you can display different values depending on the user's language choice.


So now we have this (I've clicked the dropdown arrow so you can see it's contents):



Most of this is okay, but permissions aren't relevant for custom profile field groups, so let's get rid of that. The IPSNodeModel class has methods for checking if the user has permission to each button it displays (which by default check ACP restrictions, which I'll explain more about later) - we can simply override the method which checks for that button so it always returns false:
/** * [Node] Does the currently logged in user have permission to edit permissions for this node? * * @return bool */ public function canManagePermissions() { return false; }
And now it's gone:





Making the buttons work

The first buttons I need to make work are the add on the "root" row, and the edit on each row below that. I'm going to ignore the add button on each record row for now as that is for adding a child record and we haven't got to that yet - it will start working automatically when we add support for children.

These buttons will display a form. In a previous blog entry I talked about our form helper class. I'm going to use this to build the add/edit form.

To do this, I'll add two methods to my class to display the form and to save it's values - here they are:

/** * [Node] Add/Edit Form * * @param IPSHelpersForm $form The form * @return void */ public function form( &$form ) { $form->add( new IPSHelpersFormTranslatable( 'pfield_group_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "core_pfieldgroups_{$this->id}" : NULL ) ) ) ); } /** * [Node] Save Add/Edit Form * * @param array $values Values from the form * @return void */ public function saveForm( $values ) { if ( !$this->id ) { $this->save(); } IPSLang::saveCustom( 'core', "core_pfieldgroups_{$this->id}", $values['pfield_group_title'] ); }
Most of that should be self explanatory - however in the blog entry about forms I didn't mention the Translatable class. If you have one language installed, this will just display a normal text field, however, if you have more than one, it will show one for each language. It then returns an array, which we can pass to IPSLang::saveCustom() to save the values.

This is what our form might look like if I have several languages installed:


Or, more commonly, if I just have one:



Now the add and edit forms are working, but since these are very small forms (they only have one input field) I'd like them to display in a modal popup rather than take the user to a new page (if the user has JavaScript disabled, a new page will do). To do this, I just add a property to my class:

/** * @brief [Node] Show forms modally? */ public static $modalForms = TRUE;
Next we have the copy and delete buttons. These will actually work by themselves (the central class will handle copying and deleting the records from the database), however, since we have translatable values, we need to make sure those too are copied and deleted appropriately. To do this, I'll override the two methods which handle copying and deleting:

/** * [ActiveRecord] Duplicate * * @return void */ public function __clone() { $oldId = $this->id; parent::__clone(); IPSLang::saveCustom( 'core', "core_pfieldgroups_{$this->id}", IPSDb::i()->buildAndFetchAll( array( 'select' => '*', 'from' => 'core_sys_lang_words', 'where' => array( 'word_key=?', "core_pfieldgroups_{$oldId}" ) ), 'lang_id', 'word_custom' ) ); } /** * [ActiveRecord] Delete Record * * @return void */ public function delete() { parent::delete(); IPSLang::deleteCustom( 'core', 'core_pfieldgroups_' . $this->id ); }
Search

You'll notice that the system has automatically added a search box at the top of the table. In order to make this work, we need to add a simple search method:

/** * Search * * @param string $column Column to search * @param string $query Search query * @param string|null $order Column to order by * @return array */ public static function search( $column, $query, $order ) { if ( $column === '_title' ) { $return = array(); foreach ( IPSLang::i()->searchCustom( 'core_pfieldgroups_', $query ) as $key => $value ) { try { $return[ $key ] = self::load( $key ); } catch ( Exception $e ) { } } return $return; } return parent::search( $column, $query, $order ); }
Making the rows re-orderable

The last thing I need to do to finish the handling of groups is make it so we can drag and drop to reorder them. To do this I just add another property to my class telling IPSPatternsActiveRecord which column contains the order ID:

/** * @brief [Node] Order Database Column */ public static $databaseColumnOrder = 'order';

ACP Restrictions

It's important of course to make sure we honour ACP restrictions. The easiest way to do this is to create individual ACP restrictions for add/edit/delete (this can be done in the developer center) with a common prefix, and then specify like so:

/** * @brief [Node] ACP Restrictions */ protected static $restrictions = array( 'app' => 'core', 'module' => 'membersettings', 'prefix' => 'profilefieldgroups_', );
The system will now look for ACP restrictions with the keys "profilefieldgroups_add", "profilefieldgroups_edit" and "profilefieldgroups_delete" when performing those actions.


Children

Let's recap what we have so far with a video:
http://screencast.com/t/TUBuuYBSBON
Now that we have groups sorted, we're going to create another class for the actual fields which will show as children. The process is almost exactly the same as for groups. Since the process is the same, I won't go through the process step-by-step, but here is the class I've written if you're interested:
Field.php

The only changes are:
I've declared two additional properties specifying the class name of the parent ("IPScoreProfileFieldsGroup") and which column contains the parent ID. I've declared an additional method to fetch an icon for the row so we can see what type of field this is. Just like we overwrote canManagePermissions for groups, I've also overridden canAdd in the same way, as you cannot add children to profile fields.


Now all we need to do is link them up. To do this, I add a property to my group class telling IPSNodeModel the name of the class which contains children:
/** * @brief [Node] Subnode class */ public static $subnodeClass = 'IPScoreProfileFieldsField';
The system will now automatically change the behaviour of our page in the following ways:
When clicking on a group, it will expand out to show the fields under it. The search box will include fields as well as groups in its results. When clicking the "Add" button for a group, it will show the field to add a field to that group. When clicking the "Copy" button for a group, you'll have the option to copy children too or not. When clicking the "Delete" button for a group, you'll have the option to move children elsewhere or delete them too. (This is my favourite feature) In addition to being able to drag and drop fields to reorder, you can drag a field out of one group and into another.

Here's a video:
http://screencast.com/t/5fQwgle3EX
  • 13,619 views
A few weeks ago, I posted a blog entry mentioning a new feature in 4.0 which aims to make development of applications within the IPS Social Suite (both for us and third party modification authors) easier. We focussed on managing the database schema in that blog entry and I'd now like to take you through the other features.


Modules



Two tabs (one for admin modules and one for front modules) allow you to view all modules and sections in your application. You can add modules (which will both insert it into the database and create the relevant files in the filesystem), change the default section for a module (which previously required a defaultSection.php file) and create new sections.

When you're creating a new section, the form looks like this (this is for creating a section for an admin module):


The "Type" field controls the code that will be placed in the file created for the section - the options are:
"Blank" - which will create the class with no other logic, so the section will be blank. "Table" which will create a class with a boilerplate for displaying a table. "Node Controller" - which will create a boilerplate for displaying a tree of containers such as IP.Board forums, IP.Downloads categories, IP.Nexus groups, etc. We've not posted how this class works, but a future blog entry will give more details.

No matter which type you select, the system will automatically generate a file, with a basic class structure already filled in, including ACP restrictions checks, etc.

The "Menu Tab" field is admin specific and controls under which tab in the Admin CP the section should show (for example, we have some stuff from the "core" app under the "Look & Feel" tab). Previously, one would have to edit the menu.xml file for the module to add sections, and making sections appear under tabs other the default application tab was very difficult - the new system does it for you.

The "ACP Restriction" field is also admin specific and allows you to select an existing ACP restriction which will control who can see the section. It also has a special "Create Restriction" option, which will cause the system to create a restriction, and use that. Previously, one would have to edit the permissions.xml file to create restrictions and then assign them in the menu.xml file.

Of course - you can completely bypass this feature and manually create your module folders and section files, but the addition of this feature makes the process much quicker.



Admin CP Menu

This tab contains a graphical representation of the data which was previously stored in menu.xml files.








Admin CP Restrictions

This tab contains a graphical representation of the data which was previously stored in permissions.xml files.





Extensions

Extensions are ways in which applications interact with one another. Previously, you would drop extension files in your applications "/extensions" folder, though there wasn't much reasoning to the structure of the directory, it was difficult to know what extensions were available, and sometimes understanding an extensions requirements was difficult.

In 4.0, the extensions directory is more structured - the format is owner app > extension type > extension file (so admin/group_form.php for example, is now core/GroupForm/*.php) so this tab provides a GUI for managing your applications extensions. Applications can also specify a boilerplate file for an extension, so you can see what extensions are available, and clicking the "add" button will create a file with a basic structure to get you going.





Settings

In 4.0, developers have much more control over how settings are presented, rather than all being dumped in the central "System Settings" table. With this, much of the data that was previously needed in settings.xml is no longer required, so we've simplified the process of creating setting to just providing a key and a default value.





Versions

The versions tab shows all of the application's versions and the database queries that the upgrader will run when upgrading to that version. It's sort of a combination of the versions.xml file and the setup folder.




Queries are automatically added as you modify the database schema. Naturally you can also manually add queries, or specify a custom PHP script to run in that upgrade step.
  • 11,085 views
Login Handlers are the different methods for logging into the IPS Social Suite. We currently support:
"Internal", which is for accounts created natively through the suite. Facebook Twitter Microsoft (this is currently referred to as "Windows Live", though they rebranded to "Microsoft Account" a short while ago) LDAP "IPS Connect", which is our SSO solution for connecting your site with other IPS Social Suite installations or third-party applications. A generic handler for any MySQL database you have access to.

In 4.0 we've made a number of changes to the Login Handlers which I wanted to mention.



Improved Password Encryption

We currently use a salted md5 hash for hashing passwords. md5 has been a popular password hashing technique for years - however, it is not the most secure hashing method.

md5 is designed to be computationally efficient (meaning generating a hash is quick). The problem with this is that if a server were ever compromised to the point that someone were able to gain access to a database containing passwords hashed using md5, and someone were to use a program to generate and hash different strings repeatedly until a match were found, the password could be worked out. One particularly well-known program claims to be able to make 5.6 billion md5 hashes per second with a relatively modern GPU. Even with our hashing method which includes multi-level hashing and a salt, this means, assuming an 8-character long password using only alphanumeric characters were used, a password could be calculated in about 3 days.

While I'm unaware of any cases of this actually happening, we want to make sure that our products are as secure as they can be. For this reason, in 4.0, we're migrating to Blowfish. Blowfish is a more cryptographically secure technique for generating hashes that is deliberately slow, meaning that even if your database were ever compromised, the passwords will still be secure.


New Login Handlers

In addition to the Login Handlers mentioned above, we've added support for Google and LinkedIn.


Improved Facebook and Twitter support

Currently, although you can log in with Facebook and Twitter, they're not treated on the back-end as true Login Handers. This is because of how Login Handlers in 3.x were designed (which was before such 3rd party login services were popular) in that they assumed you would provide a username (or email) and password directly into a form, and subsequently didn't accommodate the OAuth-style login processes.

Since we've rewritten the way Login Handlers are designed, this means we can treat Facebook and Twitter (and Google and LinkedIn which both also use OAuth) exactly the same as the rest.

Practically, this means you'll see Facebook and Twitter in the Login Handlers section of the Admin CP, and manage them as you would any other login method.


Updated Microsoft Support

Microsoft now support OAuth for login through them so we've updated to use that. In addition to being necessary for when they stop supporting the old way, it's much easier to set up for the administrator.
  • 27,029 views
We've just rebuilt IP.Board 3.4.4 for further beta testing.

Thanks to everyone who has tested this release so far and for reporting the bugs you've found. We've fixed a good portion of these and would like for you to update your test installations with the latest release.

All customers with an active IP.Board license can download the beta at: http://community.inv...ower.com/qa.php

Once you've uploaded all the files to your server, there's no need to run the upgrade system as the version numbers haven't changed. You will need to rebuild your languages and skins. There's instructions here on how to do that.

As always, please report any bugs you find with the beta to our bug tracker.

Please pay particular attention to using the editor with IP.Board 3.4.4. A very large focus was placed on resolving some of the outstanding bugs and complaints with the editor, and we would appreciate any testing you can perform in this area. Create new posts, edit existing posts, toggle between the editor modes - if you find any bugs, let us know.

As with all beta releases from IPS, IP.Board 3.4.4 is not supported by our technicians until it has been officially publicly released. Please do not upgrade your live installation using this beta, as you may find no path between this build and the final release that we put out. We recommended, instead, to create a copy of your live board as a test installation, and upgrade your test installation instead.

Thanks!


  • 7,469 views
IPS is seeking a knowledgeable and experienced individual to join its support team in an advanced support capacity.

Successful applicants will be responsible for answering general support tickets, including providing customers with information, troubleshooting issues not resolved as part of the general product support process and performing maintenance. You would be expected where appropriate to interact with other technicians and developers to resolve issues.

Requirements:

* Must be familiar with IPS applications.
* Must have advanced knowledge of PHP and MySQL.
* Must be able to effectively work remotely.
* English must be your primary language and you must possess strong verbal and written communication skills.

Preferable, but not required:

* Knowledge of server administration and experience working for a web hosting company.
* Knowledge of additional web technologies such as JavaScript, CSS, XML, etc.
* Experience of working in customer support.
* Experience working with the codebase or creating hooks/apps in the IPS Suite

Working hours are flexible and pay will be based on knowledge and experience. Due to the nature of the position, we require all applicants to be physically located in the United States. No exceptions to United States residency requirements.

Please contact [email protected] for more information on this position. Please include your salary requirements, availability and an overview of your experience.

We look forward to hearing from you!

  • 9,000 views
IP.Board 3.4.4 is now available for beta testing!

We have been hard at work on IP.Board 3.4.4, and following a good week of testing here on our company forums, we have built a downloadable IP.Board 3.4.4 package for you to test on your own servers. We appreciate any testing you can perform.

Please report any bugs you find with the beta to our bug tracker.

Please pay particular attention to using the editor with IP.Board 3.4.4. A very large focus was placed on resolving some of the outstanding bugs and complaints with the editor, and we would appreciate any testing you can perform in this area. Create new posts, edit existing posts, toggle between the editor modes - if you find any bugs, let us know.

As with all beta releases from IPS, IP.Board 3.4.4 is not supported by our technicians until it has been officially publicly released. Please do not upgrade your live installation using this beta, as you may find no path between this build and the final release that we put out. We recommended, instead, to create a copy of your live board as a test installation, and upgrade your test installation instead.

All customers with an active IP.Board license can download the beta at: http://community.invisionpower.com/qa.php

Thanks in advance! We look forward to your feedback.
  • 40,090 views
The eagle eyed among you may have spotted that we've just upgraded our company forums to IP.Board 3.4.4.

We routinely do this during a development cycle so that we can get some extended testing prior to a beta release. When we write new features and fix bugs we do test ourselves but of course we can't replicate the testing hundreds of active users with all the different browser and operating system combinations can offer.

The focus of 3.4.4 has been to further stabilise the editor. We've made great improvements since the initial release of 3.4.0 but we're aware that there are a handful of issues remaining which we want to get licked for this release.

If you have a few moments spare, we'd appreciate it if you could test out the editor, either by creating a post in the test forum or just by being more aware of any quirks or issues when making posts normally.

Anything you spot, can you please report into our bug tracker with as much detail as you can.

Thanks!


  • 9,742 views
There's a table in the Admin CP of the IPS Social Suite that I really like - the members table. It has some really cool options - you can reorder the data just by clicking on a column head; you can quickly search for a member by typing a name into a search box at the top; there's some filter options to quickly show banned, locked, spam and validating members; and there's an advanced search form to search for members based on practically any criteria.

It would be great if these features were available elsewhere. So much like we did for forms, we decided to create a central helper class for building tables.

To demonstrate how it works, I'm going to go through, step by step, how I recreated the Admin CP members table in IPS 4.


It starts with one line to create the table, and another to pass it to the output class:

/* Create the table */ $table = new IPSHelpersTableDb( 'core_members', 'app=core&module=members&section=members' ); /* Display */ IPSOutput::i()->output = IPSOutput::i()->getTemplate( 'global' )->block( 'members', $table );


With just those two lines, you'll see this:


Some things to note:
We're calling IPSHelpersTableDb - the "Db" part indicates that the source of data for our table is a database table. There are other classes to use, for example, a JSON document as the data source. We pass it the name of our database table (or for the other classes, whatever the data source is) and the query string part of the URL where we're going to be displaying this (which we need to build the links and AJAX calls). I'm passing it to the output through a template called "block" which simply adds the dark-blue bar at the top, which isn't actually part of the table itself, and some padding. The "members" parameter is the key for the langauge string to use in that dark-blue bar. I'm passing $table directly to the template - the helper class has a __toString method which renders the table, so the output class thinks it's been given a normal string.




The first obvious thing is that we're showing all the columns in the database table, which obviously we don't want. So let's add another line to specify which columns we want:
$table->include = array( 'name', 'email', 'joined', 'member_group_id', 'ip_address' );


In this example, I'm giving the helper class a list of columns to include - I could alternatively pass a list of columns to exclude, if that would be more appropriate.

The output is now this:


Some things to note:
It's worked out pagination itself. When you click a pagination link, the contents of the table will update with AJAX, including changing your browser's URL (unless you have JavaScript disabled of course, in which case it will work like a normal link). Pagination defaults to 25 results per page, but you can change that just by changing a property in the class. All the columns are clickable, which will resort the results. You can sort any column ascending or descending. Resorting will also update with AJAX (including changing your browser's URL), unless JavaScript is disabled.




I want the headers to display something more meaningful than the column name. The system will automatically look for language strings which match the column name - you can also optionally specify a prefix, and it'll look for langauge strings which match that followed by the column name.

Let's specify a prefix:
$table->langPrefix = 'members_';

And I'll then create some language strings that match that (so "members_name", "members_email", etc.).

The output is now this:





Next - we need to change how we display some of those values. The joined date and the group are displaying the raw values from the database, but we want something more meaningful than that.

To format the values, we simply create an array of lambda functions - one for each we want to format:


$table->parsers = array( 'joined' => function( $val, $row ) { return IPSDateTime::ts( $val )->localeDate(); }, 'member_group_id' => function( $val, $row ) { return IPSMemberGroup::load( $val )->formattedName(); } );


I'm also going to add one additional line to specify the "main" column, which applies some additional styles:

$table->mainColumn = 'name';


The output is now this:


Some things to note:
I'm using the IPSDateTime class to format the joined date. The ts method in this is a factory method which takes a UNIX timestamp and returns an object of IPSDateTime. IPSDateTime extends DateTime, so all the features of that class are available to us. The localeDate method returns a string with the date formatted appropriately according to user's locale. The IPSMemberGroup::load call being executed for each result may look like it might be resource intensive, but it caches objects it creates, so it's only actually "loading" each group once.





Now I want to add a column with the user's photo. There isn't a single "photo" column in the database we can use for this (since the photo could be one they uploaded, a photo from their Facebook account if they're using Facebook Connect, a Gravatar image, or some other things), we need to use a method in the IPSMember class.

This isn't a problem. I can simply add an element to our list of fields to include and add that into the parsers.
$table->include = array( 'photo', 'name', 'email', 'joined', 'member_group_id', 'ip_address' );

$table->parsers = array( 'photo' => function( $val, $row ) { return IPSMember::constructFromData( $row )->photo('mini'); },

I'll also want to specify that we cannot use the photo column for sorting:


$table->noSort = array( 'photo' );


The output is now this:



Some things to note:
Since this isn't a value which exists in the database, the value of $val in the lambda function will be NULL, however, $row has all the data for that record. We're not using IPSMember::load to get the member object, since that would execute an additional query for every result, which would be resource intensive, and unnecessary since we already have that data. Instead, we use the constructFromData method and pass it the row from the database.




Next, I want to specify the default sorting. This is done with just two lines of code:
$table->sortBy = $table->sortBy ?: 'joined'; $table->sortDirection = $table->sortDirection ?: 'desc';


The output is now this:





Now, I want to add a quick search box. All we need to do is specify which column the quick search should look at:

$table->quickSearch = 'name';


The output is now this:


Some things to note:
As you type, results are obtained with AJAX. You can page through your results (the number of pages will update automatically) and reorder your results by clicking the headers without loosing your search.




I also want to allow more advanced search options - like to search by email address, or joined date. To do this, I create a new array:
$table->advancedSearch = array( 'member_id' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'email' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'ip_address' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'member_group_id' => array( IPSHelpersTableSEARCH_SELECT, array( 'options' => $groups ), function( $val ) { return array( 'member_group_id=? OR ? IN(mgroup_others)', $val, $val ); } ), 'joined' => IPSHelpersTableSEARCH_DATE_RANGE, );


To explain what's going on here:
The keys are the columns we're letting the user search on. The values are usually a constant indicating the type of search that is appropriate for that column. The member_group_id element is a bit more complicated. It has to specify an array of options (I've omitted the code to generate $groups in this snippet, but it'll be at the end of this blog entry), and, because we need to search both primary and secondary groups based on the value, there's a lambda function to get the proper WHERE clause for the query.


Now, next to the quick search box there's a button which will bring up a modal popup (or just take you to a new page if JavaScript is disabled) which looks like this:


Some things to note:The date entry boxes use the HTML5 date input type:

If your browser doesn't support that, there's a JavaScript fallback:

And if you're really awkward and are using a browser that doesn't support the HTML5 date input type and have JavaScript disabled, you'll see a regular text box where you can enter a date in practically any format, and it'll work it out. After performing the search, you can reorder your results by clicking the headers without loosing your search.





Now, I want to add some filters so you can quickly see banned, spam, locked and validating members. To do this, you create an array simply specifying the WHERE clause to use in the query for each filter:
/* Filters */ $table->filters = array( 'members_filter_banned' => 'member_banned=1', 'members_filter_locked' => 'failed_login_count>=' . (int) IPSSettings::i()->ipb_bruteforce_attempts, 'members_filter_spam' => '(members_bitoptions & ' . IPSMember::$bitOptions['bw_is_spammer'] . ') != 0', 'members_filter_validating' => 'v.lost_pass=0 AND v.vid IS NOT NULL' );

For this though, I'll also need to join the core_validating database table, so we add one more line for that:

$table->joins = array( array( 'from' => array( 'core_validating' => 'v' ), 'where' => 'v.member_id=_0.member_id' ) );


The output is now this:


Some things to note:
The helper class will add the "All" filter automatically. It's getting the word to use for the filter by looking for a language string with the same key as the key in the array passed. Like everything else, clicking a filter updates the results with AJAX and the filter is retained in searches.





Finally, the last thing I need to do is add a column with some buttons. You can specify a normal array for buttons to show in the header, and a lambda functions to return an array for buttons to show for each row:
$table->rootButtons = array( 'add' => array( 'icon' => array( 'icons/add.png', 'core' ), 'title' => 'members_add', 'link' => 'app=members&module=members&section=members&do=add', ) ); $table->rowButtons = function( $row ) { return array( 'edit' => array( 'icon' => array( 'icons/edit.png', 'core' ), 'title' => 'edit', 'link' => 'app=members&module=members&section=members&do=edit&id=' . $row['member_id'], ), 'delete' => array( 'icon' => array( 'icons/delete.png', 'core' ), 'title' => 'delete', 'link' => 'app=members&module=members&section=members&do=delete&id=' . $row['member_id'], 'class' => 'delete', ), ); };




Our finished table looks like this:


And behaves like this:
http://screencast.com/t/KMFq8zCE



To recap, here's the code, in it's entirety to generate that table:


/* Create the table */ $table = new IPSHelpersTableDb( 'core_members', 'app=core&module=members&section=members' ); $table->langPrefix = 'members_'; /* Columns we need */ $table->include = array( 'photo', 'name', 'email', 'joined', 'member_group_id', 'ip_address' ); $table->mainColumn = 'name'; $table->noSort = array( 'photo' ); /* Default sort options */ $table->sortBy = $table->sortBy ?: 'joined'; $table->sortDirection = $table->sortDirection ?: 'desc'; /* Filters */ $table->joins = array( array( 'from' => array( 'core_validating' => 'v' ), 'where' => 'v.member_id=_0.member_id' ) ); $table->filters = array( 'members_filter_banned' => 'member_banned=1', 'members_filter_locked' => 'failed_login_count>=' . (int) IPSSettings::i()->ipb_bruteforce_attempts, /*@todo*/ 'members_filter_spam' => '(members_bitoptions & ' . IPSMember::$bitOptions['bw_is_spammer'] . ') != 0', 'members_filter_validating' => 'v.lost_pass=0 AND v.vid IS NOT NULL' ); /* Groups for advanced filter (need to do it this way because array_merge renumbers the result */ $groups = array( '' => 'any_group' ); foreach ( IPSMemberGroup::groups() as $k => $v ) { $groups[ $k ] = $v; } /* Search */ $table->quickSearch = 'name'; $table->advancedSearch = array( 'member_id' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'email' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'ip_address' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'member_group_id' => array( IPSHelpersTableSEARCH_SELECT, array( 'options' => $groups ), function( $val ) { return array( 'member_group_id=? OR ? IN(mgroup_others)', $val, $val ); } ), 'joined' => IPSHelpersTableSEARCH_DATE_RANGE, ); /* Custom parsers */ $table->parsers = array( 'photo' => function( $val, $row ) { return IPSMember::constructFromData( $row )->photo('mini'); }, 'joined' => function( $val, $row ) { return IPSDateTime::ts( $val )->localeDate(); }, 'member_group_id' => function( $val, $row ) { return IPSMemberGroup::load( $val )->formattedName(); } ); /* Specify the buttons */ $table->rootButtons = array( 'add' => array( 'icon' => array( 'icons/add.png', 'core' ), 'title' => 'members_add', 'link' => 'app=members&module=members&section=members&do=add', ) ); $table->rowButtons = function( $row ) { return array( 'edit' => array( 'icon' => array( 'icons/edit.png', 'core' ), 'title' => 'edit', 'link' => 'app=members&module=members&section=members&do=edit&id=' . $row['member_id'], ), 'delete' => array( 'icon' => array( 'icons/delete.png', 'core' ), 'title' => 'delete', 'link' => 'app=members&module=members&section=members&do=delete&id=' . $row['member_id'], 'class' => 'delete', ), ); }; /* Display */ IPSOutput::i()->output = IPSOutput::i()->getTemplate( 'global' )->block( 'members', $table );


  • 21,239 views

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.